diff --git a/MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp index 05b27727..94d988a3 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/SpirvShaderBinaryScenario.cpp @@ -222,24 +222,39 @@ namespace MGITest { glDeleteShader(shader); } - TEST_F(SpirvShaderBinaryScenario, SpecializeShaderRejectsAConstantTheModuleDoesNotDeclare) { + TEST_F(SpirvShaderBinaryScenario, SpecializeShaderErrorSurfaceMatchesTheExtension) { 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. + // 4242 is not one of the module's constant ids. ARB_gl_spirv enumerates that as + // GL_INVALID_VALUE, and an erroring GL command has no other effect - so the shader is left + // untouched rather than pushed into a failed-compile state. const unsigned int badId = 4242; const unsigned int value = 0; glSpecializeShader(shader, "main", 1, &badId, &value); - GLint compiled = GL_TRUE; + EXPECT_EQ(FirstGLError(), static_cast(GL_INVALID_VALUE)); + + // Same for an entry point the module does not carry. + glSpecializeShader(shader, "notMain", 0, nullptr, nullptr); + EXPECT_EQ(FirstGLError(), static_cast(GL_INVALID_VALUE)); + + // Neither refusal specialized the shader, so a well-formed call still works. + glSpecializeShader(shader, "main", 0, nullptr, nullptr); + EXPECT_EQ(FirstGLError(), 0u); + GLint compiled = GL_FALSE; 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"; + EXPECT_EQ(compiled, GL_TRUE); + + // But a SECOND specialization of a shader that HAS been specialized is INVALID_OPERATION + // until glShaderBinary re-associates the module. + glSpecializeShader(shader, "main", 0, nullptr, nullptr); + EXPECT_EQ(FirstGLError(), static_cast(GL_INVALID_OPERATION)); + glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + glSpecializeShader(shader, "main", 0, nullptr, nullptr); + EXPECT_EQ(FirstGLError(), 0u) << "re-associating the module makes specialization legal again"; glDeleteShader(shader); } diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index fa736b59..aea4e8b2 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -4527,38 +4527,124 @@ TEST_F(ProgramTest, SpecializeShaderCompilesTheModuleAndAppliesItsConstants) { 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. + // The module stays attached after specialization - GL_SPIR_V_BINARY keeps reading TRUE - but + // the shader may NOT be specialized again. ARB_gl_spirv: "Once specialized, a shader may not + // be re-specialized without first re-associating the original SPIR-V module with it, through + // ShaderBinary." GLint isSpirv = GL_FALSE; GetShaderiv(shader, GL_SPIR_V_BINARY, &isSpirv); EXPECT_EQ(isSpirv, GL_TRUE); SpecializeShader(shader, "main", 0, nullptr, nullptr); + ExpectOnlyThisGlError(GL_INVALID_OPERATION); + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + EXPECT_EQ(compiled, GL_TRUE) << "the refused call must not have disturbed the first specialization"; + + // Re-associating the module is what makes a second specialization legal again - and it is the + // only thing that does. + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ASSERT_EQ(GetError(), GL_NO_ERROR); + 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"; + EXPECT_EQ(compiled, GL_TRUE); DrainProgramTestErrors(); } -TEST_F(ProgramTest, SpecializeShaderReportsBadEntryPointsAndUnknownConstantsThroughCompileStatus) { +// glShaderSource does the same re-association in the other direction: it turns the object back +// into a GLSL shader, so a later glShaderBinary + glSpecializeShader pair is legal again. +TEST_F(ProgramTest, ShaderSourceClearsTheSpecializedLatch) { + DrainProgramTestErrors(); + + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + SpecializeShader(shader, "main", 0, nullptr, nullptr); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + const char* source = "#version 450 core\nvoid main() { gl_Position = vec4(0.0); }\n"; + ShaderSource(shader, 1, &source, nullptr); + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ASSERT_EQ(GetError(), GL_NO_ERROR); + SpecializeShader(shader, "main", 0, nullptr, nullptr); + EXPECT_EQ(GetError(), GL_NO_ERROR) << "the latch must not survive a round trip through glShaderSource"; + + DrainProgramTestErrors(); +} + +// A shader that came from glShaderBinary has never had glShaderSource called on it, so GL 4.6 +// core 7.1 makes its source the empty string - including AFTER glSpecializeShader, when the +// object internally holds the GLSL the module was translated into. That text is MobileGL's, not +// the application's, and handing it back invites an application to cache and re-submit it. +TEST_F(ProgramTest, ASpirvShaderReportsNoApplicationSource) { 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. + GLint sourceLength = -1; + GetShaderiv(shader, GL_SHADER_SOURCE_LENGTH, &sourceLength); + EXPECT_EQ(sourceLength, 0); + + SpecializeShader(shader, "main", 0, nullptr, nullptr); + ASSERT_EQ(GetError(), GL_NO_ERROR); + GLint compiled = GL_FALSE; + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + ASSERT_EQ(compiled, GL_TRUE) << "the leak this pins only exists on the specialized path"; + + sourceLength = -1; + GetShaderiv(shader, GL_SHADER_SOURCE_LENGTH, &sourceLength); + EXPECT_EQ(sourceLength, 0) << "the SPIRV-Cross GLSL is not the application's source"; + + char buffer[64]; + std::memset(buffer, 'x', sizeof(buffer)); + GLsizei written = -1; + GetShaderSource(shader, static_cast(sizeof(buffer)), &written, buffer); + EXPECT_EQ(written, 0); + EXPECT_EQ(buffer[0], '\0'); + EXPECT_EQ(GetError(), GL_NO_ERROR); + + // A GLSL shader still answers with what the application gave it. + const char* source = "#version 450 core\nvoid main() { gl_Position = vec4(0.0); }\n"; + ShaderSource(shader, 1, &source, nullptr); + GetShaderiv(shader, GL_SHADER_SOURCE_LENGTH, &sourceLength); + EXPECT_EQ(sourceLength, static_cast(std::strlen(source)) + 1); + + DrainProgramTestErrors(); +} + +// The two conditions ARB_gl_spirv ENUMERATES are GL_INVALID_VALUE, not compile failures: "an +// INVALID_VALUE error is generated if pEntryPoint does not name a valid entry point for shader" +// and "...if any element of pConstantIndex refers to a specialization constant that does not exist +// in the shader module contained in shader". Both used to be reported as COMPILE_STATUS false with +// no GL error, which an application checking glGetError could not see at all. +// +// The distinction matters beyond the error code: an erroring GL command must have NO OTHER EFFECT, +// so neither of these may leave the shader object in a failed-compile state. The conformance suite +// leans on exactly that - it fails specialization twice on one object and then requires the next, +// well-formed call on that same object to succeed. +TEST_F(ProgramTest, SpecializeShaderRaisesInvalidValueForBadEntryPointsAndUnknownConstants) { + 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. 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"; + ExpectOnlyThisGlError(GL_INVALID_VALUE); + + // An entry point the module does not carry. + SpecializeShader(shader, "notMain", 0, nullptr, nullptr); + ExpectOnlyThisGlError(GL_INVALID_VALUE); + + // Neither of them may name an entry point at all. + SpecializeShader(shader, nullptr, 0, nullptr, nullptr); + ExpectOnlyThisGlError(GL_INVALID_VALUE); + SpecializeShader(shader, "", 0, nullptr, nullptr); + ExpectOnlyThisGlError(GL_INVALID_VALUE); // A repeated constant index is GL_INVALID_VALUE at the entry point itself. const unsigned int repeated[2] = {3, 3}; @@ -4566,10 +4652,35 @@ TEST_F(ProgramTest, SpecializeShaderReportsBadEntryPointsAndUnknownConstantsThro SpecializeShader(shader, "main", 2, repeated, values); ExpectOnlyThisGlError(GL_INVALID_VALUE); - // An entry point the module does not carry. - SpecializeShader(shader, "notMain", 0, nullptr, nullptr); + // AND NOW THE POINT: none of the five refused calls specialized the shader or damaged it, so + // the well-formed call that follows must still be accepted. Latching the "specialized" flag on + // the failure path - the obvious way to implement the re-specialization rule - breaks exactly + // here, which is why the flag is only ever set on the success path. + SpecializeShader(shader, "main", 0, nullptr, nullptr); + EXPECT_EQ(GetError(), GL_NO_ERROR) << "a failed specialization does not make the shader specialized"; + GLint compiled = GL_FALSE; GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); - EXPECT_EQ(compiled, GL_FALSE); + EXPECT_EQ(compiled, GL_TRUE); + + DrainProgramTestErrors(); +} + +// A GENUINE compile failure of a well-formed request keeps the COMPILE_STATUS surface: the module +// is a valid SPIR-V module naming a real entry point, it simply cannot be translated for this +// stage. Nothing about that is one of the enumerated errors. +TEST_F(ProgramTest, SpecializeShaderStillReportsATranslationFailureThroughCompileStatus) { + DrainProgramTestErrors(); + + // The vertex module handed to a FRAGMENT shader object: its only entry point carries the + // Vertex execution model, so no fragment entry point named "main" exists in it. + const GLuint shader = CreateShader(GL_FRAGMENT_SHADER); + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kVertexModule, sizeof(kVertexModule)); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + SpecializeShader(shader, "main", 0, nullptr, nullptr); + // Reported as INVALID_VALUE (there is no such entry point FOR THIS STAGE) - the stage is part + // of what "a valid entry point for shader" means. + ExpectOnlyThisGlError(GL_INVALID_VALUE); DrainProgramTestErrors(); } @@ -4590,3 +4701,117 @@ TEST_F(ProgramTest, ShaderBinaryFormatsAreAdvertisedConsistently) { DrainProgramTestErrors(); } + +// --------------------------------------------------------------------------------------------- +// ARB_gl_spirv makes XfbBuffer / XfbStride / Offset DECORATIONS the only way a SPIR-V program +// declares transform feedback - glTransformFeedbackVaryings has no effect on such a program. The +// decorations were ignored entirely: the link ran off `in.requestedXfbVaryings`, which is empty +// for a SPIR-V program, so a module that asked for capture captured nothing and +// GL_TRANSFORM_FEEDBACK_VARYINGS answered zero. +// +// glSpecializeShader now reflects the decorations and re-expresses them as the equivalent +// glTransformFeedbackVaryings request (ARB_transform_feedback3's gl_SkipComponentsN carrying the +// declared offset), which is the form every consumer downstream already implements. +// +// The module below is `layout(xfb_buffer = 0, xfb_offset = 16) out gl_PerVertex { vec4 +// gl_Position; };` over a trivial vertex shader - the exact shape gl4cGlSpirvTests' +// spirv_modules_state_queries_test feeds in first. Offset 16 with a stride of 32 means the capture +// is four components in, i.e. one gl_SkipComponents4 ahead of gl_Position. +// --------------------------------------------------------------------------------------------- + +namespace { + // 177 words + const unsigned int kXfbVertexModule[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x00000015u, 0x00000000u, 0x00020011u, 0x00000001u, 0x00020011u, + 0x00000035u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, 0x00000000u, 0x0003000eu, + 0x00000000u, 0x00000001u, 0x0009000fu, 0x00000000u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x0000000au, + 0x0000000eu, 0x00000013u, 0x00000014u, 0x00030010u, 0x00000004u, 0x0000000bu, 0x00030003u, 0x00000002u, + 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, 0x00060005u, 0x00000008u, 0x505f6c67u, + 0x65567265u, 0x78657472u, 0x00000000u, 0x00060006u, 0x00000008u, 0x00000000u, 0x505f6c67u, 0x7469736fu, + 0x006e6f69u, 0x00030005u, 0x0000000au, 0x00000000u, 0x00050005u, 0x0000000eu, 0x69736f70u, 0x6e6f6974u, + 0x00000000u, 0x00050005u, 0x00000013u, 0x565f6c67u, 0x65747265u, 0x00444978u, 0x00060005u, 0x00000014u, + 0x495f6c67u, 0x6174736eu, 0x4965636eu, 0x00000044u, 0x00030047u, 0x00000008u, 0x00000002u, 0x00050048u, + 0x00000008u, 0x00000000u, 0x0000000bu, 0x00000000u, 0x00050048u, 0x00000008u, 0x00000000u, 0x00000023u, + 0x00000010u, 0x00040047u, 0x0000000au, 0x00000024u, 0x00000000u, 0x00040047u, 0x0000000au, 0x00000025u, + 0x00000020u, 0x00040047u, 0x0000000eu, 0x0000001eu, 0x00000000u, 0x00040047u, 0x00000013u, 0x0000000bu, + 0x00000005u, 0x00040047u, 0x00000014u, 0x0000000bu, 0x00000006u, 0x00020013u, 0x00000002u, 0x00030021u, + 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, + 0x00000004u, 0x0003001eu, 0x00000008u, 0x00000007u, 0x00040020u, 0x00000009u, 0x00000003u, 0x00000008u, + 0x0004003bu, 0x00000009u, 0x0000000au, 0x00000003u, 0x00040015u, 0x0000000bu, 0x00000020u, 0x00000001u, + 0x0004002bu, 0x0000000bu, 0x0000000cu, 0x00000000u, 0x00040020u, 0x0000000du, 0x00000001u, 0x00000007u, + 0x0004003bu, 0x0000000du, 0x0000000eu, 0x00000001u, 0x00040020u, 0x00000010u, 0x00000003u, 0x00000007u, + 0x00040020u, 0x00000012u, 0x00000001u, 0x0000000bu, 0x0004003bu, 0x00000012u, 0x00000013u, 0x00000001u, + 0x0004003bu, 0x00000012u, 0x00000014u, 0x00000001u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, + 0x00000003u, 0x000200f8u, 0x00000005u, 0x0004003du, 0x00000007u, 0x0000000fu, 0x0000000eu, 0x00050041u, + 0x00000010u, 0x00000011u, 0x0000000au, 0x0000000cu, 0x0003003eu, 0x00000011u, 0x0000000fu, 0x000100fdu, + 0x00010038u, + }; +} // namespace + +TEST_F(ProgramTest, ASpirvModulesXfbDecorationsBecomeTheProgramsCaptureList) { + DrainProgramTestErrors(); + + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kXfbVertexModule, sizeof(kXfbVertexModule)); + ASSERT_EQ(GetError(), GL_NO_ERROR); + SpecializeShader(shader, "main", 0, nullptr, nullptr); + ASSERT_EQ(GetError(), GL_NO_ERROR); + GLint compiled = GL_FALSE; + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + char shaderLog[2048] = ""; + GetShaderInfoLog(shader, sizeof(shaderLog), nullptr, shaderLog); + ASSERT_EQ(compiled, GL_TRUE) << shaderLog; + + const GLuint program = CreateProgram(); + AttachShader(program, shader); + // NO glTransformFeedbackVaryings anywhere: the declaration is the module's own. + LinkProgram(program); + GLint linked = GL_FALSE; + GetProgramiv(program, GL_LINK_STATUS, &linked); + char programLog[2048] = ""; + GetProgramInfoLog(program, sizeof(programLog), nullptr, programLog); + ASSERT_EQ(linked, GL_TRUE) << programLog; + + GLint varyingCount = -1; + GetProgramiv(program, GL_TRANSFORM_FEEDBACK_VARYINGS, &varyingCount); + EXPECT_GT(varyingCount, 0) << "the module's xfb decorations declared a capture and none was recorded"; + + // The captured name is the built-in the block redeclared. It is found by BuiltIn decoration, + // not by string, because a stripped module carries no OpMemberName at all. + Bool sawPosition = false; + for (GLint i = 0; i < varyingCount; ++i) { + char name[128] = ""; + GLsizei nameLength = 0; + GLsizei size = 0; + GLenum type = 0; + GetTransformFeedbackVarying(program, static_cast(i), sizeof(name), &nameLength, &size, &type, name); + if (String(name) == "gl_Position") sawPosition = true; + } + EXPECT_TRUE(sawPosition) << "gl_Position was declared captured by the module's Offset decoration"; + EXPECT_EQ(GetError(), GL_NO_ERROR); + + DrainProgramTestErrors(); +} + +// The other half of the same fix: the decorations must NOT survive into the GLSL the module is +// translated into. SPIRV-Cross re-emits them as layout(xfb_buffer/xfb_stride/xfb_offset), glslang +// re-encodes them into the regenerated SPIR-V, and the DirectGLES ESSL hop then refuses them +// outright ("Need GL_ARB_enhanced_layouts for xfb_stride or xfb_buffer") and drops the stage - +// a program that links clean and draws nothing. Compiling at all is the observable proof they are +// gone; the ESSL leg is exercised by the integration scenario. +TEST_F(ProgramTest, ASpirvModulesXfbDecorationsDoNotSurviveIntoTheTranslatedSource) { + DrainProgramTestErrors(); + + const GLuint shader = CreateShader(GL_VERTEX_SHADER); + ShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V, kXfbVertexModule, sizeof(kXfbVertexModule)); + SpecializeShader(shader, "main", 0, nullptr, nullptr); + ASSERT_EQ(GetError(), GL_NO_ERROR); + + GLint compiled = GL_FALSE; + GetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + char shaderLog[2048] = ""; + GetShaderInfoLog(shader, sizeof(shaderLog), nullptr, shaderLog); + EXPECT_EQ(compiled, GL_TRUE) << shaderLog; + + DrainProgramTestErrors(); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index fc03b021..bdd6c50d 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -12,6 +12,7 @@ #include "ShaderCompiler.h" +#include #include #include diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp index 3faa175b..93fd2b0c 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp @@ -538,75 +538,151 @@ namespace MobileGL { } } // namespace + namespace { + // The four gl_PerVertex members, by their GL interface names. These are the only + // built-ins GL lets transform feedback capture, and a SPIR-V module names them by + // BuiltIn decoration rather than by string - so the mapping has to live somewhere. + const char* XfbBuiltInName(SpvBuiltIn builtin) { + switch (builtin) { + case SpvBuiltInPosition: + return "gl_Position"; + case SpvBuiltInPointSize: + return "gl_PointSize"; + case SpvBuiltInClipDistance: + return "gl_ClipDistance"; + case SpvBuiltInCullDistance: + return "gl_CullDistance"; + default: + return nullptr; + } + } + } // namespace + Vector SpvcSession::ReflectTransformFeedbackCaptures() const { Vector captures; if (compiler == nullptr || resources == nullptr) return captures; + // XfbBuffer/XfbStride sit on the declaring VARIABLE; Offset sits on the variable + // for a plain output and on each MEMBER for a block. + auto readVariableDecorations = [this](SpvId id, Uint32& outBuffer, Uint32& outStride) { + outBuffer = spvc_compiler_has_decoration(compiler, id, SpvDecorationXfbBuffer) == SPVC_TRUE + ? spvc_compiler_get_decoration(compiler, id, SpvDecorationXfbBuffer) + : 0u; + outStride = spvc_compiler_has_decoration(compiler, id, SpvDecorationXfbStride) == SPVC_TRUE + ? spvc_compiler_get_decoration(compiler, id, SpvDecorationXfbStride) + : 0u; + }; + + // ---- application outputs: plain variables and application blocks ---- const spvc_reflected_resource* outputs = nullptr; SizeT outputCount = 0; if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &outputs, - &outputCount) != SPVC_SUCCESS) { - return captures; + &outputCount) == SPVC_SUCCESS) { + for (SizeT i = 0; i < outputCount; ++i) { + const spvc_reflected_resource& output = outputs[i]; + Uint32 buffer = 0; + Uint32 stride = 0; + readVariableDecorations(output.id, buffer, stride); + + const spvc_type type = spvc_compiler_get_type_handle(compiler, output.base_type_id); + const unsigned memberCount = + type != nullptr && spvc_type_get_basetype(type) == SPVC_BASETYPE_STRUCT + ? spvc_type_get_num_member_types(type) + : 0u; + + if (memberCount == 0) { + if (spvc_compiler_has_decoration(compiler, output.id, SpvDecorationOffset) != SPVC_TRUE) { + continue; + } + SpirvXfbCapture capture; + capture.name = output.name ? output.name : ""; + capture.buffer = buffer; + capture.stride = stride; + capture.offset = spvc_compiler_get_decoration(compiler, output.id, SpvDecorationOffset); + capture.componentCount = XfbComponentCount(compiler, output.type_id); + if (!capture.name.empty()) captures.push_back(Move(capture)); + continue; + } + + for (unsigned member = 0; member < memberCount; ++member) { + if (spvc_compiler_has_member_decoration(compiler, output.base_type_id, member, + SpvDecorationOffset) != SPVC_TRUE) { + continue; + } + const char* memberName = + spvc_compiler_get_member_name(compiler, output.base_type_id, member); + if (memberName == nullptr || *memberName == '\0') continue; + SpirvXfbCapture capture; + const String blockName = output.name ? String(output.name) : String{}; + // GL's capture interface spells an application block's member + // "Block.member"; a redeclared built-in block contributes its members + // by their own names, which the built-in walk below handles. + capture.name = blockName.empty() ? String(memberName) + : blockName + "." + String(memberName); + capture.buffer = buffer; + capture.stride = stride; + capture.offset = spvc_compiler_get_member_decoration(compiler, output.base_type_id, + member, SpvDecorationOffset); + capture.componentCount = + XfbComponentCount(compiler, spvc_type_get_member_type(type, member)); + captures.push_back(Move(capture)); + } + } } - for (SizeT i = 0; i < outputCount; ++i) { - const spvc_reflected_resource& output = outputs[i]; - // XfbBuffer/XfbStride sit on the VARIABLE; Offset sits on the variable for a - // plain output and on each MEMBER for a block (which is how a redeclared - // gl_PerVertex carries it). - const Bool hasBuffer = - spvc_compiler_has_decoration(compiler, output.id, SpvDecorationXfbBuffer) == SPVC_TRUE; - const Uint32 buffer = - hasBuffer ? spvc_compiler_get_decoration(compiler, output.id, SpvDecorationXfbBuffer) : 0u; - const Uint32 stride = - spvc_compiler_has_decoration(compiler, output.id, SpvDecorationXfbStride) == SPVC_TRUE - ? spvc_compiler_get_decoration(compiler, output.id, SpvDecorationXfbStride) - : 0u; + // ---- the redeclared built-in block ---- + // SPIRV-Cross keeps gl_PerVertex out of the STAGE_OUTPUT list and reports it here + // instead, one entry per built-in member. That is the shape the conformance suite + // feeds in first (`layout(xfb_buffer = 0, xfb_offset = 16) out gl_PerVertex { vec4 + // gl_Position; }`), so walking only the list above would have found nothing at all. + const spvc_reflected_builtin_resource* builtins = nullptr; + SizeT builtinCount = 0; + if (spvc_resources_get_builtin_resource_list_for_type( + resources, SPVC_BUILTIN_RESOURCE_TYPE_STAGE_OUTPUT, &builtins, &builtinCount) == + SPVC_SUCCESS) { + for (SizeT i = 0; i < builtinCount; ++i) { + const spvc_reflected_builtin_resource& entry = builtins[i]; + const char* name = XfbBuiltInName(entry.builtin); + if (name == nullptr) continue; - const spvc_type type = spvc_compiler_get_type_handle(compiler, output.base_type_id); - const unsigned memberCount = - type != nullptr && spvc_type_get_basetype(type) == SPVC_BASETYPE_STRUCT - ? spvc_type_get_num_member_types(type) - : 0u; + Uint32 buffer = 0; + Uint32 stride = 0; + readVariableDecorations(entry.resource.id, buffer, stride); - if (memberCount == 0) { - if (spvc_compiler_has_decoration(compiler, output.id, SpvDecorationOffset) != SPVC_TRUE) { + const spvc_type blockType = + spvc_compiler_get_type_handle(compiler, entry.resource.base_type_id); + if (blockType == nullptr || + spvc_type_get_basetype(blockType) != SPVC_BASETYPE_STRUCT) { continue; } - SpirvXfbCapture capture; - capture.name = output.name ? output.name : ""; - capture.buffer = buffer; - capture.stride = stride; - capture.offset = spvc_compiler_get_decoration(compiler, output.id, SpvDecorationOffset); - capture.componentCount = XfbComponentCount(compiler, output.type_id); - if (!capture.name.empty()) captures.push_back(Move(capture)); - continue; - } - - for (unsigned member = 0; member < memberCount; ++member) { - if (spvc_compiler_has_member_decoration(compiler, output.base_type_id, member, - SpvDecorationOffset) != SPVC_TRUE) { - continue; + // The member index is not in the reflection entry, so it is recovered by + // matching the BuiltIn decoration - the same key the entry is keyed on. + const unsigned memberCount = spvc_type_get_num_member_types(blockType); + for (unsigned member = 0; member < memberCount; ++member) { + if (spvc_compiler_has_member_decoration(compiler, entry.resource.base_type_id, member, + SpvDecorationBuiltIn) != SPVC_TRUE) { + continue; + } + if (spvc_compiler_get_member_decoration(compiler, entry.resource.base_type_id, member, + SpvDecorationBuiltIn) != + static_cast(entry.builtin)) { + continue; + } + if (spvc_compiler_has_member_decoration(compiler, entry.resource.base_type_id, member, + SpvDecorationOffset) != SPVC_TRUE) { + break; // this built-in is present but not captured + } + SpirvXfbCapture capture; + capture.name = name; + capture.buffer = buffer; + capture.stride = stride; + capture.offset = spvc_compiler_get_member_decoration( + compiler, entry.resource.base_type_id, member, SpvDecorationOffset); + capture.componentCount = + XfbComponentCount(compiler, spvc_type_get_member_type(blockType, member)); + captures.push_back(Move(capture)); + break; } - const char* memberName = - spvc_compiler_get_member_name(compiler, output.base_type_id, member); - if (memberName == nullptr || *memberName == '\0') continue; - SpirvXfbCapture capture; - // A redeclared built-in block contributes its members by their own names - // ("gl_Position"), which is how GL's capture interface spells them; an - // application block spells them "Block.member". - const String blockName = output.name ? String(output.name) : String{}; - const Bool isBuiltInBlock = blockName.compare(0, 3, "gl_") == 0; - capture.name = isBuiltInBlock || blockName.empty() - ? String(memberName) - : blockName + "." + String(memberName); - capture.buffer = buffer; - capture.stride = stride; - capture.offset = spvc_compiler_get_member_decoration(compiler, output.base_type_id, member, - SpvDecorationOffset); - capture.componentCount = XfbComponentCount( - compiler, spvc_type_get_member_type(type, member)); - captures.push_back(Move(capture)); } } @@ -624,28 +700,36 @@ namespace MobileGL { void SpvcSession::StripTransformFeedbackDecorations() { if (compiler == nullptr || resources == nullptr) return; + auto stripVariable = [this](SpvId variableId, spvc_type_id baseTypeId) { + spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationXfbBuffer); + spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationXfbStride); + spvc_compiler_unset_decoration(compiler, variableId, SpvDecorationOffset); + + const spvc_type type = spvc_compiler_get_type_handle(compiler, baseTypeId); + if (type == nullptr || spvc_type_get_basetype(type) != SPVC_BASETYPE_STRUCT) return; + const unsigned memberCount = spvc_type_get_num_member_types(type); + for (unsigned member = 0; member < memberCount; ++member) { + spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationOffset); + spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationXfbBuffer); + spvc_compiler_unset_member_decoration(compiler, baseTypeId, member, SpvDecorationXfbStride); + } + }; + const spvc_reflected_resource* outputs = nullptr; SizeT outputCount = 0; if (spvc_resources_get_resource_list_for_type(resources, SPVC_RESOURCE_TYPE_STAGE_OUTPUT, &outputs, - &outputCount) != SPVC_SUCCESS) { - return; + &outputCount) == SPVC_SUCCESS) { + for (SizeT i = 0; i < outputCount; ++i) { + stripVariable(outputs[i].id, outputs[i].base_type_id); + } } - for (SizeT i = 0; i < outputCount; ++i) { - const spvc_reflected_resource& output = outputs[i]; - spvc_compiler_unset_decoration(compiler, output.id, SpvDecorationXfbBuffer); - spvc_compiler_unset_decoration(compiler, output.id, SpvDecorationXfbStride); - spvc_compiler_unset_decoration(compiler, output.id, SpvDecorationOffset); - - const spvc_type type = spvc_compiler_get_type_handle(compiler, output.base_type_id); - if (type == nullptr || spvc_type_get_basetype(type) != SPVC_BASETYPE_STRUCT) continue; - const unsigned memberCount = spvc_type_get_num_member_types(type); - for (unsigned member = 0; member < memberCount; ++member) { - spvc_compiler_unset_member_decoration(compiler, output.base_type_id, member, - SpvDecorationOffset); - spvc_compiler_unset_member_decoration(compiler, output.base_type_id, member, - SpvDecorationXfbBuffer); - spvc_compiler_unset_member_decoration(compiler, output.base_type_id, member, - SpvDecorationXfbStride); + const spvc_reflected_builtin_resource* builtins = nullptr; + SizeT builtinCount = 0; + if (spvc_resources_get_builtin_resource_list_for_type( + resources, SPVC_BUILTIN_RESOURCE_TYPE_STAGE_OUTPUT, &builtins, &builtinCount) == + SPVC_SUCCESS) { + for (SizeT i = 0; i < builtinCount; ++i) { + stripVariable(builtins[i].resource.id, builtins[i].resource.base_type_id); } } }