diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 278119c1..c144b4f6 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -108,6 +108,7 @@ add_executable(MobileGLIntegrationTest Scenarios/AtomicCounterScenario.cpp Scenarios/SsboArrayDynamicIndexScenario.cpp Scenarios/StorageBufferRegrowScenario.cpp + Scenarios/SpirvShaderBinaryScenario.cpp Scenarios/RelinkStageSetScenario.cpp Scenarios/GuiBatchScenario.cpp Scenarios/UnboundImageDescriptorScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp new file mode 100644 index 00000000..05b27727 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp @@ -0,0 +1,324 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - AN APPLICATION-SUPPLIED SPIR-V MODULE RENDERS, END TO END. +// +// GL_ARB_gl_spirv is core in 4.6 and MobileGL advertises a 4.6 context, but glShaderBinary and +// glSpecializeShader were DECLARE_GL_FUNCTION_STUB entry points: they took their arguments, +// recorded no error and did nothing, and glGetShaderiv(GL_SPIR_V_BINARY) raised GL_INVALID_ENUM. +// Every gl_spirv conformance body died on the first of those two calls. +// +// This scenario is the end-to-end proof that the path now WORKS rather than merely answers: two +// modules that glslang compiled ahead of time (embedded below as words, so the test depends on +// no toolchain at run time), handed to glShaderBinary, specialized with a scale and a channel +// index, linked, drawn, and read back. It runs on both backends and, in CI, on llvmpipe/lavapipe. +// +// The two specialization constants are the load-bearing part. The vertex module scales its +// position by constant id 3 and the fragment module writes 1.0 into the channel named by constant +// id 7 - so a specialization that silently did nothing would leave the default scale of 1.0 (a +// full-viewport quad instead of a quarter-sized one) and the default channel 0 (red instead of +// green), and BOTH would show up in the readback. A "specialization" that merely stored the +// values without folding them in is exactly the failure mode this shape is built to catch. +// +// The GLSL the modules came from: +// vertex: layout(location = 0) in vec2 aPos; +// layout(constant_id = 3) const float uScale = 1.0; +// void main() { gl_Position = vec4(aPos * uScale, 0.0, 1.0); } +// fragment: layout(location = 0) out vec4 oColor; +// layout(constant_id = 7) const int uChannel = 0; +// void main() { vec4 c = vec4(0,0,0,1); c[uChannel] = 1.0; oColor = c; } +// compiled with `glslangValidator -G --target-env opengl`. + +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +#ifndef GL_SHADER_BINARY_FORMAT_SPIR_V +#define GL_SHADER_BINARY_FORMAT_SPIR_V 0x9551 +#endif +#ifndef GL_SPIR_V_BINARY +#define GL_SPIR_V_BINARY 0x9552 +#endif + +namespace MGITest { + namespace { + + class SpirvShaderBinaryScenario : public ScenarioTest {}; + + // 255 words + const unsigned int kVertexModule[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x00000020u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu, + 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, + 0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000012u, 0x0000001eu, + 0x0000001fu, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, + 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu, + 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, + 0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu, + 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u, + 0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, 0x00040005u, 0x00000012u, 0x736f5061u, 0x00000000u, + 0x00040005u, 0x00000014u, 0x61635375u, 0x0000656cu, 0x00050005u, 0x0000001eu, 0x565f6c67u, 0x65747265u, + 0x00444978u, 0x00060005u, 0x0000001fu, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u, + 0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u, + 0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, + 0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000012u, + 0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x00000001u, 0x00000003u, 0x00040047u, 0x0000001eu, + 0x0000000bu, 0x00000005u, 0x00040047u, 0x0000001fu, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u, + 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, + 0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u, + 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, 0x0000000bu, + 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, + 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u, + 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, 0x00000006u, 0x00000002u, + 0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000001u, + 0x00040032u, 0x00000006u, 0x00000014u, 0x3f800000u, 0x0004002bu, 0x00000006u, 0x00000016u, 0x00000000u, + 0x0004002bu, 0x00000006u, 0x00000017u, 0x3f800000u, 0x00040020u, 0x0000001bu, 0x00000003u, 0x00000007u, + 0x00040020u, 0x0000001du, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x0000001du, 0x0000001eu, 0x00000001u, + 0x0004003bu, 0x0000001du, 0x0000001fu, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, + 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000010u, 0x00000013u, 0x00000012u, 0x0005008eu, + 0x00000010u, 0x00000015u, 0x00000013u, 0x00000014u, 0x00050051u, 0x00000006u, 0x00000018u, 0x00000015u, + 0x00000000u, 0x00050051u, 0x00000006u, 0x00000019u, 0x00000015u, 0x00000001u, 0x00070050u, 0x00000007u, + 0x0000001au, 0x00000018u, 0x00000019u, 0x00000016u, 0x00000017u, 0x00050041u, 0x0000001bu, 0x0000001cu, + 0x0000000du, 0x0000000fu, 0x0003003eu, 0x0000001cu, 0x0000001au, 0x000100fdu, 0x00010038u, + }; + + // 134 words + const unsigned int kFragmentModule[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x00000014u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu, + 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, + 0x0006000fu, 0x00000004u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000012u, 0x00030010u, 0x00000004u, + 0x00000008u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, + 0x00030005u, 0x00000009u, 0x00000063u, 0x00050005u, 0x0000000eu, 0x61684375u, 0x6c656e6eu, 0x00000000u, + 0x00040005u, 0x00000012u, 0x6c6f436fu, 0x0000726fu, 0x00040047u, 0x0000000eu, 0x00000001u, 0x00000007u, + 0x00040047u, 0x00000012u, 0x0000001eu, 0x00000000u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, + 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, + 0x00040020u, 0x00000008u, 0x00000007u, 0x00000007u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x00000000u, + 0x0004002bu, 0x00000006u, 0x0000000bu, 0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000cu, 0x0000000au, + 0x0000000au, 0x0000000au, 0x0000000bu, 0x00040015u, 0x0000000du, 0x00000020u, 0x00000001u, 0x00040032u, + 0x0000000du, 0x0000000eu, 0x00000000u, 0x00040020u, 0x0000000fu, 0x00000007u, 0x00000006u, 0x00040020u, + 0x00000011u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000003u, 0x00050036u, + 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003bu, 0x00000008u, + 0x00000009u, 0x00000007u, 0x0003003eu, 0x00000009u, 0x0000000cu, 0x00050041u, 0x0000000fu, 0x00000010u, + 0x00000009u, 0x0000000eu, 0x0003003eu, 0x00000010u, 0x0000000bu, 0x0004003du, 0x00000007u, 0x00000013u, + 0x00000009u, 0x0003003eu, 0x00000012u, 0x00000013u, 0x000100fdu, 0x00010038u, + }; + + + // The quad the vertex module transforms. Full-viewport before the scale, so a scale of + // 0.5 covers exactly the middle half of each axis and the corners stay background. + const float kQuad[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f}; + + // The specialization constant ids the two modules declare. + constexpr unsigned int kScaleConstantId = 3; + constexpr unsigned int kChannelConstantId = 7; + + unsigned int MakeSpirvShader(GLenum type, const unsigned int* words, size_t wordCount, + unsigned int constantId, unsigned int constantValue, std::string* outLog) { + const GLuint shader = glCreateShader(type); + glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, words, + static_cast(wordCount * sizeof(unsigned int))); + if (glGetError() != GL_NO_ERROR) { + if (outLog) *outLog = "glShaderBinary rejected the module"; + glDeleteShader(shader); + return 0; + } + + GLint isSpirv = GL_FALSE; + glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); + if (glGetError() != GL_NO_ERROR || isSpirv != GL_TRUE) { + if (outLog) *outLog = "GL_SPIR_V_BINARY did not read TRUE after glShaderBinary"; + glDeleteShader(shader); + return 0; + } + + glSpecializeShader(shader, "main", 1, &constantId, &constantValue); + GLint compiled = GL_FALSE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (compiled != GL_TRUE) { + if (outLog) { + GLint length = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); + std::vector log(static_cast(length > 0 ? length : 1), '\0'); + glGetShaderInfoLog(shader, static_cast(log.size()), nullptr, log.data()); + *outLog = std::string(log.data()); + } + glDeleteShader(shader); + return 0; + } + return shader; + } + + } // namespace + + TEST_F(SpirvShaderBinaryScenario, ShaderBinaryFormatIsAdvertisedExactlyOnce) { + if (!Ready()) return; + + GLint formatCount = -1; + glGetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &formatCount); + EXPECT_EQ(FirstGLError(), 0u); + ASSERT_EQ(formatCount, 1) << "a 4.6 context supports exactly the SPIR-V shader binary format"; + + std::vector formats(static_cast(formatCount), 0); + glGetIntegerv(GL_SHADER_BINARY_FORMATS, formats.data()); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_EQ(formats[0], static_cast(GL_SHADER_BINARY_FORMAT_SPIR_V)) + << "the count and the list have to describe the same thing"; + } + + TEST_F(SpirvShaderBinaryScenario, AnUnsupportedBinaryFormatIsRejectedInsteadOfSilentlyAccepted) { + if (!Ready()) return; + + const GLuint shader = glCreateShader(GL_VERTEX_SHADER); + // 0x8DF9 is GL_SHADER_BINARY_FORMATS' neighbour, not a format: any value but + // GL_SHADER_BINARY_FORMAT_SPIR_V is GL_INVALID_ENUM. The stub used to return silently. + glShaderBinary(1, &shader, 0x8DF9, kVertexModule, sizeof(kVertexModule)); + EXPECT_EQ(FirstGLError(), static_cast(GL_INVALID_ENUM)); + + GLint isSpirv = GL_TRUE; + glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_EQ(isSpirv, GL_FALSE) << "a rejected glShaderBinary must not have attached anything"; + + glDeleteShader(shader); + } + + TEST_F(SpirvShaderBinaryScenario, CompileShaderOnASpirvShaderIsInvalidOperationAndShaderSourceTakesItBack) { + if (!Ready()) return; + + const GLuint shader = glCreateShader(GL_VERTEX_SHADER); + glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ASSERT_EQ(FirstGLError(), 0u); + + glCompileShader(shader); + EXPECT_EQ(FirstGLError(), static_cast(GL_INVALID_OPERATION)) + << "glSpecializeShader, not glCompileShader, is what compiles a SPIR-V shader"; + + // glShaderSource takes the object back to being a GLSL shader, and GL_SPIR_V_BINARY with + // it - the transition the conformance suite checks explicitly. + const char* source = "#version 450\nvoid main() { gl_Position = vec4(0.0); }\n"; + glShaderSource(shader, 1, &source, nullptr); + ASSERT_EQ(FirstGLError(), 0u); + GLint isSpirv = GL_TRUE; + glGetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(isSpirv, GL_FALSE); + glCompileShader(shader); + EXPECT_EQ(FirstGLError(), 0u) << "the object is an ordinary GLSL shader again"; + + glDeleteShader(shader); + } + + TEST_F(SpirvShaderBinaryScenario, SpecializeShaderRejectsAConstantTheModuleDoesNotDeclare) { + if (!Ready()) return; + + const GLuint shader = glCreateShader(GL_VERTEX_SHADER); + glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ASSERT_EQ(FirstGLError(), 0u); + + // 4242 is not one of the module's constant ids. A specialization failure is a COMPILE + // failure, not a GL error - the extension routes it through COMPILE_STATUS. + const unsigned int badId = 4242; + const unsigned int value = 0; + glSpecializeShader(shader, "main", 1, &badId, &value); + GLint compiled = GL_TRUE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + EXPECT_EQ(compiled, GL_FALSE); + GLint logLength = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength); + EXPECT_GT(logLength, 0) << "a failed specialization has to say why"; + + glDeleteShader(shader); + } + + TEST_F(SpirvShaderBinaryScenario, SpecializedModulesLinkAndRenderWithTheirConstantsApplied) { + if (!Ready()) return; + HeadlessGL& gl = Gl(); + const int width = gl.Width(); + const int height = gl.Height(); + ASSERT_GE(width, 16); + ASSERT_GE(height, 16); + + std::string log; + // Scale 0.5 as a float, handed over as the GLuint bit pattern the extension specifies. + unsigned int halfBits = 0; + const float half = 0.5f; + std::memcpy(&halfBits, &half, sizeof(halfBits)); + + const unsigned int vs = MakeSpirvShader(GL_VERTEX_SHADER, kVertexModule, + sizeof(kVertexModule) / sizeof(kVertexModule[0]), + kScaleConstantId, halfBits, &log); + ASSERT_NE(vs, 0u) << "vertex: " << log; + // Channel 1 is green; the module's own default is 0 (red), so a specialization that did + // nothing paints the wrong colour. + const unsigned int fs = MakeSpirvShader(GL_FRAGMENT_SHADER, kFragmentModule, + sizeof(kFragmentModule) / sizeof(kFragmentModule[0]), + kChannelConstantId, 1u, &log); + ASSERT_NE(fs, 0u) << "fragment: " << log; + + const GLuint program = glCreateProgram(); + glAttachShader(program, vs); + glAttachShader(program, fs); + glLinkProgram(program); + GLint linked = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (linked != GL_TRUE) { + GLint length = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length); + std::vector programLog(static_cast(length > 0 ? length : 1), '\0'); + glGetProgramInfoLog(program, static_cast(programLog.size()), nullptr, programLog.data()); + FAIL() << "linking two specialized SPIR-V modules failed: " << programLog.data(); + } + + BindDefaultFramebuffer(); + glViewport(0, 0, width, height); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + ClearTo(0.0f, 0.0f, 0.0f, 1.0f); + + GLuint vao = 0, vbo = 0; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(kQuad), kQuad, GL_STATIC_DRAW); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr); + glUseProgram(program); + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + EXPECT_EQ(FirstGLError(), 0u); + + const Image painted = ReadPixels(width, height); + const Rgba8 centre = painted.At(width / 2, height / 2); + EXPECT_LT(centre.r, 32) << "the fragment module wrote the wrong channel; constant id 7 was not applied"; + EXPECT_GT(centre.g, 224) << "the centre of a 0.5-scaled quad must be painted"; + + // A pixel just inside the corner is OUTSIDE the 0.5-scaled quad and must still be the + // clear colour - which is what proves constant id 3 reached the vertex module. At the + // default scale of 1.0 the quad covers the whole viewport and this pixel would be green. + const Rgba8 corner = painted.At(1, 1); + EXPECT_LT(corner.g, 32) << "the quad was not scaled; the vertex specialization constant was not applied"; + + glBindVertexArray(0); + glDeleteBuffers(1, &vbo); + glDeleteVertexArrays(1, &vao); + glDeleteProgram(program); + glDeleteShader(vs); + glDeleteShader(fs); + gl.EndFrame(); + } + +} // namespace MGITest diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index 67d4e9b8..fa736b59 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -4363,3 +4363,230 @@ TEST_F(ProgramTest, UniformEntryPointsRejectAnUnlinkedProgram) { DrainProgramTestErrors(); } + +// --------------------------------------------------------------------------------------------- +// GL_ARB_gl_spirv, core since 4.6. glShaderBinary and glSpecializeShader were +// DECLARE_GL_FUNCTION_STUB entry points - they took their arguments, recorded no error and did +// nothing - and glGetShaderiv(GL_SPIR_V_BINARY) fell into the terminal INVALID_ENUM arm, which is +// where all nine gl_spirv conformance bodies died. +// +// The module below is a real one, compiled ahead of time by glslangValidator (-G --target-env +// opengl) so this GPU-free binary needs no toolchain at run time. Its GLSL: +// layout(location = 0) in vec2 aPos; +// layout(constant_id = 3) const float uScale = 1.0; +// void main() { gl_Position = vec4(aPos * uScale, 0.0, 1.0); } +// The RENDERING half of the path is asserted separately, on a real context, in +// MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp. +// --------------------------------------------------------------------------------------------- + +namespace { + // Asserts a call recorded exactly one error and drains it, so the next case starts clean. + void ExpectOnlyThisGlError(GLenum expected) { + EXPECT_EQ(GetError(), expected); + EXPECT_EQ(GetError(), GL_NO_ERROR) << "the call recorded more than one error"; + } + + // 255 words + const unsigned int kVertexModule[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x00000020u, 0x00000000u, 0x00020011u, 0x00000001u, 0x0006000bu, + 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, + 0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000du, 0x00000012u, 0x0000001eu, + 0x0000001fu, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, + 0x00060005u, 0x0000000bu, 0x505f6c67u, 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x0000000bu, + 0x00000000u, 0x505f6c67u, 0x7469736fu, 0x006e6f69u, 0x00070006u, 0x0000000bu, 0x00000001u, 0x505f6c67u, + 0x746e696fu, 0x657a6953u, 0x00000000u, 0x00070006u, 0x0000000bu, 0x00000002u, 0x435f6c67u, 0x4470696cu, + 0x61747369u, 0x0065636eu, 0x00070006u, 0x0000000bu, 0x00000003u, 0x435f6c67u, 0x446c6c75u, 0x61747369u, + 0x0065636eu, 0x00030005u, 0x0000000du, 0x00000000u, 0x00040005u, 0x00000012u, 0x736f5061u, 0x00000000u, + 0x00040005u, 0x00000014u, 0x61635375u, 0x0000656cu, 0x00050005u, 0x0000001eu, 0x565f6c67u, 0x65747265u, + 0x00444978u, 0x00060005u, 0x0000001fu, 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u, + 0x0000000bu, 0x00000002u, 0x00050048u, 0x0000000bu, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u, + 0x0000000bu, 0x00000001u, 0x0000000bu, 0x00000001u, 0x00050048u, 0x0000000bu, 0x00000002u, 0x0000000bu, + 0x00000003u, 0x00050048u, 0x0000000bu, 0x00000003u, 0x0000000bu, 0x00000004u, 0x00040047u, 0x00000012u, + 0x0000001eu, 0x00000000u, 0x00040047u, 0x00000014u, 0x00000001u, 0x00000003u, 0x00040047u, 0x0000001eu, + 0x0000000bu, 0x00000005u, 0x00040047u, 0x0000001fu, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u, + 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, + 0x00000006u, 0x00000004u, 0x00040015u, 0x00000008u, 0x00000020u, 0x00000000u, 0x0004002bu, 0x00000008u, + 0x00000009u, 0x00000001u, 0x0004001cu, 0x0000000au, 0x00000006u, 0x00000009u, 0x0006001eu, 0x0000000bu, + 0x00000007u, 0x00000006u, 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x0000000bu, + 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x00040015u, 0x0000000eu, 0x00000020u, 0x00000001u, + 0x0004002bu, 0x0000000eu, 0x0000000fu, 0x00000000u, 0x00040017u, 0x00000010u, 0x00000006u, 0x00000002u, + 0x00040020u, 0x00000011u, 0x00000001u, 0x00000010u, 0x0004003bu, 0x00000011u, 0x00000012u, 0x00000001u, + 0x00040032u, 0x00000006u, 0x00000014u, 0x3f800000u, 0x0004002bu, 0x00000006u, 0x00000016u, 0x00000000u, + 0x0004002bu, 0x00000006u, 0x00000017u, 0x3f800000u, 0x00040020u, 0x0000001bu, 0x00000003u, 0x00000007u, + 0x00040020u, 0x0000001du, 0x00000001u, 0x0000000eu, 0x0004003bu, 0x0000001du, 0x0000001eu, 0x00000001u, + 0x0004003bu, 0x0000001du, 0x0000001fu, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, + 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000010u, 0x00000013u, 0x00000012u, 0x0005008eu, + 0x00000010u, 0x00000015u, 0x00000013u, 0x00000014u, 0x00050051u, 0x00000006u, 0x00000018u, 0x00000015u, + 0x00000000u, 0x00050051u, 0x00000006u, 0x00000019u, 0x00000015u, 0x00000001u, 0x00070050u, 0x00000007u, + 0x0000001au, 0x00000018u, 0x00000019u, 0x00000016u, 0x00000017u, 0x00050041u, 0x0000001bu, 0x0000001cu, + 0x0000000du, 0x0000000fu, 0x0003003eu, 0x0000001cu, 0x0000001au, 0x000100fdu, 0x00010038u, + }; +} // namespace + +TEST_F(ProgramTest, ShaderBinaryStoresASpirvModuleAndTheStateQueryReportsIt) { + DrainProgramTestErrors(); + + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + GLint isSpirv = GL_TRUE; + GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(GetError(), GL_NO_ERROR) << "GL_SPIR_V_BINARY is an accepted pname in a 4.6 context"; + EXPECT_EQ(isSpirv, GL_FALSE); + + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + EXPECT_EQ(GetError(), GL_NO_ERROR); + GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(isSpirv, GL_TRUE); + + // glCompileShader on a SPIR-V shader is INVALID_OPERATION: glSpecializeShader is what compiles + // one. This is the whole of spirv_modules_error_verification_test's first assertion. + CompileShader(shader); + EXPECT_EQ(GetError(), GL_INVALID_OPERATION); + + // glShaderSource takes the object back to being a GLSL shader. + const char* source = "#version 450 core\nvoid main() { gl_Position = vec4(0.0); }\n"; + ShaderSource(shader, 1, &source, nullptr); + GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(isSpirv, GL_FALSE); + CompileShader(shader); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + DrainProgramTestErrors(); +} + +TEST_F(ProgramTest, ShaderBinaryValidatesItsArguments) { + DrainProgramTestErrors(); + + GLuint shaders[2] = {0, 0}; + shaders[0] = CreateShader(GL_VERTEX_SHADER); + shaders[1] = CreateShader(GL_FRAGMENT_SHADER); + + // The only accepted format is the SPIR-V one; the stub used to accept everything silently. + ShaderBinary(1, shaders, GL_PROGRAM_BINARY_FORMATS, kVertexModule, sizeof(kVertexModule)); + ExpectOnlyThisGlError(GL_INVALID_ENUM); + + // A SPIR-V module is a sequence of 32-bit words. + ShaderBinary(1, shaders, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule) - 1); + ExpectOnlyThisGlError(GL_INVALID_VALUE); + + // The same shader twice is INVALID_VALUE, and nothing may have been attached. + GLuint duplicated[2] = {shaders[0], shaders[0]}; + ShaderBinary(2, duplicated, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ExpectOnlyThisGlError(GL_INVALID_VALUE); + GLint isSpirv = GL_TRUE; + GetShaderiv(shaders[0], GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(isSpirv, GL_FALSE) << "a rejected glShaderBinary is all-or-nothing"; + + // A name that is not a shader object. + GLuint bogus = 0xBADBEEF; + ShaderBinary(1, &bogus, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ExpectOnlyThisGlError(GL_INVALID_VALUE); + + // Something that is not SPIR-V at all: the magic number gate, before SPIRV-Cross ever sees it. + const unsigned int notSpirv[4] = {0xDEADBEEFu, 0u, 0u, 0u}; + ShaderBinary(1, shaders, GL_SHADER_BINARY_FORMAT_SPIR_V, notSpirv, sizeof(notSpirv)); + ExpectOnlyThisGlError(GL_INVALID_VALUE); + GetShaderiv(shaders[0], GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(isSpirv, GL_FALSE); + + // ONE call, TWO shader objects - the shape spirv_modules_shader_binary_multiple_shader_objects_test + // exercises. Both end up holding the module. + ShaderBinary(2, shaders, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + EXPECT_EQ(GetError(), GL_NO_ERROR); + for (const GLuint shader : shaders) { + GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(isSpirv, GL_TRUE); + } + + DrainProgramTestErrors(); +} + +TEST_F(ProgramTest, SpecializeShaderCompilesTheModuleAndAppliesItsConstants) { + DrainProgramTestErrors(); + + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + + // Before any module: INVALID_OPERATION rather than a silent no-op. + const unsigned int constantId = 3; + const unsigned int constantValue = 0; + SpecializeShader(shader, "main", 1, &constantId, &constantValue); + ExpectOnlyThisGlError(GL_INVALID_OPERATION); + + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + // Constant id 3 is the module's `uScale`, handed over as the bit pattern of 0.5f. + float half = 0.5f; + unsigned int halfBits = 0; + std::memcpy(&halfBits, &half, sizeof(halfBits)); + SpecializeShader(shader, "main", 1, &constantId, &halfBits); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + GLint compiled = GL_FALSE; + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + char infoLog[2048] = ""; + GetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog); + EXPECT_EQ(compiled, GL_TRUE) << infoLog; + + // The module stays attached after specialization: glSpecializeShader may legally run again + // with different constants, and it has to re-specialize the ORIGINAL words. + GLint isSpirv = GL_FALSE; + GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); + EXPECT_EQ(isSpirv, GL_TRUE); + SpecializeShader(shader, "main", 0, nullptr, nullptr); + EXPECT_EQ(GetError(), GL_NO_ERROR); + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + EXPECT_EQ(compiled, GL_TRUE) << "a second specialization of the same module must also compile"; + + DrainProgramTestErrors(); +} + +TEST_F(ProgramTest, SpecializeShaderReportsBadEntryPointsAndUnknownConstantsThroughCompileStatus) { + DrainProgramTestErrors(); + + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + // A constant id the module does not declare. ARB_gl_spirv routes a failed specialization + // through COMPILE_STATUS and the info log, exactly as glCompileShader does - it is not a GL + // error, which is why an application that only checks glGetError would see nothing. + const unsigned int unknownId = 4242; + const unsigned int value = 0; + SpecializeShader(shader, "main", 1, &unknownId, &value); + GLint compiled = GL_TRUE; + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + EXPECT_EQ(compiled, GL_FALSE); + GLint logLength = 0; + GetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength); + EXPECT_GT(logLength, 0) << "a failed specialization has to say why"; + + // A repeated constant index is GL_INVALID_VALUE at the entry point itself. + const unsigned int repeated[2] = {3, 3}; + const unsigned int values[2] = {0, 0}; + SpecializeShader(shader, "main", 2, repeated, values); + ExpectOnlyThisGlError(GL_INVALID_VALUE); + + // An entry point the module does not carry. + SpecializeShader(shader, "notMain", 0, nullptr, nullptr); + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + EXPECT_EQ(compiled, GL_FALSE); + + DrainProgramTestErrors(); +} + +TEST_F(ProgramTest, ShaderBinaryFormatsAreAdvertisedConsistently) { + DrainProgramTestErrors(); + + GLint count = -1; + GetIntegerv(GL_NUM_SHADER_BINARY_FORMATS, &count); + EXPECT_EQ(GetError(), GL_NO_ERROR); + ASSERT_EQ(count, 1); + + GLint formats[4] = {0, 0, 0, 0}; + GetIntegerv(GL_SHADER_BINARY_FORMATS, formats); + EXPECT_EQ(GetError(), GL_NO_ERROR); + EXPECT_EQ(formats[0], static_cast(GL_SHADER_BINARY_FORMAT_SPIR_V)) + << "the count and the list have to describe the same thing"; + + DrainProgramTestErrors(); +}