From 21159caf31ee7a080b1ec6f01c416e4ea79da47d Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 12 Aug 2026 07:36:19 -0400 Subject: [PATCH] [Fix, Test] (MG_Util, MG_Backend/DirectGLES): transform feedback captured nothing at all from an interface block --- CMakeLists.txt | 1 + MobileGL/MG_Backend/DirectGLES/Managers.cpp | 53 +- .../MG_Test/ShaderTranspiler/CMakeLists.txt | 1 + .../FlattenXfbInterfaceBlocksTest.cpp | 226 +++++++ .../ShaderTranspiler/ShaderCompiler.cpp | 21 + .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 19 + .../FlattenXfbInterfaceBlocksPass.cpp | 636 ++++++++++++++++++ .../FlattenXfbInterfaceBlocksPass.h | 73 ++ 8 files changed, 1028 insertions(+), 2 deletions(-) create mode 100644 MobileGL/MG_Test/ShaderTranspiler/FlattenXfbInterfaceBlocksTest.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ff4cd6e1..e728087e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -279,6 +279,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index a50882e1..917e6318 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -4427,6 +4427,21 @@ namespace MobileGL::MG_Backend::DirectGLES { } auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv(); + // Blocks a transform-feedback capture request names a member of ("StageData" of + // "StageData.attrib[0]"). The Adreno ES driver accepts such a request, links, and + // then captures nothing at all for it, so those blocks - and ONLY those - get + // flattened into per-member variables below, in EVERY stage, so a producer and its + // consumer keep matching. gl_PerVertex members ("gl_Position") carry no block + // prefix and so never enter this set. + std::set xfbCaptureBlockNames; + for (const auto& xfbVarying : stateProgramObject->GetTransformFeedbackVaryings()) { + const SizeT dot = xfbVarying.name.find('.'); + if (dot != String::npos && dot > 0) { + xfbCaptureBlockNames.insert(xfbVarying.name.substr(0, dot)); + } + } + std::set flattenedXfbBlockNames; + for (int index = 0; index < attachedShaders.size(); ++index) { auto& shader = attachedShaders[index]; GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage()); @@ -4484,6 +4499,25 @@ namespace MobileGL::MG_Backend::DirectGLES { effectiveSpirv = &splitArrayInputSpirv; } + // Adopt the rewritten module only when THIS stage actually had one of the + // blocks - the optimizer hands back a re-serialised copy either way, and taking + // that copy for a module it did not rewrite is not free (it cost the + // create-indirect retrace 0.15 SSIM when the array-input split first missed + // this gate). The report has to be per stage, not cumulative: a fragment shader + // consuming the same block reports a name the vertex stage already reported, + // and its own rewrite must still be taken or the two stages stop matching. + Vector flattenedXfbSpirv; + std::set stageFlattenedXfbBlockNames; + if (!xfbCaptureBlockNames.empty() && + MG_Util::ShaderTranspiler::ShaderCompiler::FlattenXfbInterfaceBlocksForEssl( + *effectiveSpirv, xfbCaptureBlockNames, stageFlattenedXfbBlockNames, + flattenedXfbSpirv) && + !flattenedXfbSpirv.empty() && !stageFlattenedXfbBlockNames.empty()) { + effectiveSpirv = &flattenedXfbSpirv; + flattenedXfbBlockNames.insert(stageFlattenedXfbBlockNames.begin(), + stageFlattenedXfbBlockNames.end()); + } + // ESSL stage-matches uniform blocks by member precision, but SPIRV-Cross prints // a RelaxedPrecision member as explicit "mediump" in the vertex stage and as // UNQUALIFIED (mediump-by-default) in the fragment stage; after @@ -4692,8 +4726,23 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& xfbVaryings = stateProgramObject->GetTransformFeedbackVaryings(); Vector xfbNames; xfbNames.reserve(xfbVaryings.size()); - for (const auto& xfbVarying : xfbVaryings) { - xfbNames.push_back(xfbVarying.name.c_str()); + // A block this build flattened no longer HAS the member the application asked + // for; it has the variable that replaced it. Everything else - including a + // member of a block that was left alone - keeps the application's spelling. + // Storage first, pointers after: xfbNames holds pointers into these strings. + Vector rewrittenXfbNames(xfbVaryings.size()); + for (SizeT nameIndex = 0; nameIndex < xfbVaryings.size(); ++nameIndex) { + String flatName; + if (!flattenedXfbBlockNames.empty() && + MG_Util::ShaderTranspiler::ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock( + xfbVaryings[nameIndex].name, flattenedXfbBlockNames, flatName)) { + rewrittenXfbNames[nameIndex] = std::move(flatName); + } else { + rewrittenXfbNames[nameIndex] = xfbVaryings[nameIndex].name; + } + } + for (const auto& xfbName : rewrittenXfbNames) { + xfbNames.push_back(xfbName.c_str()); } MGLOG_D("Declaring %zu transform feedback varyings on program %u", xfbNames.size(), m_backendProgramId); diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index 18da1c3b..25dacdd2 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -4,6 +4,7 @@ add_executable( SpirvPassTest SpirvPassTest.cpp DemoteFloat64Test.cpp + FlattenXfbInterfaceBlocksTest.cpp ) target_include_directories(SpirvPassTest PRIVATE diff --git a/MobileGL/MG_Test/ShaderTranspiler/FlattenXfbInterfaceBlocksTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/FlattenXfbInterfaceBlocksTest.cpp new file mode 100644 index 00000000..d8f7e232 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/FlattenXfbInterfaceBlocksTest.cpp @@ -0,0 +1,226 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FlattenXfbInterfaceBlocksTest.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 + +#include + +#include +#include +#include + +#include "Includes.h" +#include "Init.h" +#include +#include +#include + +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; +using MobileGL::MG_Util::ShaderTranspiler::SpvcSession; + +namespace { + Vector CompileToSpirv(GLenum stage, const String& source) { + using namespace MG_Util::ShaderTranspiler; + ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source}; + auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib); + EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log); + if (!shaderResult) return {}; + + ProgramAttrib programAttrib{.shaders = {shaderResult.value()}}; + auto programResult = ShaderCompiler::LinkProgram(programAttrib); + EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log); + if (!programResult) return {}; + + ProgramBinaryAttrib binaryAttrib{.shaderTypes = {stage}, .program = *programResult.value()}; + auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log); + if (!binaryResult || binaryResult->empty()) return {}; + return binaryResult->front(); + } + + String Disassemble(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + String text; + tools.Disassemble(spirv, &text); + return text; + } + + String Transpile(const Vector& spirv) { + SpvcSession session(spirv, SessionUsageBit::Transpile); + auto essl = ShaderCompiler::DecompileShader(session); + EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log); + return essl ? essl.value() : String{}; + } + + // The KHR-GL43.vertex_attrib_binding.basic-input capture program's output side, verbatim: + // a 16-element vec4 array inside a named output block, which is what the capture list + // addresses member by member ("StageData.attrib[0]" ... "StageData.attrib[15]"). + const char* kCaptureVertexSource = R"(#version 430 core +layout(location = 0) in vec4 vs_in_attrib; +out StageData { + vec4 attrib[16]; +} vs_out; +void main() { + for (int i = 0; i < 16; ++i) { + vs_out.attrib[i] = vs_in_attrib; + } +} +)"; + + // Mixed member widths, so a member that claims the wrong number of locations moves every + // member after it. + // 440, because a location on the BLOCK is ARB_enhanced_layouts. + const char* kMixedBlockVertexSource = R"(#version 440 core +layout(location = 0) in vec4 vs_in_attrib; +layout(location = 0) out StageData { + vec4 first; + vec2 second; + mat4 third; + vec4 fourth; +} vs_out; +void main() { + vs_out.first = vs_in_attrib; + vs_out.second = vs_in_attrib.xy; + vs_out.third = mat4(vs_in_attrib.x); + vs_out.fourth = vs_in_attrib; +} +)"; +} // namespace + +class FlattenXfbInterfaceBlocksTest : public ::testing::Test { +protected: + void SetUp() override { + MobileGL::Initialize(); + ShaderCompiler::SetSpirvValidationEnabled(true); + m_validationFailuresAtStart = ShaderCompiler::SpirvValidationFailureCount(); + } + + void TearDown() override { + EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), m_validationFailuresAtStart) + << "the flattened module did not survive spirv-val"; + } + + Uint64 m_validationFailuresAtStart = 0; +}; + +TEST_F(FlattenXfbInterfaceBlocksTest, FlattensACapturedBlockIntoOneVariablePerMember) { + const Vector input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource); + ASSERT_FALSE(input.empty()); + + std::set flattened; + Vector output; + ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output)); + ASSERT_FALSE(output.empty()); + EXPECT_EQ(flattened, (std::set{"StageData"})); + + const String dis = Disassemble(output); + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + ASSERT_TRUE(tools.Validate(output)) << dis; + EXPECT_NE(dis.find("StageData_attrib"), String::npos) << dis; + // The block itself must have stopped being an interface variable, or the driver would see + // both spellings of the same data. + EXPECT_NE(dis.find("Private"), String::npos) << dis; +} + +// The declaration is the point of the whole exercise: the emitted ESSL has to declare a plain +// output ARRAY, not an interface block, because that is the shape the Adreno driver can capture. +// SPIR-V validation does NOT catch the difference - leaving the struct's Block decoration on the +// demoted shadow produced a module that validated and emitted `StageData vs_out;` next to a block +// declaration the driver rejected with a bare "'vs_out' : syntax error". +TEST_F(FlattenXfbInterfaceBlocksTest, TheEmittedDeclarationIsAPlainArrayNotABlock) { + const Vector input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource); + ASSERT_FALSE(input.empty()); + + // Negative control: untouched, the block is emitted AS a block. + const String before = Transpile(input); + EXPECT_NE(before.find("out StageData"), String::npos) << before; + EXPECT_EQ(before.find("StageData_attrib"), String::npos) << before; + + std::set flattened; + Vector output; + ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output)); + + const String after = Transpile(output); + EXPECT_NE(after.find("StageData_attrib[16]"), String::npos) << after; + EXPECT_EQ(after.find("out StageData"), String::npos) + << "the block must not still be declared as an output block:\n" + << after; +} + +// GL 4.6 core 11.1.2.1: consecutive members take consecutive locations, and a member takes as +// many as its type needs. Getting a span wrong silently moves every member after it. +TEST_F(FlattenXfbInterfaceBlocksTest, GivesEachMemberItsOwnConsecutiveLocations) { + const Vector input = CompileToSpirv(GL_VERTEX_SHADER, kMixedBlockVertexSource); + ASSERT_FALSE(input.empty()); + + std::set flattened; + Vector output; + ASSERT_TRUE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"StageData"}, flattened, output)); + ASSERT_FALSE(output.empty()); + + const String dis = Disassemble(output); + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + ASSERT_TRUE(tools.Validate(output)) << dis; + EXPECT_NE(dis.find("OpDecorate %StageData_first Location 0"), String::npos) << dis; + EXPECT_NE(dis.find("OpDecorate %StageData_second Location 1"), String::npos) << dis; + EXPECT_NE(dis.find("OpDecorate %StageData_third Location 2"), String::npos) << dis; + // mat4 takes four, so the member after it starts at 2 + 4. + EXPECT_NE(dis.find("OpDecorate %StageData_fourth Location 6"), String::npos) << dis; +} + +// Nothing captures this block, so nothing may touch it: a shader that merely HAS an output +// block must reach the driver exactly as it was. +TEST_F(FlattenXfbInterfaceBlocksTest, LeavesABlockNoCaptureNamesAlone) { + const Vector input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource); + ASSERT_FALSE(input.empty()); + + std::set flattened; + Vector output; + ASSERT_TRUE( + ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {"SomeOtherBlock"}, flattened, output)); + EXPECT_TRUE(flattened.empty()); + + const String after = Transpile(output); + EXPECT_NE(after.find("out StageData"), String::npos) << after; + EXPECT_EQ(after.find("StageData_attrib"), String::npos) << after; +} + +// An empty request must not even run the optimizer: every program without transform feedback +// takes this path on every build. +TEST_F(FlattenXfbInterfaceBlocksTest, DeclinesAnEmptyRequestWithoutRewriting) { + const Vector input = CompileToSpirv(GL_VERTEX_SHADER, kCaptureVertexSource); + ASSERT_FALSE(input.empty()); + + std::set flattened; + Vector output; + EXPECT_FALSE(ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(input, {}, flattened, output)); + EXPECT_TRUE(flattened.empty()); + EXPECT_TRUE(output.empty()); +} + +// The capture list has to follow the declaration exactly, and only for blocks that were +// actually rewritten - a member of a block left alone keeps the application's spelling, and so +// does a name with no block prefix at all (gl_Position, a plain varying). +TEST_F(FlattenXfbInterfaceBlocksTest, RewritesOnlyTheCaptureNamesOfFlattenedBlocks) { + String rewritten; + EXPECT_TRUE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("StageData.attrib[0]", {"StageData"}, + rewritten)); + EXPECT_EQ(rewritten, "StageData_attrib[0]"); + + EXPECT_TRUE( + ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("StageData.attrib", {"StageData"}, rewritten)); + EXPECT_EQ(rewritten, "StageData_attrib"); + + EXPECT_FALSE( + ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("Other.member", {"StageData"}, rewritten)); + EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("gl_Position", {"StageData"}, rewritten)); + EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock("vColor", {"StageData"}, rewritten)); + EXPECT_FALSE(ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock(".leading", {"StageData"}, rewritten)); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 69349a38..b8a56feb 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -21,6 +21,7 @@ #include "SpirvPasses/DemoteFloat64Pass.h" #include "SpirvPasses/LowerDrawParametersPass.h" #include "SpirvPasses/PackDoubleVertexInputsPass.h" +#include "SpirvPasses/FlattenXfbInterfaceBlocksPass.h" #include "SpirvPasses/SplitArrayVertexInputsPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h" #include "SpirvPasses/ZeroBaseVertexPass.h" @@ -684,6 +685,26 @@ namespace MobileGL { outputBinary); } + bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector& inputBinary, + const std::set& blockNames, + std::set& flattenedBlockNames, + Vector& outputBinary) { + using namespace spvtools; + if (blockNames.empty()) return false; + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(FlattenXfbInterfaceBlocksPass::CreateFlattenXfbInterfaceBlocksPass( + blockNames, &flattenedBlockNames)); + + return RunOptimizerChecked("FlattenXfbInterfaceBlocksForEssl", optimizer, inputBinary, + outputBinary); + } + + bool ShaderCompiler::RewriteXfbCaptureNameForFlattenedBlock( + const String& captureName, const std::set& flattenedBlockNames, String& outName) { + return FlattenXfbInterfaceBlocksPass::RewriteCaptureName(captureName, flattenedBlockNames, + outName); + } + bool ShaderCompiler::PackDoubleVertexInputsForVulkan(const Vector& inputBinary, Vector& outputBinary) { using namespace spvtools; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 5340dfd2..5bce2494 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -12,6 +12,8 @@ #include "glslang/TVarEntryInfo.h" #include "glslang/TMglGlslIoResolver.h" +#include + namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { @@ -34,6 +36,23 @@ namespace MobileGL { // driver. Only for the DirectGLES transpile path. static bool SplitArrayVertexInputsForEssl(const Vector& inputBinary, Vector& outputBinary); + // Replaces the named interface BLOCKS with one variable per member, named + // "_", shadowing the block itself so the body is untouched. The + // Adreno ES driver silently captures NOTHING for a transform-feedback varying + // named as a block member, so a capture list that names one has to be respelled + // - and the declaration with it. `flattenedBlockNames` reports which blocks + // this stage actually rewrote, which is what the capture list must follow. + // Only for the DirectGLES transpile path. + static bool FlattenXfbInterfaceBlocksForEssl(const Vector& inputBinary, + const std::set& blockNames, + std::set& flattenedBlockNames, + Vector& outputBinary); + // The capture request "StageData.attrib[0]" as the pass above renamed it, + // "StageData_attrib[0]", or false when it does not name a member of a block + // that was flattened. + static bool RewriteXfbCaptureNameForFlattenedBlock(const String& captureName, + const std::set& flattenedBlockNames, + String& outName); // Drops RelaxedPrecision member decorations from uniform-block structs so // SPIRV-Cross prints the same (highp) member precision in every stage; ES // drivers reject cross-stage uniform blocks whose member precisions differ. diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp new file mode 100644 index 00000000..8d252255 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp @@ -0,0 +1,636 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.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 + +#include "FlattenXfbInterfaceBlocksPass.h" + +#include "spirv.hpp" +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_context.h" +#include "source/opt/module.h" +#include "source/opt/types.h" +#include "source/util/make_unique.h" +#include "source/util/string_utils.h" + +#include +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::IRContext; + using spvtools::opt::Instruction; + using spvtools::opt::Operand; + namespace analysis = spvtools::opt::analysis; + + // Decorations that describe how ONE member is interpolated or stored, and + // therefore have to travel from the block member to the variable that replaces + // it. Location is handled separately (it is computed per member); everything + // else about a block - Block itself, member Offsets, builtin decorations - is + // about the aggregate and stays behind on the shadow. + Bool IsMemberDecorationToCarry(spv::Decoration decoration) { + switch (decoration) { + case spv::Decoration::RelaxedPrecision: + case spv::Decoration::Flat: + case spv::Decoration::NoPerspective: + case spv::Decoration::Centroid: + case spv::Decoration::Sample: + case spv::Decoration::Invariant: + case spv::Decoration::Patch: + return true; + default: + return false; + } + } + + // Decorations that say something about a member's placement WITHIN the block's + // location, which a free-standing variable cannot express the same way. Rather + // than move a member to a place the consumer will not look for it, decline. + Bool IsMemberDecorationThatBlocksFlattening(spv::Decoration decoration) { + switch (decoration) { + case spv::Decoration::Component: + case spv::Decoration::XfbBuffer: + case spv::Decoration::XfbStride: + case spv::Decoration::Stream: + return true; + default: + return false; + } + } + + // Every name the module already spells, so a synthesised "_" that + // would collide with one declines instead of emitting two declarations of the + // same identifier (which the driver rejects, taking the whole program with it). + std::unordered_set CollectNames(IRContext& irContext) { + std::unordered_set names; + for (auto& debugInst : irContext.debugs2()) { + if (debugInst.opcode() != spv::Op::OpName) continue; + names.insert(debugInst.GetInOperand(1).AsString()); + } + return names; + } + + // Locations one value of `type` occupies (GL 4.6 core 11.1.2.1 / 15.2): a + // matrix takes one per column, a double-precision vector wider than two takes + // two, an array takes its element's span once per element. 0 means "this pass + // cannot place it", which declines the whole block rather than guessing. + Uint32 LocationSpan(const analysis::Type* type) { + if (type == nullptr) return 0; + if (type->AsFloat() != nullptr || type->AsInteger() != nullptr || + type->AsBool() != nullptr) { + return 1u; + } + if (const auto* vector = type->AsVector()) { + const auto* element = vector->element_type(); + if (element->AsFloat() == nullptr && element->AsInteger() == nullptr && + element->AsBool() == nullptr) { + return 0; + } + const auto* elementFloat = element->AsFloat(); + const Bool is64Bit = elementFloat != nullptr && elementFloat->width() == 64; + return (is64Bit && vector->element_count() > 2) ? 2u : 1u; + } + if (const auto* matrix = type->AsMatrix()) { + const Uint32 columnSpan = LocationSpan(matrix->element_type()); + return columnSpan == 0 ? 0 : columnSpan * matrix->element_count(); + } + if (const auto* array = type->AsArray()) { + const Uint32 elementSpan = LocationSpan(array->element_type()); + if (elementSpan == 0) return 0; + // A runtime array has no span; a vertex-stage interface never has one. + if (!array->length_info().words.empty() && + array->length_info().words[0] != + static_cast(analysis::Array::LengthInfo::kConstant)) { + return 0; + } + // words[0] is the tag, words[1..] the constant value; only a + // single-word length can be an array size here. + if (array->length_info().words.size() != 2) return 0; + const Uint32 count = array->length_info().words[1]; + return count == 0 ? 0 : elementSpan * count; + } + // Structs (a nested block member) carry their own layout rules and are not + // worth guessing at: declining leaves the module exactly as it was. + return 0; + } + + // The Location a variable carries, or false when it carries none. + Bool FindLocationDecoration(IRContext& irContext, Uint32 variableId, Uint32& outLocation) { + for (auto& annotation : irContext.annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate) continue; + if (annotation.GetSingleWordInOperand(0) != variableId) continue; + if (static_cast(annotation.GetSingleWordInOperand(1)) != + spv::Decoration::Location) { + continue; + } + outLocation = annotation.GetSingleWordInOperand(2); + return true; + } + return false; + } + + // The Location decorating member `member` of struct type `typeId`, if any. + Bool FindMemberLocationDecoration(IRContext& irContext, Uint32 typeId, Uint32 member, + Uint32& outLocation) { + for (auto& annotation : irContext.annotations()) { + if (annotation.opcode() != spv::Op::OpMemberDecorate) continue; + if (annotation.GetSingleWordInOperand(0) != typeId) continue; + if (annotation.GetSingleWordInOperand(1) != member) continue; + if (static_cast(annotation.GetSingleWordInOperand(2)) != + spv::Decoration::Location) { + continue; + } + outLocation = annotation.GetSingleWordInOperand(3); + return true; + } + return false; + } + + String FindName(IRContext& irContext, Uint32 id) { + for (auto& debugInst : irContext.debugs2()) { + if (debugInst.opcode() != spv::Op::OpName) continue; + if (debugInst.GetSingleWordInOperand(0) != id) continue; + return debugInst.GetInOperand(1).AsString(); + } + return String(); + } + + String FindMemberName(IRContext& irContext, Uint32 typeId, Uint32 member) { + for (auto& debugInst : irContext.debugs2()) { + if (debugInst.opcode() != spv::Op::OpMemberName) continue; + if (debugInst.GetSingleWordInOperand(0) != typeId) continue; + if (debugInst.GetSingleWordInOperand(1) != member) continue; + return debugInst.GetInOperand(2).AsString(); + } + return String(); + } + } // namespace + + Bool FlattenXfbInterfaceBlocksPass::RewriteCaptureName(const String& captureName, + const std::set& flattenedBlockNames, + String& outName) { + const SizeT dot = captureName.find('.'); + if (dot == String::npos || dot == 0) return false; + const String blockName = captureName.substr(0, dot); + if (flattenedBlockNames.find(blockName) == flattenedBlockNames.end()) return false; + outName = blockName + "_" + captureName.substr(dot + 1); + return true; + } + + spvtools::opt::Pass::Status FlattenXfbInterfaceBlocksPass::Process() { + if (m_blockNames.empty()) return Status::SuccessWithoutChange; + + auto* irContext = context(); + auto entryPoints = irContext->module()->entry_points(); + if (entryPoints.begin() == entryPoints.end()) return Status::SuccessWithoutChange; + Instruction* entryPoint = &*entryPoints.begin(); + + // A stage whose outputs are published somewhere other than the end of the entry + // point cannot take the shadow-and-copy-out shape: a geometry shader's outputs + // are captured by every OpEmitVertex, and a tessellation control shader's are + // per-invocation slots of an arrayed interface. Copying at OpReturn would + // publish once, at the end, which is silently the wrong data rather than a + // failure - so those stages keep their blocks and the capture keeps its + // spelling. + const auto executionModel = + static_cast(entryPoint->GetSingleWordInOperand(0)); + if (executionModel == spv::ExecutionModel::Geometry || + executionModel == spv::ExecutionModel::TessellationControl) { + MGLOG_I("FlattenXfbInterfaceBlocksPass: execution model %u publishes outputs outside " + "the entry point's return; leaving its blocks declared as blocks", + static_cast(executionModel)); + return Status::SuccessWithoutChange; + } + + auto* defUseMgr = irContext->get_def_use_mgr(); + auto* typeMgr = irContext->get_type_mgr(); + + struct Member { + Uint32 typeId = 0; + Uint32 interfacePointerTypeId = 0; + Uint32 privatePointerTypeId = 0; + Uint32 variableId = 0; + Uint32 location = 0; + Bool hasLocation = false; + String name; + }; + struct Target { + Instruction* variable = nullptr; + Uint32 structTypeId = 0; + Uint32 privatePointerTypeId = 0; + spv::StorageClass storageClass = spv::StorageClass::Output; + String blockName; + std::vector members; + }; + std::vector targets; + const std::unordered_set existingNames = CollectNames(*irContext); + + for (Instruction& inst : irContext->types_values()) { + if (inst.opcode() != spv::Op::OpVariable) continue; + const auto storageClass = + static_cast(inst.GetSingleWordInOperand(0)); + if (storageClass != spv::StorageClass::Input && + storageClass != spv::StorageClass::Output) { + continue; + } + Instruction* pointerType = defUseMgr->GetDef(inst.type_id()); + if (pointerType == nullptr) continue; + const Uint32 pointeeTypeId = pointerType->GetSingleWordInOperand(1); + const analysis::Type* pointeeType = typeMgr->GetType(pointeeTypeId); + const auto* structType = pointeeType != nullptr ? pointeeType->AsStruct() : nullptr; + if (structType == nullptr) continue; + + const String blockName = FindName(*irContext, pointeeTypeId); + if (blockName.empty() || + m_blockNames.find(blockName) == m_blockNames.end()) { + continue; + } + + Target target; + target.variable = &inst; + target.structTypeId = pointeeTypeId; + target.storageClass = storageClass; + target.blockName = blockName; + + Uint32 blockLocation = 0; + const Bool hasBlockLocation = + FindLocationDecoration(*irContext, inst.result_id(), blockLocation); + + Bool usable = true; + for (auto& annotation : irContext->annotations()) { + if (annotation.opcode() != spv::Op::OpMemberDecorate) continue; + if (annotation.GetSingleWordInOperand(0) != pointeeTypeId) continue; + if (IsMemberDecorationThatBlocksFlattening( + static_cast(annotation.GetSingleWordInOperand(2)))) { + usable = false; + break; + } + } + Uint32 runningLocation = blockLocation; + const auto& memberTypes = structType->element_types(); + for (Uint32 memberIndex = 0; usable && memberIndex < memberTypes.size(); ++memberIndex) { + Member member; + member.typeId = typeMgr->GetId(memberTypes[memberIndex]); + member.name = FindMemberName(*irContext, pointeeTypeId, memberIndex); + if (member.typeId == 0 || member.name.empty() || + existingNames.count(blockName + "_" + member.name) != 0) { + usable = false; + break; + } + const Uint32 span = LocationSpan(memberTypes[memberIndex]); + if (span == 0) { + usable = false; + break; + } + Uint32 memberLocation = 0; + if (FindMemberLocationDecoration(*irContext, pointeeTypeId, memberIndex, + memberLocation)) { + member.location = memberLocation; + member.hasLocation = true; + } else if (hasBlockLocation) { + member.location = runningLocation; + member.hasLocation = true; + } + runningLocation += span; + target.members.push_back(member); + } + if (!usable || target.members.empty()) { + MGLOG_I("FlattenXfbInterfaceBlocksPass: block '%s' has a member this pass cannot " + "place; leaving it declared as a block", + blockName.c_str()); + continue; + } + targets.push_back(std::move(target)); + } + + if (targets.empty()) return Status::SuccessWithoutChange; + + // The entry point's first block, past the leading OpVariable run SPIR-V + // requires to stay at the top of a function. Resolved BEFORE anything is + // mutated: a decline after the rewrite has started would leave a half-converted + // module behind, and the optimizer serialises whatever the module holds + // regardless of the status this returns. + const Uint32 entryFunctionId = entryPoint->GetSingleWordInOperand(1); + spvtools::opt::Function* entryFunction = nullptr; + for (auto& function : *irContext->module()) { + if (function.result_id() == entryFunctionId) { + entryFunction = &function; + break; + } + } + if (entryFunction == nullptr || entryFunction->begin() == entryFunction->end()) { + return Status::SuccessWithoutChange; + } + { + auto& probeBlock = *entryFunction->begin(); + auto probe = probeBlock.begin(); + while (probe != probeBlock.end() && probe->opcode() == spv::Op::OpVariable) ++probe; + if (probe == probeBlock.end()) return Status::SuccessWithoutChange; + } + + // Demoting the block changes the STORAGE CLASS of every pointer derived from it, + // and a derived pointer's own result type still says Input/Output - which is an + // invalid module ("the result pointer storage class and base pointer storage + // class in OpAccessChain do not match") that spirv-val rejects and a driver may + // silently miscompile. Collected before anything is mutated so an unsupported + // use can still decline the whole rewrite rather than leave the module broken. + std::unordered_set derivedPointers; + for (const auto& target : targets) { + derivedPointers.insert(target.variable->result_id()); + } + std::vector pointersToRetype; + for (auto& function : *irContext->module()) { + for (auto& block : function) { + for (auto& inst : block) { + const spv::Op opcode = inst.opcode(); + const Bool indexes = opcode == spv::Op::OpAccessChain || + opcode == spv::Op::OpInBoundsAccessChain || + opcode == spv::Op::OpCopyObject; + if (indexes) { + if (inst.NumInOperands() > 0 && + derivedPointers.count(inst.GetSingleWordInOperand(0)) != 0) { + derivedPointers.insert(inst.result_id()); + pointersToRetype.push_back(&inst); + } + continue; + } + // Reads and writes are exactly what a shadowed block is for; the + // pointer they use does not change shape. + if (opcode == spv::Op::OpLoad || opcode == spv::Op::OpStore) continue; + for (Uint32 i = 0; i < inst.NumInOperands(); ++i) { + const Operand& operand = inst.GetInOperand(i); + if (operand.type != SPV_OPERAND_TYPE_ID || operand.words.size() != 1) { + continue; + } + if (derivedPointers.count(operand.words[0]) == 0) continue; + MGLOG_I("FlattenXfbInterfaceBlocksPass: interface block %%%u reaches a " + "SPIR-V opcode %u that this pass cannot follow; leaving it " + "declared as a block", + operand.words[0], static_cast(opcode)); + return Status::SuccessWithoutChange; + } + } + } + } + + // Every type this pass names has to exist before the variables that name it. + for (auto& target : targets) { + target.privatePointerTypeId = + typeMgr->FindPointerToType(target.structTypeId, spv::StorageClass::Private); + for (auto& member : target.members) { + member.interfacePointerTypeId = + typeMgr->FindPointerToType(member.typeId, target.storageClass); + member.privatePointerTypeId = + typeMgr->FindPointerToType(member.typeId, spv::StorageClass::Private); + } + } + + std::vector interfaceOperands; + for (Uint32 i = 0; i < entryPoint->NumInOperands(); ++i) { + interfaceOperands.push_back(entryPoint->GetInOperand(i)); + } + + std::unordered_set strippedBlockStructTypes; + for (auto& target : targets) { + Instruction* variable = target.variable; + const Uint32 oldVariableId = variable->result_id(); + + std::vector> newDecorations; + std::vector> newNames; + for (auto& member : target.members) { + member.variableId = irContext->TakeNextId(); + irContext->AddGlobalValue(spvtools::MakeUnique( + irContext, spv::Op::OpVariable, member.interfacePointerTypeId, member.variableId, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, + {static_cast(target.storageClass)}}})); + + // The name IS the contract: it is what the rewritten capture request + // asks the driver for, and what makes the producer and consumer of a + // flattened block still match each other by name. + const String flatName = target.blockName + "_" + member.name; + std::vector nameOperands; + nameOperands.push_back({SPV_OPERAND_TYPE_ID, {member.variableId}}); + nameOperands.push_back( + {SPV_OPERAND_TYPE_LITERAL_STRING, + spvtools::utils::MakeVector(flatName)}); + newNames.push_back(spvtools::MakeUnique(irContext, spv::Op::OpName, 0, 0, + nameOperands)); + + if (member.hasLocation) { + newDecorations.push_back(spvtools::MakeUnique( + irContext, spv::Op::OpDecorate, 0, 0, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {member.variableId}}, + {SPV_OPERAND_TYPE_DECORATION, + {static_cast(spv::Decoration::Location)}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {member.location}}})); + } + } + + // Member decorations that describe the member (not the aggregate) move to + // the variable that now carries it. + for (auto& annotation : irContext->annotations()) { + if (annotation.opcode() != spv::Op::OpMemberDecorate) continue; + if (annotation.GetSingleWordInOperand(0) != target.structTypeId) continue; + const Uint32 memberIndex = annotation.GetSingleWordInOperand(1); + if (memberIndex >= target.members.size()) continue; + const auto decoration = + static_cast(annotation.GetSingleWordInOperand(2)); + if (!IsMemberDecorationToCarry(decoration)) continue; + std::vector operands; + operands.push_back({SPV_OPERAND_TYPE_ID, {target.members[memberIndex].variableId}}); + for (Uint32 i = 2; i < annotation.NumInOperands(); ++i) { + operands.push_back(annotation.GetInOperand(i)); + } + newDecorations.push_back( + spvtools::MakeUnique(irContext, spv::Op::OpDecorate, 0, 0, operands)); + } + + // The block's own Location described where its members start and means + // nothing on a Private shadow; leaving it would also make SPIRV-Cross print + // a location for a variable that no longer has an interface. + std::vector deadDecorations; + // A struct type shared by two flattened variables would otherwise have its + // Block decoration killed twice - and the second kill is a use-after-free, + // not a no-op. + const Bool structTypeAlreadyStripped = + strippedBlockStructTypes.count(target.structTypeId) != 0; + strippedBlockStructTypes.insert(target.structTypeId); + for (auto& annotation : irContext->annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate) continue; + const Uint32 target0 = annotation.GetSingleWordInOperand(0); + if (target0 == oldVariableId) { + deadDecorations.push_back(&annotation); + continue; + } + if (structTypeAlreadyStripped) continue; + // The struct type stops being an interface block the moment its only + // interface variable becomes a Private shadow, and SPIRV-Cross prints a + // Block-decorated struct as a BLOCK declaration - `out StageData {...} + // vs_out;` for a variable that is no longer an output, which is not + // ESSL and which the driver rejects with a bare syntax error on the + // instance name. A block name is unique per stage, so nothing else can + // still need this decoration. + if (target0 == target.structTypeId && + static_cast(annotation.GetSingleWordInOperand(1)) == + spv::Decoration::Block) { + deadDecorations.push_back(&annotation); + } + } + for (auto* annotation : deadDecorations) { + irContext->KillInst(annotation); + } + for (auto& decoration : newDecorations) { + irContext->AddAnnotationInst(std::move(decoration)); + } + for (auto& debugName : newNames) { + irContext->AddDebug2Inst(std::move(debugName)); + } + + // Demote the block to a Private shadow. Every OpAccessChain and OpStore the + // body already performs on it stays valid and keeps its types; only the + // storage class changed, and Private is the one storage class a pointer of + // any shape may live in. + variable->SetResultType(target.privatePointerTypeId); + variable->SetInOperand(0, {static_cast(spv::StorageClass::Private)}); + variable->RemoveFromList(); + irContext->AddGlobalValue(std::unique_ptr(variable)); + + // SPIR-V 1.3 lists only Input/Output in the entry-point interface, and the + // block is neither any more: it is replaced in place by its members. + std::vector rebuilt; + for (Uint32 i = 0; i < interfaceOperands.size(); ++i) { + const Operand& operand = interfaceOperands[i]; + if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID && operand.words.size() == 1 && + operand.words[0] == oldVariableId) { + for (const auto& member : target.members) { + rebuilt.push_back({SPV_OPERAND_TYPE_ID, {member.variableId}}); + } + continue; + } + rebuilt.push_back(operand); + } + interfaceOperands = std::move(rebuilt); + } + + entryPoint->SetInOperands(std::move(interfaceOperands)); + + // Index constants for the copy access chains. Appended after the variables + // above, which is legal: nothing declared before them names them, and the only + // instructions that do are the ones inserted into the function body below. + auto* constMgr = irContext->get_constant_mgr(); + std::vector memberIndexConstants; + Uint32 maxMembers = 0; + for (const auto& target : targets) { + maxMembers = std::max(maxMembers, static_cast(target.members.size())); + } + memberIndexConstants.resize(maxMembers, 0); + for (Uint32 index = 0; index < maxMembers; ++index) { + memberIndexConstants[index] = constMgr->GetUIntConstId(index); + } + + // Input: seed the shadow once, before any code that reads it. Output: publish + // it at every exit, after all the code that writes it. + auto& entryBlock = *entryFunction->begin(); + auto insertPoint = entryBlock.begin(); + while (insertPoint != entryBlock.end() && insertPoint->opcode() == spv::Op::OpVariable) { + ++insertPoint; + } + if (insertPoint == entryBlock.end()) return Status::SuccessWithoutChange; + + for (const auto& target : targets) { + if (target.storageClass != spv::StorageClass::Input) continue; + for (Uint32 memberIndex = 0; memberIndex < target.members.size(); ++memberIndex) { + const auto& member = target.members[memberIndex]; + const Uint32 loadedId = irContext->TakeNextId(); + const Uint32 memberPointerId = irContext->TakeNextId(); + insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpLoad, member.typeId, loadedId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {member.variableId}}})); + ++insertPoint; + insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpAccessChain, member.privatePointerTypeId, memberPointerId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {target.variable->result_id()}}, + {SPV_OPERAND_TYPE_ID, {memberIndexConstants[memberIndex]}}})); + ++insertPoint; + insertPoint = insertPoint.InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpStore, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {memberPointerId}}, + {SPV_OPERAND_TYPE_ID, {loadedId}}})); + ++insertPoint; + } + } + + // EVERY return, not just the last block: a shader with an early `return` would + // otherwise publish nothing on that path. + std::vector returns; + for (auto& block : *entryFunction) { + Instruction* terminator = block.terminator(); + if (terminator != nullptr && terminator->opcode() == spv::Op::OpReturn) { + returns.push_back(terminator); + } + } + for (Instruction* returnInst : returns) { + for (const auto& target : targets) { + if (target.storageClass != spv::StorageClass::Output) continue; + for (Uint32 memberIndex = 0; memberIndex < target.members.size(); ++memberIndex) { + const auto& member = target.members[memberIndex]; + const Uint32 memberPointerId = irContext->TakeNextId(); + const Uint32 loadedId = irContext->TakeNextId(); + returnInst->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpAccessChain, member.privatePointerTypeId, + memberPointerId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {target.variable->result_id()}}, + {SPV_OPERAND_TYPE_ID, {memberIndexConstants[memberIndex]}}})); + returnInst->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpLoad, member.typeId, loadedId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {memberPointerId}}})); + returnInst->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpStore, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {member.variableId}}, + {SPV_OPERAND_TYPE_ID, {loadedId}}})); + } + } + } + + // Retype the derived pointers collected above. Done last, so the pointer types it + // appends land after the variables. + for (Instruction* pointer : pointersToRetype) { + Instruction* resultType = defUseMgr->GetDef(pointer->type_id()); + if (resultType == nullptr || resultType->opcode() != spv::Op::OpTypePointer) continue; + if (static_cast(resultType->GetSingleWordInOperand(0)) == + spv::StorageClass::Private) { + continue; + } + pointer->SetResultType(typeMgr->FindPointerToType(resultType->GetSingleWordInOperand(1), + spv::StorageClass::Private)); + } + + if (m_flattenedBlockNames != nullptr) { + for (const auto& target : targets) { + m_flattenedBlockNames->insert(target.blockName); + } + } + + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken FlattenXfbInterfaceBlocksPass::CreateFlattenXfbInterfaceBlocksPass( + const std::set& blockNames, std::set* flattenedBlockNames) { + return spvtools::Optimizer::PassToken( + MakeUnique(blockNames, flattenedBlockNames)); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.h new file mode 100644 index 00000000..0fd4cf00 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.h @@ -0,0 +1,73 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.h +// 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 + +#pragma once +#include "source/opt/pass.h" +#include "spirv-tools/optimizer.hpp" + +#include + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // Replaces a named interface BLOCK whose members transform feedback captures with + // one free-standing variable per member, named "_", and + // demotes the block itself to a Private shadow that the entry point copies into + // (Input) or out of (Output). The block's own body code is untouched: every + // OpAccessChain into it - dynamically indexed ones included - keeps working + // against the shadow. + // + // WHY. The Adreno ES driver accepts "BlockName.member" in + // glTransformFeedbackVaryings, links, and reports the names straight back from + // glGetTransformFeedbackVarying - and then captures NONE of them: the recorded + // stream holds gl_Position in slot 0 and leaves the rest of every vertex's record + // untouched. Proven on an Adreno 830 by writing a recognisable gl_Position into + // the KHR-GL43.vertex_attrib_binding.basic-input capture program: the value landed + // in the slot that had asked for StageData.attrib[0]. The same program with the + // block flattened into a plain output array captures every member correctly, which + // is what this pass produces. Requesting the members under the ESSL instance-name + // spelling ("vs_out.attrib[0]") instead makes the driver fail the link outright, so + // the capture list cannot be spelled around the defect - only the declaration can. + // + // The rename is deterministic and derived only from names that GLSL interface + // matching already requires both sides of a stage boundary to agree on (block name + // and member names), so running this pass over EVERY stage of a program with the + // same block set keeps a producer and its consumer matched. + // + // DirectGLES only: Vulkan captures by xfb_offset, never by name. + class FlattenXfbInterfaceBlocksPass : public spvtools::opt::Pass { + public: + // `blockNames` are block TYPE names, i.e. the "StageData" of + // "StageData.attrib[0]". `flattenedBlockNames` receives the subset this pass + // actually rewrote, so the caller can rename exactly those capture requests + // and leave the rest spelled as the application wrote them. + FlattenXfbInterfaceBlocksPass(const std::set& blockNames, + std::set* flattenedBlockNames) + : m_blockNames(blockNames), m_flattenedBlockNames(flattenedBlockNames) {} + + const char* name() const override { return "mobilegl-flatten-xfb-interface-blocks"; } + Status Process() override; + + // The member of `blockName` spelled by a capture request, as this pass names + // it: "StageData.attrib[0]" -> "StageData_attrib[0]". Returns false when the + // name does not address a member of a flattened block. + static Bool RewriteCaptureName(const String& captureName, const std::set& flattenedBlockNames, + String& outName); + + static spvtools::Optimizer::PassToken CreateFlattenXfbInterfaceBlocksPass( + const std::set& blockNames, std::set* flattenedBlockNames); + + private: + std::set m_blockNames; + std::set* m_flattenedBlockNames = nullptr; + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL