diff --git a/util/test/demos/demos.vcxproj b/util/test/demos/demos.vcxproj
index ae75e5aad..b39934009 100644
--- a/util/test/demos/demos.vcxproj
+++ b/util/test/demos/demos.vcxproj
@@ -283,6 +283,7 @@
+
diff --git a/util/test/demos/demos.vcxproj.filters b/util/test/demos/demos.vcxproj.filters
index 433808646..8d1a84458 100644
--- a/util/test/demos/demos.vcxproj.filters
+++ b/util/test/demos/demos.vcxproj.filters
@@ -739,6 +739,9 @@
D3D11\demos
+
+ OpenGL\demos
+
diff --git a/util/test/demos/gl/gl_shader_debug_zoo.cpp b/util/test/demos/gl/gl_shader_debug_zoo.cpp
new file mode 100644
index 000000000..5a68a99a7
--- /dev/null
+++ b/util/test/demos/gl/gl_shader_debug_zoo.cpp
@@ -0,0 +1,675 @@
+/******************************************************************************
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2019-2025 Baldur Karlsson
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ ******************************************************************************/
+
+#include "gl_test.h"
+
+RD_TEST(GL_Shader_Debug_Zoo, OpenGLGraphicsTest)
+{
+ static constexpr const char *Description =
+ "Tests shader debugging on GL shaders, focussing on GL-specific functionality that is not "
+ "available or implemented separately from Vulkan.";
+
+ std::string vertex = R"EOSHADER(
+
+#ifndef LOCATION
+#define LOCATION layout
+#endif
+
+out gl_PerVertex
+{
+ vec4 gl_Position;
+};
+
+LOCATION(location = 0) in vec3 inPosition;
+LOCATION(location = 1) in vec4 inColor;
+LOCATION(location = 2) in vec2 inUV;
+
+LOCATION(location = 1) out vec4 v2fColor;
+LOCATION(location = 2) out vec2 v2fUV;
+
+#ifdef MIXED
+// no location interpolators
+in float a2v_a;
+out float v2f_a;
+#endif
+
+#ifdef BLOCK
+LOCATION(location = 3) in float a2v_a;
+LOCATION(location = 4) in int a2v_b;
+
+LOCATION(location = 3) out v2fBlock
+{
+ float a;
+ flat int b;
+} v2f;
+#endif
+
+#ifdef WEIRD_V2F
+LOCATION(location = 3) in float a2v_a;
+LOCATION(location = 4) in int a2v_b;
+
+LOCATION(location = 3) out mat3 v2fMat;
+LOCATION(location = 6) out float v2fArr[2];
+#endif
+#ifdef MULTI
+LOCATION(location = 8) flat out int v2fInstData;
+#endif
+
+void main()
+{
+ gl_Position = vec4(inPosition.xyz, 1);
+ v2fColor = inColor;
+ v2fUV = inUV;
+
+#ifdef BLOCK
+ v2f.a = a2v_a;
+ v2f.b = a2v_b;
+#endif
+
+#ifdef MIXED
+ v2f_a = a2v_a;
+#endif
+
+#ifdef WEIRD_V2F
+ v2fMat = mat3(a2v_a * inPosition.xyz, (a2v_a + 1) * inPosition.xyz, (a2v_a + 2) * inPosition.xyz);
+ v2fArr[0] = sqrt(a2v_a);
+ v2fArr[1] = cos(a2v_a);
+#endif
+
+#ifdef MULTI
+ v2fInstData = a2v_b + gl_VertexID/3;
+ if(gl_InstanceID != 1 && a2v_a > 5.0)
+ gl_Position.y += 10.0f;
+#endif
+}
+
+)EOSHADER";
+
+ std::string pixel = R"EOSHADER(
+
+#ifndef LOCATION
+#define LOCATION layout
+#endif
+
+LOCATION(location = 1) in vec4 v2fColor;
+LOCATION(location = 2) in vec2 v2fUV;
+
+LOCATION(location = 0) out vec4 outColor;
+
+#ifdef MIXED
+// no location interpolators
+in float v2f_a;
+#endif
+
+#ifdef BLOCK
+LOCATION(location = 3) in v2fBlock
+{
+ float a;
+ flat int b;
+} v2f;
+#endif
+
+#ifdef WEIRD_V2F
+LOCATION(location = 3) in mat3 v2fMat;
+LOCATION(location = 6) in float v2fArr[2];
+#endif
+#ifdef MULTI
+LOCATION(location = 8) flat in int v2fInstData;
+#endif
+
+void main()
+{
+ outColor = v2fColor + v2fUV.xyxy;
+
+#ifdef BLOCK
+ outColor.r += v2f.a;
+ outColor.g += float(v2f.b);
+#endif
+
+#ifdef MIXED
+ outColor.r += v2f_a;
+#endif
+
+#ifdef WEIRD_V2F
+ outColor.rgb += v2fMat * outColor.rgb;
+ outColor.g += v2fArr[0] * v2fArr[1];
+#endif
+
+#ifdef MULTI
+ if(v2fInstData != 51) discard;
+#endif
+}
+
+)EOSHADER";
+
+ std::string bindingZooVertex = R"EOSHADER(
+
+#version 460 core
+
+layout(location = 0) in vec3 inPosition;
+layout(location = 1) in vec4 inColor;
+layout(location = 2) in vec2 inUV;
+
+layout(location = 1) out vec4 v2fColor;
+layout(location = 2) out vec2 v2fUV;
+
+void main()
+{
+ gl_Position = vec4(inPosition.xyz, 1);
+ v2fColor = inColor;
+ v2fUV = inUV;
+}
+
+)EOSHADER";
+
+ std::string bindingZooPixel = R"EOSHADER(
+
+#version 460 core
+
+// have uniforms unused - our glslang recompilation will likely assign these locations but drivers likely won't.
+uniform int unused;
+
+// ensure alphabetical order does not match declaration order, also to test that we don't depend on that
+// by accident
+uniform vec4 zzzTest;
+
+uniform mat4 matrix_test;
+
+uniform float array_test[4];
+
+struct TestStruct
+{
+ float a;
+ int b;
+};
+
+uniform TestStruct struct_test;
+
+layout(binding = 0, std140) uniform ubo_test
+{
+ vec4 data;
+} ubo;
+
+layout(binding = 0, std430) buffer ssbo_test
+{
+ vec4 data;
+} ssbo;
+
+layout(binding = 0) uniform sampler2D tex2d_test;
+layout(binding = 1) uniform samplerBuffer texBuf_test;
+
+layout(location = 1) in vec4 v2fColor;
+layout(location = 2) in vec2 v2fUV;
+
+layout(location = 0) out vec4 outColor;
+
+void main()
+{
+ vec4 col = vec4(0, 0, 0, 0);
+ col += matrix_test * zzzTest;
+ col.r += array_test[0];
+ col.g += array_test[1];
+ col.b += array_test[2];
+ col.a += array_test[3];
+ col += float(struct_test.b) * struct_test.a;
+ col += ubo.data;
+ col += ssbo.data;
+ col += dFdx(v2fUV).xyxy;
+ col += texture(tex2d_test, v2fUV);
+ col += texelFetch(texBuf_test, int(v2fUV.x*10));
+ outColor = col;
+}
+
+)EOSHADER";
+
+ void Prepare(int argc, char **argv)
+ {
+ OpenGLGraphicsTest::Prepare(argc, argv);
+
+ if(!Avail.empty())
+ return;
+
+ if(!SpvCompilationSupported())
+ Avail = InternalSpvCompiler() ? "Internal SPIR-V compiler did not initialise"
+ : "Couldn't find 'glslc' or 'glslangValidator' in PATH - "
+ "required for SPIR-V compilation";
+ }
+
+ int main()
+ {
+ // initialise, create window, create context, etc
+ if(!Init())
+ return 3;
+
+ GLuint vao = MakeVAO();
+ glBindVertexArray(vao);
+
+ const DefaultA2V tri[9] = {
+ {Vec3f(-1.0f, 1.0f, 0.0f), Vec4f(0.0f, 1.0f, 0.0f, 1.0f), Vec2f(0.0f, 0.0f)},
+ {Vec3f(1.0f, 1.0f, 0.0f), Vec4f(0.0f, 1.0f, 0.0f, 1.0f), Vec2f(0.0f, 1.0f)},
+ {Vec3f(-1.0f, -1.0f, 0.0f), Vec4f(0.0f, 1.0f, 0.0f, 1.0f), Vec2f(1.0f, 0.0f)},
+
+ {Vec3f(-1.0f, 1.0f, 0.0f), Vec4f(0.0f, 0.0f, 1.0f, 1.0f), Vec2f(0.0f, 0.0f)},
+ {Vec3f(1.0f, 1.0f, 0.0f), Vec4f(0.0f, 0.0f, 1.0f, 1.0f), Vec2f(0.0f, 1.0f)},
+ {Vec3f(-1.0f, -1.0f, 0.0f), Vec4f(0.0f, 0.0f, 1.0f, 1.0f), Vec2f(1.0f, 0.0f)},
+
+ {Vec3f(-1.0f, 1.0f, 0.0f), Vec4f(1.0f, 0.0f, 0.0f, 1.0f), Vec2f(0.0f, 0.0f)},
+ {Vec3f(1.0f, 1.0f, 0.0f), Vec4f(1.0f, 0.0f, 0.0f, 1.0f), Vec2f(0.0f, 1.0f)},
+ {Vec3f(-1.0f, -1.0f, 0.0f), Vec4f(1.0f, 0.0f, 0.0f, 1.0f), Vec2f(1.0f, 0.0f)},
+ };
+
+ GLuint vb = MakeBuffer();
+ glBindBuffer(GL_ARRAY_BUFFER, vb);
+ glBufferStorage(GL_ARRAY_BUFFER, sizeof(tri), tri, 0);
+
+ ConfigureDefaultVAO();
+
+ struct ExtraA2V
+ {
+ float a;
+ int b;
+ };
+
+ const ExtraA2V tri2[6] = {
+ {2.5f, 50}, {3.5f, 7}, {4.5f, 8}, {5.5f, 40}, {6.5f, 50}, {7.5f, 60},
+ };
+
+ GLuint vb2 = MakeBuffer();
+ glBindBuffer(GL_ARRAY_BUFFER, vb2);
+ glBufferStorage(GL_ARRAY_BUFFER, sizeof(tri2), tri2, 0);
+
+ glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, sizeof(ExtraA2V), (void *)(0));
+ glVertexAttribDivisor(3, 1);
+ glVertexAttribIPointer(4, 1, GL_INT, sizeof(ExtraA2V), (void *)(sizeof(float)));
+ glVertexAttribDivisor(4, 1);
+
+ glEnableVertexAttribArray(3);
+ glEnableVertexAttribArray(4);
+
+ std::string vs = "#version 460 core\n";
+ vs += vertex;
+
+ std::string ps = "#version 460 core\n";
+ ps += pixel;
+
+ GLuint spirvprogram = MakeProgram();
+ GLuint spirvStrippedProgram = MakeProgram();
+
+ {
+ GLuint vsShad = glCreateShader(GL_VERTEX_SHADER);
+ GLuint psShad = glCreateShader(GL_FRAGMENT_SHADER);
+
+ std::vector vsSPIRV =
+ CompileShaderToSpv(vs, SPIRVTarget::opengl, ShaderLang::glsl, ShaderStage::vert, "main");
+ std::vector fsSPIRV =
+ CompileShaderToSpv(ps, SPIRVTarget::opengl, ShaderLang::glsl, ShaderStage::frag, "main");
+
+ glShaderBinary(1, &vsShad, GL_SHADER_BINARY_FORMAT_SPIR_V, vsSPIRV.data(),
+ (GLsizei)vsSPIRV.size() * 4);
+ glShaderBinary(1, &psShad, GL_SHADER_BINARY_FORMAT_SPIR_V, fsSPIRV.data(),
+ (GLsizei)fsSPIRV.size() * 4);
+
+ std::string entry_point = "main";
+ glSpecializeShaderARB(vsShad, (const GLchar *)entry_point.c_str(), 0, nullptr, nullptr);
+ glSpecializeShaderARB(psShad, (const GLchar *)entry_point.c_str(), 0, nullptr, nullptr);
+
+ glAttachShader(spirvprogram, vsShad);
+ glAttachShader(spirvprogram, psShad);
+
+ glLinkProgram(spirvprogram);
+
+ glDetachShader(spirvprogram, vsShad);
+ glDetachShader(spirvprogram, psShad);
+
+ glDeleteShader(vsShad);
+ glDeleteShader(psShad);
+
+ // strip any OpName/OpMemberName
+
+ for(size_t i = 5; i < vsSPIRV.size();)
+ {
+ uint16_t WordCount = vsSPIRV[i] >> 16;
+ uint16_t opcode = vsSPIRV[i] & 0xffff;
+
+ // OpName / OpMemberName
+ if(opcode == 5 || opcode == 6)
+ {
+ vsSPIRV.erase(vsSPIRV.begin() + i, vsSPIRV.begin() + i + WordCount);
+ continue;
+ }
+
+ i += WordCount;
+ }
+
+ for(size_t i = 5; i < fsSPIRV.size();)
+ {
+ uint16_t WordCount = fsSPIRV[i] >> 16;
+ uint16_t opcode = fsSPIRV[i] & 0xffff;
+
+ // OpName / OpMemberName
+ if(opcode == 5 || opcode == 6)
+ {
+ fsSPIRV.erase(fsSPIRV.begin() + i, fsSPIRV.begin() + i + WordCount);
+ continue;
+ }
+
+ i += WordCount;
+ }
+
+ vsShad = glCreateShader(GL_VERTEX_SHADER);
+ psShad = glCreateShader(GL_FRAGMENT_SHADER);
+
+ glShaderBinary(1, &vsShad, GL_SHADER_BINARY_FORMAT_SPIR_V, vsSPIRV.data(),
+ (GLsizei)vsSPIRV.size() * 4);
+ glShaderBinary(1, &psShad, GL_SHADER_BINARY_FORMAT_SPIR_V, fsSPIRV.data(),
+ (GLsizei)fsSPIRV.size() * 4);
+
+ glSpecializeShaderARB(vsShad, (const GLchar *)entry_point.c_str(), 0, nullptr, nullptr);
+ glSpecializeShaderARB(psShad, (const GLchar *)entry_point.c_str(), 0, nullptr, nullptr);
+
+ glAttachShader(spirvStrippedProgram, vsShad);
+ glAttachShader(spirvStrippedProgram, psShad);
+
+ glLinkProgram(spirvStrippedProgram);
+
+ glDetachShader(spirvStrippedProgram, vsShad);
+ glDetachShader(spirvStrippedProgram, psShad);
+
+ glDeleteShader(vsShad);
+ glDeleteShader(psShad);
+ }
+
+ GLuint simpleModernGLSLProgram = MakeProgram(vs, ps);
+
+ vs = "#version 460 core\n";
+ vs += "#define LOCATION(x)\n";
+ vs += vertex;
+
+ ps = "#version 460 core\n";
+ ps += "#define LOCATION(x)\n";
+ ps += pixel;
+
+ GLuint noLocationGLSLProgram = MakeProgram(vs, ps, "", [](GLuint prog) {
+ glBindAttribLocation(prog, 0, "inPosition");
+ glBindAttribLocation(prog, 1, "inColor");
+ glBindAttribLocation(prog, 2, "inUV");
+ });
+
+ vs = "#version 460 core\n";
+ vs += "#define BLOCK 1\n";
+ vs += vertex;
+
+ ps = "#version 460 core\n";
+ ps += "#define BLOCK 1\n";
+ ps += pixel;
+
+ GLuint blockLocationGLSLProgram = MakeProgram(vs, ps);
+
+ vs = "#version 460 core\n";
+ vs += "#define BLOCK 1\n";
+ vs += "#define LOCATION(x)\n";
+ vs += vertex;
+
+ ps = "#version 460 core\n";
+ ps += "#define BLOCK 1\n";
+ ps += "#define LOCATION(x)\n";
+ ps += pixel;
+
+ GLuint blockNoLocationGLSLProgram = MakeProgram(vs, ps, "", [](GLuint prog) {
+ glBindAttribLocation(prog, 0, "inPosition");
+ glBindAttribLocation(prog, 1, "inColor");
+ glBindAttribLocation(prog, 2, "inUV");
+ glBindAttribLocation(prog, 3, "a2v_a");
+ glBindAttribLocation(prog, 4, "a2v_b");
+ });
+
+ vs = "#version 460 core\n";
+ vs += "#define MIXED 1\n";
+ vs += vertex;
+
+ ps = "#version 460 core\n";
+ ps += "#define MIXED 1\n";
+ ps += pixel;
+
+ GLuint mixedLocationGLSLProgram =
+ MakeProgram(vs, ps, "", [](GLuint prog) { glBindAttribLocation(prog, 3, "a2v_a"); });
+
+ vs = "#version 460 core\n";
+ vs += "#define WEIRD_V2F 1\n";
+ vs += vertex;
+
+ ps = "#version 460 core\n";
+ ps += "#define WEIRD_V2F 1\n";
+ ps += pixel;
+
+ GLuint weirdLocationGLSLProgram = MakeProgram(vs, ps);
+
+ GLuint pipeline = MakePipeline();
+ GLuint pipelineVert = MakePipelineProgram(GL_VERTEX_SHADER, vs);
+ GLuint pipelinePix = MakePipelineProgram(GL_FRAGMENT_SHADER, ps);
+ glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, pipelineVert);
+ glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, pipelinePix);
+
+ vs = "#version 460 core\n";
+ vs += "#define WEIRD_V2F 1\n";
+ vs += "#define MULTI 1\n";
+ vs += vertex;
+
+ ps = "#version 460 core\n";
+ ps += "#define WEIRD_V2F 1\n";
+ ps += "#define MULTI 1\n";
+ ps += pixel;
+
+ GLuint multiGLSLProgram = MakeProgram(vs, ps);
+
+ vs = "#version 460 core\n";
+ vs += "#define WEIRD_V2F 1\n";
+ vs += "#define LOCATION(x)\n";
+ vs += vertex;
+
+ ps = "#version 460 core\n";
+ ps += "#define WEIRD_V2F 1\n";
+ ps += "#define LOCATION(x)\n";
+ ps += pixel;
+
+ GLuint weirdNoLocationGLSLProgram = MakeProgram(vs, ps, "", [](GLuint prog) {
+ glBindAttribLocation(prog, 0, "inPosition");
+ glBindAttribLocation(prog, 1, "inColor");
+ glBindAttribLocation(prog, 2, "inUV");
+ glBindAttribLocation(prog, 3, "a2v_a");
+ glBindAttribLocation(prog, 4, "a2v_b");
+ });
+
+ GLuint bindingZooProgram = MakeProgram(bindingZooVertex, bindingZooPixel);
+ (void)bindingZooProgram;
+
+ glUseProgram(bindingZooProgram);
+
+ glUniform4f(glGetUniformLocation(bindingZooProgram, "zzzTest"), 0.2f, 0.3f, 0.4f, 0.5f);
+ float mat[16] = {};
+ for(int i = 0; i < 16; i++)
+ mat[i] = 0.6f + float(i) / 10.0f;
+ glUniformMatrix4fv(glGetUniformLocation(bindingZooProgram, "matrix_test"), 1, GL_FALSE, mat);
+ float arr[4] = {};
+ for(int i = 0; i < 4; i++)
+ arr[i] = 2.1f + float(i) / 10.0f;
+ glUniform1fv(glGetUniformLocation(bindingZooProgram, "array_test"), 4, arr);
+ glUniform1f(glGetUniformLocation(bindingZooProgram, "struct_test.a"), 9.9f);
+ glUniform1i(glGetUniformLocation(bindingZooProgram, "struct_test.b"), 99);
+
+ const size_t bindOffset = 16;
+
+ Vec4f cbufferdata[1024 + bindOffset];
+
+ for(int i = 0; i < bindOffset; i++)
+ cbufferdata[i] = Vec4f(-99.9f, -88.8f, -77.7f, -66.6f);
+
+ for(int i = 0; i < 1024; i++)
+ cbufferdata[bindOffset + i] =
+ Vec4f(float(i * 4 + 0), float(i * 4 + 1), float(i * 4 + 2), float(i * 4 + 3));
+
+ GLuint cb = MakeBuffer();
+ glBindBuffer(GL_UNIFORM_BUFFER, cb);
+ glBufferStorage(GL_UNIFORM_BUFFER, sizeof(cbufferdata), cbufferdata, GL_MAP_WRITE_BIT);
+
+ glBindBufferRange(GL_UNIFORM_BUFFER, 0, cb, bindOffset * sizeof(Vec4f), 16 * sizeof(Vec4f));
+
+ GLuint ssbo = MakeBuffer();
+ glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
+ glBufferStorage(GL_SHADER_STORAGE_BUFFER, sizeof(cbufferdata), cbufferdata, GL_MAP_WRITE_BIT);
+
+ glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, ssbo, bindOffset * sizeof(Vec4f) * 2,
+ 16 * sizeof(Vec4f));
+
+ GLuint tbuf_store = MakeBuffer();
+ glBindBuffer(GL_TEXTURE_BUFFER, tbuf_store);
+ glBufferStorage(GL_TEXTURE_BUFFER, sizeof(Vec4f) * 16,
+ cbufferdata + bindOffset * sizeof(Vec4f) * 3, 0);
+
+ GLuint tbuf = MakeTexture();
+ glBindTexture(GL_TEXTURE_BUFFER, tbuf);
+ glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32F, tbuf_store);
+
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_BUFFER, tbuf);
+
+ Texture rgba8;
+ LoadXPM(SmileyTexture, rgba8);
+
+ glActiveTexture(GL_TEXTURE0);
+ GLuint smiley = MakeTexture();
+ glBindTexture(GL_TEXTURE_2D, smiley);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, rgba8.width, rgba8.height);
+ glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, rgba8.width, rgba8.height, GL_RGBA, GL_UNSIGNED_BYTE,
+ rgba8.data.data());
+
+ // render offscreen to make picked values accurate
+ GLuint fbo = MakeFBO();
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+ // Color render texture
+ GLuint colattach = MakeTexture();
+
+ glBindTexture(GL_TEXTURE_2D, colattach);
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, screenWidth, screenHeight);
+ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colattach, 0);
+
+ while(Running())
+ {
+ glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+ float col[] = {0.0f, 0.0f, 0.0f, 1.0f};
+ glClearBufferfv(GL_COLOR, 0, col);
+
+ glBindVertexArray(vao);
+
+ int test = 0;
+
+ pushMarker("GLSL tests");
+ {
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(simpleModernGLSLProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(noLocationGLSLProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(blockLocationGLSLProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(blockNoLocationGLSLProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(mixedLocationGLSLProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(weirdLocationGLSLProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(multiGLSLProgram);
+ glDrawArraysInstancedBaseInstance(GL_TRIANGLES, 3, 3, 3, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(multiGLSLProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 9);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(weirdNoLocationGLSLProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(bindingZooProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(0);
+ glBindProgramPipeline(pipeline);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glBindProgramPipeline(0);
+ }
+ popMarker();
+
+ pushMarker("SPIRV tests");
+ {
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(spirvprogram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+
+ glViewport(test * 4, screenHeight - 4, 4, 4);
+ test++;
+ glUseProgram(spirvStrippedProgram);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ }
+ popMarker();
+
+ glBlitNamedFramebuffer(fbo, 0, 0, 0, screenWidth, screenHeight, 0, 0, screenWidth,
+ screenHeight, GL_COLOR_BUFFER_BIT, GL_NEAREST);
+
+ Present();
+ }
+
+ return 0;
+ }
+};
+
+REGISTER_TEST();
diff --git a/util/test/demos/gl/gl_test.cpp b/util/test/demos/gl/gl_test.cpp
index 3e62b4192..c45ad7cd7 100644
--- a/util/test/demos/gl/gl_test.cpp
+++ b/util/test/demos/gl/gl_test.cpp
@@ -143,7 +143,8 @@ void OpenGLGraphicsTest::Shutdown()
delete mainWindow;
}
-GLuint OpenGLGraphicsTest::MakeProgram(std::string vertSrc, std::string fragSrc, std::string geomSrc)
+GLuint OpenGLGraphicsTest::MakeProgram(std::string vertSrc, std::string fragSrc,
+ std::string geomSrc, std::function prelink)
{
GLuint vs = vertSrc.empty() ? 0 : glCreateShader(GL_VERTEX_SHADER);
GLuint fs = fragSrc.empty() ? 0 : glCreateShader(GL_FRAGMENT_SHADER);
@@ -232,6 +233,9 @@ GLuint OpenGLGraphicsTest::MakeProgram(std::string vertSrc, std::string fragSrc,
if(!vs || !fs)
glProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
+ if(prelink)
+ prelink(program);
+
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &status);
diff --git a/util/test/demos/gl/gl_test.h b/util/test/demos/gl/gl_test.h
index a0e522f03..9a807b205 100644
--- a/util/test/demos/gl/gl_test.h
+++ b/util/test/demos/gl/gl_test.h
@@ -28,6 +28,7 @@
#include "3rdparty/glad/glad.h"
+#include
#include
struct OpenGLGraphicsTest : public GraphicsTest
@@ -44,7 +45,8 @@ struct OpenGLGraphicsTest : public GraphicsTest
void PostInit();
- GLuint MakeProgram(std::string vertSrc, std::string fragSrc, std::string geomSrc = "");
+ GLuint MakeProgram(std::string vertSrc, std::string fragSrc, std::string geomSrc = "",
+ std::function prelink = {});
GLuint MakeProgram(std::string compSrc);
GLuint MakeProgram();
GLuint MakePipelineProgram(GLenum type, std::string src);
diff --git a/util/test/tests/GL/GL_Shader_Debug_Zoo.py b/util/test/tests/GL/GL_Shader_Debug_Zoo.py
new file mode 100644
index 000000000..f202ae2fb
--- /dev/null
+++ b/util/test/tests/GL/GL_Shader_Debug_Zoo.py
@@ -0,0 +1,165 @@
+import renderdoc as rd
+from typing import List
+import rdtest
+
+
+class GL_Shader_Debug_Zoo(rdtest.TestCase):
+ demos_test_name = 'GL_Shader_Debug_Zoo'
+
+ def check_capture(self):
+ if not self.controller.GetAPIProperties().shaderDebugging:
+ rdtest.log.success("Shader debugging not enabled, skipping test")
+ return
+
+ failed = False
+
+ x = -3
+ for child in ["GLSL tests", "SPIRV tests"]:
+ rdtest.log.begin_section(child)
+ section = self.find_action(child)
+ for test in range(len(section.children)):
+ action = section.children[test]
+
+ if not (action.flags & rd.ActionFlags.Drawcall):
+ continue
+
+ x += 4
+
+ self.controller.SetFrameEvent(action.eventId, False)
+ pipe: rd.PipeState = self.controller.GetPipelineState()
+
+ if not pipe.GetShaderReflection(rd.ShaderStage.Vertex).debugInfo.debuggable:
+ rdtest.log.print("Skipping undebuggable shader at {} in {}.".format(test, child))
+ return
+
+ if not pipe.GetShaderReflection(rd.ShaderStage.Pixel).debugInfo.debuggable:
+ rdtest.log.print("Skipping undebuggable shader at {} in {}.".format(test, child))
+ return
+
+ y = 1
+
+ inputs = rd.DebugPixelInputs()
+ inputs.primitive = 0
+ inputs.sample = 0
+
+ if action.numIndices > 3:
+ rdtest.log.print("prim 1")
+ inputs.primitive = 1
+
+ # Debug the shader
+ trace: rd.ShaderDebugTrace = self.controller.DebugPixel(x, y, inputs)
+
+ rdtest.log.print(f"debugging {x},{y}")
+
+ if trace.debugger is None:
+ failed = True
+ rdtest.log.error("Test {} in sub-section {} did not debug pixel".format(test, child))
+ self.controller.FreeTrace(trace)
+ continue
+
+ _, variables = self.process_trace(trace)
+
+ output: rd.SourceVariableMapping = self.find_output_source_var(trace, rd.ShaderBuiltin.ColorOutput, 0)
+
+ debugged = self.evaluate_source_var(output, variables)
+
+ try:
+ self.check_pixel_value(pipe.GetOutputTargets()[0].resource, x, y, debugged.value.f32v[0:4])
+ except rdtest.TestFailureException as ex:
+ failed = True
+ rdtest.log.error("Test {} in sub-section {} did not match pixel. {}".format(test, child, str(ex)))
+ continue
+ finally:
+ self.controller.FreeTrace(trace)
+
+ rdtest.log.success("Test {} pixel in sub-section {} matched as expected".format(test, child))
+
+ vtx = 1
+ inst = 0
+ idx = vtx
+
+ if action.flags & rd.ActionFlags.Instanced:
+ inst = 1
+
+ if action.flags & rd.ActionFlags.Indexed:
+ ib = pipe.GetIBuffer()
+
+ mesh = rd.MeshFormat()
+ mesh.indexResourceId = ib.resourceId
+ mesh.indexByteStride = ib.byteStride
+ mesh.indexByteOffset = ib.byteOffset + action.indexOffset * ib.byteStride
+ mesh.indexByteSize = ib.byteSize
+ mesh.baseVertex = action.baseVertex
+
+ indices = rdtest.fetch_indices(self.controller, action, mesh, 0, vtx, 1)
+
+ idx = indices[1]
+
+ postvs = self.get_postvs(action, rd.MeshDataStage.VSOut, first_index=vtx, num_indices=1, instance=inst)
+
+ trace: rd.ShaderDebugTrace = self.controller.DebugVertex(vtx, inst, idx, 0)
+
+ if trace.debugger is None:
+ failed = True
+ rdtest.log.error("Test {} in sub-section {} did not debug vertex".format(test, child))
+ self.controller.FreeTrace(trace)
+ continue
+
+ _, variables = self.process_trace(trace)
+
+ outputs = 0
+
+ for var in trace.sourceVars:
+ var: rd.SourceVariableMapping
+ if var.variables[0].type == rd.DebugVariableType.Variable and var.signatureIndex >= 0:
+ name = var.name
+
+ if name not in postvs[0].keys():
+ name = name.replace(".", "Block.")
+ if name not in postvs[0].keys():
+ failed = True
+ rdtest.log.error("Don't have expected output for {}".format(name))
+ continue
+
+ expect = postvs[0][name]
+ value = self.evaluate_source_var(var, variables)
+
+ if len(expect) != value.columns:
+ failed = True
+ rdtest.log.error(
+ "Output {} at EID {} has different size ({} values) to expectation ({} values)"
+ .format(name, action.eventId, value.columns, len(expect)))
+ continue
+
+ compType = rd.VarTypeCompType(value.type)
+ if compType == rd.CompType.UInt:
+ debugged = list(value.value.u32v[0:value.columns])
+ elif compType == rd.CompType.SInt:
+ debugged = list(value.value.s32v[0:value.columns])
+ else:
+ debugged = list(value.value.f32v[0:value.columns])
+
+ if not rdtest.value_compare(expect, debugged):
+ failed = True
+ rdtest.log.error("Test {} in sub-section {} did not match vertex.\nExpected {} but got {}".format(test, child, expect, debugged))
+ break
+
+ is_eq, diff_amt = rdtest.value_compare_diff(expect, debugged, eps=5.0E-06)
+ if not is_eq:
+ failed = True
+ rdtest.log.error(
+ "Debugged value {} at EID {} vert {} (idx {}) instance {}: {} difference. {} doesn't exactly match postvs output {}".format(
+ name, action.eventId, vtx, idx, inst, diff_amt, debugged, expect))
+
+ outputs = outputs + 1
+
+ self.controller.FreeTrace(trace)
+
+ rdtest.log.success("Test {} vertex in sub-section {} matched as expected".format(test, child))
+
+ rdtest.log.end_section(child)
+
+ if failed:
+ raise rdtest.TestFailureException("Some tests were not as expected")
+
+ rdtest.log.success("All tests matched")