diff --git a/CMakeLists.txt b/CMakeLists.txt index 33068f02..17b53424 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -300,6 +300,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index a4fe2568..aaf44b8b 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -5405,6 +5405,21 @@ namespace MobileGL::MG_Backend::DirectGLES { effectiveSpirv = &outputIndexSpirv; } + // Same rule, different resource, every stage: GL 4.3 lets an array of storage + // blocks be indexed with any dynamically-uniform expression, GLSL ES keeps the + // ES 3.1 constant-expression rule, and the Qualcomm compiler enforces it + // ("indexing into an SSBO array using a non-constant expression is not + // permitted") - losing the stage, the program, and every dispatch that used + // it, while the frontend keeps reporting the link glslang performed. Fold or + // lower the index here, on the ESSL path only: the same module is legal for + // DirectVulkan, which binds the array as one descriptor array. + Vector blockArrayIndexSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl( + *effectiveSpirv, blockArrayIndexSpirv, enableSpirvValidation) && + !blockArrayIndexSpirv.empty()) { + effectiveSpirv = &blockArrayIndexSpirv; + } + MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile); diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index ea34ed21..335fdabb 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable( UniquifyIoBlockNamesTest.cpp LowerViewportIndexTest.cpp ClampMultisampleFetchTest.cpp + LegalizeStorageBlockArrayIndexTest.cpp ) target_include_directories(SpirvPassTest PRIVATE diff --git a/MobileGL/MG_Test/ShaderTranspiler/LegalizeStorageBlockArrayIndexTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/LegalizeStorageBlockArrayIndexTest.cpp new file mode 100644 index 00000000..f4c7bfbd --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/LegalizeStorageBlockArrayIndexTest.cpp @@ -0,0 +1,251 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LegalizeStorageBlockArrayIndexTest.cpp +// Copyright (c) 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 + +#define SPV_ENABLE_UTILITY_CODE +#include "glslang/SPIRV/spirv.hpp11" +#undef SPV_ENABLE_UTILITY_CODE + +#include "Includes.h" +#include +#include + +#include + +#include +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + constexpr SizeT kSpirvHeaderWordCount = 5u; + + template + void ForEachInstruction(const Vector& spirv, Visitor&& visit) { + for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) { + const Uint32 wordCount = spirv[offset] >> 16u; + if (wordCount == 0u || offset + wordCount > spirv.size()) break; + visit(static_cast(spirv[offset] & 0xffffu), &spirv[offset], wordCount); + offset += wordCount; + } + } + + Vector CompileCompute(const String& source) { + using namespace MobileGL::MG_Util::ShaderTranspiler; + ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .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 = {GL_COMPUTE_SHADER}, .program = *programResult.value()}; + auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log); + if (!binaryResult || binaryResult->empty()) return {}; + return binaryResult->front(); + } + + bool Validates(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + tools.SetMessageConsumer( + [](spv_message_level_t, const char*, const spv_position_t& position, const char* message) { + ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message; + }); + return tools.Validate(spirv); + } + + Uint32 CountOpcode(const Vector& spirv, spv::Op wanted) { + Uint32 count = 0u; + ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) { + if (opcode == wanted) ++count; + }); + return count; + } + + // Test-side reference walker, deliberately independent of the production detection so a + // bug in the pass cannot hide behind the same helper: true when some access chain rooted + // at an array-of-storage-blocks variable carries a non-constant FIRST index, which is + // exactly what the Qualcomm ES compiler refuses. + bool HasDynamicBlockArrayIndex(const Vector& spirv) { + std::set blockStructs; // OpTypeStruct ids decorated Block / BufferBlock + std::set constants; // OpConstant / OpConstantNull result ids + std::set blockArrayTypes; // OpTypeArray ids whose element is such a struct + std::set blockArrayPointers;// OpTypePointer ids pointing at one of those arrays + std::set blockArrayVars; // OpVariable ids of one of those pointer types + + ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) { + switch (opcode) { + case spv::Op::OpDecorate: + if (wordCount >= 3u) { + const auto decoration = static_cast(words[2]); + if (decoration == spv::Decoration::Block || + decoration == spv::Decoration::BufferBlock) { + blockStructs.insert(words[1]); + } + } + break; + case spv::Op::OpConstant: + if (wordCount >= 3u) constants.insert(words[2]); + break; + case spv::Op::OpConstantNull: + if (wordCount >= 3u) constants.insert(words[2]); + break; + case spv::Op::OpTypeArray: + // OpTypeArray + if (wordCount >= 4u && blockStructs.count(words[2]) != 0u) { + blockArrayTypes.insert(words[1]); + } + break; + case spv::Op::OpTypePointer: + // OpTypePointer + if (wordCount >= 4u && blockArrayTypes.count(words[3]) != 0u) { + blockArrayPointers.insert(words[1]); + } + break; + case spv::Op::OpVariable: + // OpVariable + if (wordCount >= 4u && blockArrayPointers.count(words[1]) != 0u) { + blockArrayVars.insert(words[2]); + } + break; + default: + break; + } + }); + + bool dynamic = false; + ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) { + if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return; + // OpAccessChain ... + if (wordCount < 5u) return; + if (blockArrayVars.count(words[3]) == 0u) return; + if (constants.count(words[4]) != 0u) return; + dynamic = true; + }); + return dynamic; + } + + // `for (i = 0; i < 4; ++i)` over an array of storage blocks - the shape + // KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1 uses. Foldable: the + // induction variable is a literal after unrolling. + constexpr const char* kLoopIndexedBlockArray = R"(#version 450 core +layout(local_size_x = 1) in; +layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4]; +layout(std430, binding = 8) buffer Out { uint data[4]; } g_out; +void main() { + for (int i = 0; i < 4; ++i) { + g_out.data[i] = g_blocks[i].data[0]; + } +} +)"; + + // A uniform-sourced index - the shape + // KHR-GL43.shader_storage_buffer_object.advanced-indirectAddressing-case2 uses. Nothing + // can fold it, so the switch/select lowering is what has to carry it. + constexpr const char* kUniformIndexedBlockArray = R"(#version 450 core +layout(local_size_x = 1) in; +layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4]; +layout(std430, binding = 8) buffer Out { uint value; } g_out; +uniform int g_index; +void main() { + g_blocks[g_index].data[0] = 7u; + g_out.value = g_blocks[g_index].data[1]; +} +)"; + + // The positive control from the device run: dynamic addressing through an array MEMBER of + // ONE block is legal ES and must not be rewritten. + constexpr const char* kArrayMemberInsideOneBlock = R"(#version 450 core +layout(local_size_x = 1) in; +layout(std430, binding = 0) buffer Blk { uint data[4]; } g_block; +layout(std430, binding = 8) buffer Out { uint value; } g_out; +uniform int g_index; +void main() { + g_out.value = g_block.data[g_index]; +} +)"; + + // A block array indexed only with literals is already legal ES. + constexpr const char* kConstantIndexedBlockArray = R"(#version 450 core +layout(local_size_x = 1) in; +layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4]; +layout(std430, binding = 8) buffer Out { uint value; } g_out; +void main() { + g_out.value = g_blocks[2].data[0] + g_blocks[3].data[1]; +} +)"; +} // namespace + +TEST(LegalizeStorageBlockArrayIndexPass, FoldsALoopIndexedBlockArray) { + const Vector input = CompileCompute(kLoopIndexedBlockArray); + ASSERT_FALSE(input.empty()); + EXPECT_TRUE(HasDynamicBlockArrayIndex(input)); + + Vector output; + ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true)); + ASSERT_FALSE(output.empty()); + // Either half of the legalization is an acceptable outcome here - what the ES driver + // cares about is only that no dynamic subscript survives. + EXPECT_FALSE(HasDynamicBlockArrayIndex(output)); + EXPECT_TRUE(Validates(output)); +} + +TEST(LegalizeStorageBlockArrayIndexPass, LowersAUniformIndexedWriteToASwitchAndAReadToSelects) { + const Vector input = CompileCompute(kUniformIndexedBlockArray); + ASSERT_FALSE(input.empty()); + EXPECT_TRUE(HasDynamicBlockArrayIndex(input)); + EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u); + + Vector output; + ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true)); + ASSERT_FALSE(output.empty()); + EXPECT_FALSE(HasDynamicBlockArrayIndex(output)); + // One switch for the store, and one select per element past the first for the load. + EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u); + EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u); + EXPECT_TRUE(Validates(output)); +} + +TEST(LegalizeStorageBlockArrayIndexPass, LeavesADynamicMemberOfOneBlockByteIdentical) { + const Vector input = CompileCompute(kArrayMemberInsideOneBlock); + ASSERT_FALSE(input.empty()); + EXPECT_FALSE(HasDynamicBlockArrayIndex(input)); + + Vector output; + ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true)); + EXPECT_EQ(output, input); +} + +TEST(LegalizeStorageBlockArrayIndexPass, LeavesAConstantIndexedBlockArrayByteIdentical) { + const Vector input = CompileCompute(kConstantIndexedBlockArray); + ASSERT_FALSE(input.empty()); + EXPECT_FALSE(HasDynamicBlockArrayIndex(input)); + + Vector output; + ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true)); + EXPECT_EQ(output, input); +} + +TEST(LegalizeStorageBlockArrayIndexPass, IsIdempotent) { + const Vector input = CompileCompute(kUniformIndexedBlockArray); + ASSERT_FALSE(input.empty()); + + Vector once; + ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, once, true)); + ASSERT_FALSE(once.empty()); + + Vector twice; + ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(once, twice, true)); + EXPECT_EQ(twice, once); +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index c691f5e8..54a1743b 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -41,6 +41,7 @@ #include "SpirvPasses/StripNoPerspectivePass.h" #include "SpirvPasses/EmulateNoPerspectivePass.h" #include "SpirvPasses/LegalizeFragmentOutputIndexPass.h" +#include "SpirvPasses/LegalizeStorageBlockArrayIndexPass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" #include "source/opt/build_module.h" @@ -933,6 +934,80 @@ namespace MobileGL { return true; } + bool ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl( + const Vector& inputBinary, Vector& outputBinary, + const bool enableSpirvValidation) { + using namespace spvtools; + + // Detection gates everything: a module that declares no array of storage + // blocks, or indexes one only with constants - every shader but a handful - + // pays one BuildModule and is handed back byte for byte, so the folding chain + // can never perturb a shader that did not need it. + if (!LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing( + inputBinary)) { + outputBinary = inputBinary; + return true; + } + + // Stock passes do the real work, exactly as in the fragment-output + // legalization. The only bespoke member of the chain is the loop-control hint + // the stock unroller demands (see the pass header); with it set, the + // `for (i = 0; i < 4; ++i) arr[i]...` shape folds to literals here and the + // fallback below never runs. + Optimizer folder(SPV_ENV_VULKAN_1_1); + // First, because both the unroller and the marking pass below read the + // induction variable as an OpPhi, and glslang emits it as loads and stores of + // a Function variable. + folder.RegisterPass(CreateLocalMultiStoreElimPass()); + folder.RegisterPass(LegalizeStorageBlockArrayIndexPass::CreateMarkLoopsForUnrollPass()); + folder.RegisterPass(CreateLoopUnrollPass(true)); + // Fold the unrolled induction values into the access chains, then clear out + // what constant conditions leave behind. + folder.RegisterPass(CreateCCPPass()); + folder.RegisterPass(CreateSimplificationPass()); + folder.RegisterPass(CreateDeadBranchElimPass()); + folder.RegisterPass(CreateBlockMergePass()); + + Vector folded; + if (!RunOptimizerChecked("LegalizeStorageBlockArrayIndexingForEssl.fold", folder, + inputBinary, folded, true, enableSpirvValidation) || + folded.empty()) { + // Fail open onto the fallback rather than onto the illegal module. + folded = inputBinary; + } + + if (!LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing( + folded)) { + outputBinary = folded; + return true; + } + + // Genuinely dynamic (uniform-derived, non-constant trip count, ...): lower it. + Optimizer lowerer(SPV_ENV_VULKAN_1_1); + lowerer.RegisterPass( + LegalizeStorageBlockArrayIndexPass::CreateLowerToConstantSwitchPass()); + // The chains the lowering replaced are dead now; remove_outputs must stay + // false here for the same reason it does in SanitizeAndOptimizeBinary. + lowerer.RegisterPass(CreateAggressiveDCEPass(false)); + + if (!RunOptimizerChecked("LegalizeStorageBlockArrayIndexingForEssl.lower", lowerer, folded, + outputBinary, true, enableSpirvValidation) || + outputBinary.empty()) { + outputBinary = folded; + return true; + } + + if (LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing( + outputBinary)) { + // MGLOG_W, latched, for the same reason the fragment-output one is: this + // runs per shader compile and shader packs compile lazily mid-session. + MGLOG_W_ONCE("[spirv] LegalizeStorageBlockArrayIndexingForEssl: an array of storage " + "blocks is still indexed dynamically; a strict ES driver will reject " + "this shader"); + } + return true; + } + bool ShaderCompiler::LowerRectImages(const Vector& inputBinary, Vector& outputBinary, const bool enableSpirvValidation) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 84e84ab6..c55c8275 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -156,6 +156,20 @@ namespace MobileGL { static bool LegalizeFragmentOutputIndexingForEssl(const Vector& inputBinary, Vector& outputBinary, bool enableSpirvValidation = false); + // Makes every index into an ARRAY OF SHADER STORAGE BLOCKS a constant integral + // expression. GL 4.3 allows any dynamically-uniform index there; the Qualcomm + // ES compiler enforces the ES 3.1 constant-expression rule and refuses the whole + // stage ("indexing into an SSBO array using a non-constant expression is not + // permitted"), which loses the program while the frontend still reports + // GL_LINK_STATUS = TRUE. Same two halves as the fragment-output legalization: + // fold the loop-derived indices, then lower whatever is genuinely dynamic to a + // switch over the array's range. DirectGLES transpile path only - Vulkan has no + // such restriction and must keep seeing one descriptor array. Copies the input + // through untouched when no block array is indexed dynamically, which is every + // shader but a handful. See LegalizeStorageBlockArrayIndexPass. + static bool LegalizeStorageBlockArrayIndexingForEssl(const Vector& inputBinary, + Vector& outputBinary, + bool enableSpirvValidation = false); // Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so // shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan // backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex, diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp new file mode 100644 index 00000000..afa25909 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.cpp @@ -0,0 +1,610 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.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 "LegalizeStorageBlockArrayIndexPass.h" + +#include "spirv.hpp" +#include "source/opt/basic_block.h" +#include "source/opt/build_module.h" +#include "source/opt/constants.h" +#include "source/opt/decoration_manager.h" +#include "source/opt/def_use_manager.h" +#include "source/opt/function.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_builder.h" +#include "source/opt/ir_context.h" +#include "source/opt/loop_descriptor.h" +#include "source/opt/module.h" +#include "source/opt/type_manager.h" +#include "source/util/make_unique.h" + +#include +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::MakeUnique; + using spvtools::opt::BasicBlock; + using spvtools::opt::Function; + using spvtools::opt::Instruction; + using spvtools::opt::InstructionBuilder; + using spvtools::opt::IRContext; + using spvtools::opt::Operand; + + // GL_MAX_*_SHADER_STORAGE_BLOCKS is 16 on the devices MobileGL targets, and + // each lowered element costs one basic block per write, so a module claiming + // more than this is refused rather than exploded. The largest array in the + // conformance suite is 8. + constexpr uint32_t kMaxLoweredArrayLength = 32; + // One CFG-changing rewrite per round (analyses are dropped after each), so + // the round budget bounds the work on a pathological module. + constexpr int kMaxLoweringRounds = 256; + // Full unrolling copies the body once per iteration, and nothing in the stock + // unroller bounds that. Past this count the loop is left alone and the switch + // lowering, whose cost is the array length rather than the trip count, takes + // it instead. A loop over an array of storage blocks iterates at most + // GL_MAX_*_SHADER_STORAGE_BLOCKS times in any shader that is not already + // broken. + constexpr size_t kMaxUnrolledIterations = 64; + + struct DynamicIndexUse { + Instruction* accessChain = nullptr; + uint32_t arrayLength = 0; + }; + + bool HasDecoration(IRContext* context, uint32_t id, spv::Decoration kind) { + for (Instruction* decoration : context->get_decoration_mgr()->GetDecorationsFor(id, false)) { + if (decoration->opcode() != spv::Op::OpDecorate || + decoration->NumInOperands() < 2) { + continue; + } + if (static_cast(decoration->GetSingleWordInOperand(1)) == kind) { + return true; + } + } + return false; + } + + // Every variable that is an ARRAY OF STORAGE BLOCKS, mapped to that array's + // length. Two spellings are accepted because both reach here depending on the + // SPIR-V version glslang targets: StorageBuffer + Block (1.3, what MobileGL + // asks for) and Uniform + BufferBlock (the pre-1.3 encoding). A UNIFORM block + // array - Uniform + Block - is deliberately NOT collected; see the header. + // + // A length that is not a plain OpConstant (a spec constant) maps to 0: still + // detected as illegal ESSL, never lowered. + std::unordered_map CollectStorageBlockArrays(IRContext* context) { + std::unordered_map blockArrays; + auto* defUseMgr = context->get_def_use_mgr(); + auto* constantMgr = context->get_constant_mgr(); + + for (Instruction& inst : context->module()->types_values()) { + if (inst.opcode() != spv::Op::OpVariable) { + continue; + } + const auto storageClass = + static_cast(inst.GetSingleWordInOperand(0)); + if (storageClass != spv::StorageClass::StorageBuffer && + storageClass != spv::StorageClass::Uniform) { + continue; + } + + Instruction* pointerType = defUseMgr->GetDef(inst.type_id()); + if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) { + continue; + } + Instruction* pointeeType = defUseMgr->GetDef(pointerType->GetSingleWordInOperand(1)); + if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray) { + continue; + } + Instruction* elementType = defUseMgr->GetDef(pointeeType->GetSingleWordInOperand(0)); + if (elementType == nullptr || elementType->opcode() != spv::Op::OpTypeStruct) { + continue; + } + + const bool isStorageBlock = + storageClass == spv::StorageClass::StorageBuffer + ? HasDecoration(context, elementType->result_id(), spv::Decoration::Block) + : HasDecoration(context, elementType->result_id(), + spv::Decoration::BufferBlock); + if (!isStorageBlock) { + continue; + } + + uint32_t arrayLength = 0; + const spvtools::opt::analysis::Constant* lengthConstant = + constantMgr->FindDeclaredConstant(pointeeType->GetSingleWordInOperand(1)); + if (lengthConstant != nullptr && lengthConstant->AsIntConstant() != nullptr) { + arrayLength = lengthConstant->AsIntConstant()->GetU32BitValue(); + } + blockArrays.emplace(inst.result_id(), arrayLength); + } + return blockArrays; + } + + // "Constant integral expression" in the ESSL sense: an OpConstant (or the + // zero an OpConstantNull stands for). A spec constant is deliberately NOT + // one - SPIRV-Cross prints it as an identifier, which is exactly what the + // driver rejects. + bool IsConstantIndex(IRContext* context, uint32_t indexId) { + Instruction* def = context->get_def_use_mgr()->GetDef(indexId); + return def != nullptr && (def->opcode() == spv::Op::OpConstant || + def->opcode() == spv::Op::OpConstantNull); + } + + // Access chains that index an array of storage blocks with a non-constant. + // Only the FIRST index is considered: it is the one that selects the block, + // and it is the only one ESSL constrains here. Indices inside the block - the + // member selector and any array subscript below it - are legal however they + // are computed, and chains rooted at another access chain are already inside + // one element. + std::vector CollectDynamicIndexUses(IRContext* context) { + std::vector uses; + const std::unordered_map blockArrays = + CollectStorageBlockArrays(context); + if (blockArrays.empty()) { + return uses; + } + + for (Function& function : *context->module()) { + for (BasicBlock& block : function) { + for (Instruction& inst : block) { + if (inst.opcode() != spv::Op::OpAccessChain && + inst.opcode() != spv::Op::OpInBoundsAccessChain) { + continue; + } + if (inst.NumInOperands() < 2) { + continue; + } + const auto arrayIt = blockArrays.find(inst.GetSingleWordInOperand(0)); + if (arrayIt == blockArrays.end()) { + continue; + } + if (IsConstantIndex(context, inst.GetSingleWordInOperand(1))) { + continue; + } + uses.push_back({&inst, arrayIt->second}); + } + } + } + return uses; + } + + // The block index operand of |accessChain| replaced by the constant |element|, + // built at the builder's insertion point. Every later index is copied through + // unchanged: `arr[idx].data[j]` keeps its (legal) dynamic member subscript. + Instruction* CloneChainWithConstantIndex(InstructionBuilder& builder, IRContext* context, + Instruction* accessChain, uint32_t constantIndexId) { + std::vector operands; + operands.reserve(accessChain->NumInOperands()); + for (uint32_t i = 0; i < accessChain->NumInOperands(); ++i) { + if (i == 1) { + operands.push_back({SPV_OPERAND_TYPE_ID, {constantIndexId}}); + } else { + operands.push_back(accessChain->GetInOperand(i)); + } + } + return builder.AddInstruction(MakeUnique(context, accessChain->opcode(), + accessChain->type_id(), + context->TakeNextId(), operands)); + } + + // The id of |element| as a constant of the same integer type as |indexId|. + uint32_t ConstantLikeIndex(IRContext* context, uint32_t indexId, uint32_t element) { + Instruction* indexDef = context->get_def_use_mgr()->GetDef(indexId); + const spvtools::opt::analysis::Type* indexType = + context->get_type_mgr()->GetType(indexDef->type_id()); + const spvtools::opt::analysis::Constant* constant = + context->get_constant_mgr()->GetConstant(indexType, {element}); + return context->get_constant_mgr()->GetDefiningInstruction(constant)->result_id(); + } + + // A 32-bit integer is the only index this pass lowers: OpSwitch matches its + // literals against the selector's width, and every ESSL block-array index is + // an int or uint. + bool IsLowerableIndexType(IRContext* context, uint32_t indexId) { + Instruction* indexDef = context->get_def_use_mgr()->GetDef(indexId); + if (indexDef == nullptr) { + return false; + } + const spvtools::opt::analysis::Type* type = + context->get_type_mgr()->GetType(indexDef->type_id()); + const spvtools::opt::analysis::Integer* integer = + type != nullptr ? type->AsInteger() : nullptr; + return integer != nullptr && integer->width() == 32; + } + + // The condition type OpSelect needs for |resultTypeId|. Before SPIR-V 1.4 a + // scalar bool may not select between vectors, so a vector result needs a bool + // vector of the same width - built by broadcasting the scalar comparison. + // Anything that is neither scalar nor vector (a matrix or struct element) is + // refused: pre-1.4 OpSelect cannot express it either. + bool TryGetSelectConditionType(IRContext* context, uint32_t resultTypeId, + uint32_t* conditionTypeId, uint32_t* dimension) { + auto* typeMgr = context->get_type_mgr(); + const spvtools::opt::analysis::Type* resultType = typeMgr->GetType(resultTypeId); + if (resultType == nullptr) { + return false; + } + + spvtools::opt::analysis::Bool boolType; + if (resultType->AsVector() != nullptr) { + const uint32_t count = resultType->AsVector()->element_count(); + spvtools::opt::analysis::Vector boolVector(&boolType, count); + *conditionTypeId = typeMgr->GetTypeInstruction(&boolVector); + *dimension = count; + return *conditionTypeId != 0; + } + if (resultType->AsInteger() != nullptr || resultType->AsFloat() != nullptr || + resultType->AsBool() != nullptr) { + *conditionTypeId = typeMgr->GetTypeInstruction(&boolType); + *dimension = 1; + return *conditionTypeId != 0; + } + return false; + } + + // Whether fully unrolling |loop| is bounded work. The trip count is read the + // same way the stock unroller reads it, so a loop this declines to measure is + // one CanPerformUnroll would refuse anyway - the hint would be inert on it, + // and the fallback lowering is what handles it. Requires the induction + // variable to already be an OpPhi, which is why this runs after ssa-rewrite. + bool IsBoundedUnrollCandidate(spvtools::opt::Loop* loop) { + const spvtools::opt::BasicBlock* condition = loop->FindConditionBlock(); + if (condition == nullptr) { + return false; + } + const Instruction* induction = loop->FindConditionVariable(condition); + if (induction == nullptr || induction->opcode() != spv::Op::OpPhi) { + return false; + } + size_t iterations = 0; + if (!loop->FindNumberOfIterations(induction, &*condition->ctail(), &iterations)) { + return false; + } + return iterations <= kMaxUnrolledIterations; + } + } // namespace + + bool LegalizeStorageBlockArrayIndexPass::BinaryHasDynamicStorageBlockArrayIndexing( + const std::vector& binary) { + if (binary.empty()) { + return false; + } + std::unique_ptr context = spvtools::BuildModule( + SPV_ENV_VULKAN_1_1, + [](spv_message_level_t, const char*, const spv_position_t&, const char*) {}, + binary.data(), binary.size()); + if (!context) { + return false; + } + return !CollectDynamicIndexUses(context.get()).empty(); + } + + spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::Process() { + return m_mode == Mode::MarkLoopsForUnroll ? MarkLoopsForUnroll() : LowerToConstantSwitch(); + } + + spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::MarkLoopsForUnroll() { + auto* irContext = context(); + const std::vector uses = CollectDynamicIndexUses(irContext); + if (uses.empty()) { + return Status::SuccessWithoutChange; + } + + bool modified = false; + for (const DynamicIndexUse& use : uses) { + BasicBlock* block = irContext->get_instr_block(use.accessChain); + if (block == nullptr) { + continue; + } + Function* function = block->GetParent(); + if (function == nullptr) { + continue; + } + + spvtools::opt::LoopDescriptor* loops = irContext->GetLoopDescriptor(function); + for (spvtools::opt::Loop* loop = (*loops)[block->id()]; loop != nullptr; + loop = loop->GetParent()) { + if (!IsBoundedUnrollCandidate(loop)) { + continue; + } + Instruction* mergeInst = loop->GetHeaderBlock()->GetLoopMergeInst(); + // Only a bare `None` control is promoted, and only when no extra + // literal (PartialCount, PeelCount, ...) follows it: the unroller + // tests the control word for equality with Unroll, so ORing the bit + // into a control that already carries something - DontUnroll above + // all - would neither unroll nor mean what it says. + if (mergeInst == nullptr || mergeInst->NumOperands() != 3 || + mergeInst->GetSingleWordOperand(2) != + static_cast(spv::LoopControlMask::MaskNone)) { + continue; + } + mergeInst->SetOperand( + 2, {static_cast(spv::LoopControlMask::Unroll)}); + modified = true; + } + } + + if (!modified) { + return Status::SuccessWithoutChange; + } + MGLOG_D("[spirv] storage-block array index: marked enclosing loops for full unrolling"); + return Status::SuccessWithChange; + } + + spvtools::opt::Pass::Status LegalizeStorageBlockArrayIndexPass::LowerToConstantSwitch() { + auto* irContext = context(); + + bool modified = false; + // Access chains this pass has already refused, so a shape it cannot rewrite + // exactly cannot spin the round loop. + std::unordered_set declined; + + for (int round = 0; round < kMaxLoweringRounds; ++round) { + const std::vector uses = CollectDynamicIndexUses(irContext); + bool progressed = false; + + for (const DynamicIndexUse& use : uses) { + if (declined.count(use.accessChain->result_id()) != 0) { + continue; + } + const LoweringOutcome outcome = LowerOneChain(use.accessChain, use.arrayLength); + if (outcome == LoweringOutcome::Declined) { + declined.insert(use.accessChain->result_id()); + continue; + } + if (outcome == LoweringOutcome::Changed) { + modified = true; + progressed = true; + // A store rewrite splits the block it sat in; every cached + // analysis (and the instruction list this loop is walking) is + // stale from here on. Recollect from scratch. + break; + } + } + + if (!progressed) { + break; + } + } + + if (!modified) { + return Status::SuccessWithoutChange; + } + return Status::SuccessWithChange; + } + + LegalizeStorageBlockArrayIndexPass::LoweringOutcome + LegalizeStorageBlockArrayIndexPass::LowerOneChain(Instruction* accessChain, uint32_t arrayLength) { + auto* irContext = context(); + if (arrayLength == 0 || arrayLength > kMaxLoweredArrayLength) { + MGLOG_D("[spirv] storage-block array index: array length %u is not lowerable", + arrayLength); + return LoweringOutcome::Declined; + } + if (!IsLowerableIndexType(irContext, accessChain->GetSingleWordInOperand(1))) { + return LoweringOutcome::Declined; + } + + std::vector stores; + std::vector loads; + bool unsupportedUse = false; + irContext->get_def_use_mgr()->ForEachUser(accessChain, [&](Instruction* user) { + switch (user->opcode()) { + case spv::Op::OpName: + case spv::Op::OpDecorate: + case spv::Op::OpDecorateId: + return; + case spv::Op::OpStore: + // Only as the pointer. A pointer stored as a *value* is not a storage + // block write and cannot be redirected element-wise. + if (user->GetSingleWordInOperand(0) == accessChain->result_id()) { + stores.push_back(user); + } else { + unsupportedUse = true; + } + return; + case spv::Op::OpLoad: + // Memory operands (Volatile, Aligned, ...) would be dropped by the + // per-element rebuild, so a load carrying any is refused instead. + if (user->NumInOperands() == 1) { + loads.push_back(user); + } else { + unsupportedUse = true; + } + return; + default: + // A pointer passed to a function, copied, chained further, used by an + // atomic, or measured by OpArrayLength cannot be resolved to one + // element here. + unsupportedUse = true; + return; + } + }); + + if (unsupportedUse) { + MGLOG_D("[spirv] storage-block array index: chain %%%u has a use this pass cannot " + "rewrite", + accessChain->result_id()); + return LoweringOutcome::Declined; + } + + if (!loads.empty()) { + return LowerLoad(accessChain, arrayLength, loads.front()); + } + if (!stores.empty()) { + return LowerStore(accessChain, arrayLength, stores.front()); + } + + // No uses left: the chain itself is what detection is still seeing. + irContext->KillInst(accessChain); + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return LoweringOutcome::Changed; + } + + // switch (idx) { case 0: arr[0]... = v; break; case 1: arr[1]... = v; break; ... } + // + // The block holding the store is split at the store, and the tail becomes the + // switch's merge block, so whatever followed the store still runs exactly once on + // every path. An index outside [0, length) reaches the default target, which is + // the merge block: nothing is stored, which is what indexing a block array out of + // range already meant. + LegalizeStorageBlockArrayIndexPass::LoweringOutcome + LegalizeStorageBlockArrayIndexPass::LowerStore(Instruction* accessChain, uint32_t arrayLength, + Instruction* store) { + auto* irContext = context(); + BasicBlock* block = irContext->get_instr_block(store); + if (block == nullptr) { + return LoweringOutcome::Declined; + } + // Splitting a loop header keeps the label - and so the back edge's target - + // on the first half while the OpLoopMerge moves to the second, which is not + // a loop any more. Refuse instead of producing that. + if (block->GetLoopMergeInst() != nullptr) { + MGLOG_D("[spirv] storage-block array index: store sits in a loop header, declining"); + return LoweringOutcome::Declined; + } + Function* function = block->GetParent(); + if (function == nullptr) { + return LoweringOutcome::Declined; + } + + const uint32_t indexId = accessChain->GetSingleWordInOperand(1); + const uint32_t valueId = store->GetSingleWordInOperand(1); + std::vector memoryOperands; + for (uint32_t i = 2; i < store->NumInOperands(); ++i) { + memoryOperands.push_back(store->GetInOperand(i)); + } + + const uint32_t mergeLabelId = irContext->TakeNextId(); + block->SplitBasicBlock(irContext, mergeLabelId, BasicBlock::iterator(store)); + // |store| now heads the merge block; the per-element stores replace it. + irContext->KillInst(store); + + std::vector> targets; + targets.reserve(arrayLength); + BasicBlock* insertAfter = block; + for (uint32_t element = 0; element < arrayLength; ++element) { + const uint32_t caseLabelId = irContext->TakeNextId(); + auto caseBlock = MakeUnique(MakeUnique( + irContext, spv::Op::OpLabel, 0, caseLabelId, std::initializer_list{})); + caseBlock->SetParent(function); + BasicBlock* casePtr = function->InsertBasicBlockAfter(std::move(caseBlock), insertAfter); + // The builders below register what they add, but this label was built by + // hand: without this the OpSwitch would name a target the def-use manager + // has never seen, which a consistency-checking build calls out. + irContext->AnalyzeDefUse(casePtr->GetLabelInst()); + irContext->set_instr_block(casePtr->GetLabelInst(), casePtr); + + InstructionBuilder caseBuilder( + irContext, casePtr, + IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); + const uint32_t constantId = ConstantLikeIndex(irContext, indexId, element); + Instruction* elementChain = + CloneChainWithConstantIndex(caseBuilder, irContext, accessChain, constantId); + + std::vector storeOperands; + storeOperands.push_back({SPV_OPERAND_TYPE_ID, {elementChain->result_id()}}); + storeOperands.push_back({SPV_OPERAND_TYPE_ID, {valueId}}); + for (const Operand& memoryOperand : memoryOperands) { + storeOperands.push_back(memoryOperand); + } + caseBuilder.AddInstruction( + MakeUnique(irContext, spv::Op::OpStore, 0, 0, storeOperands)); + caseBuilder.AddBranch(mergeLabelId); + + targets.push_back({Operand::OperandData{element}, caseLabelId}); + insertAfter = casePtr; + } + + InstructionBuilder switchBuilder( + irContext, block, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); + switchBuilder.AddSwitch(indexId, mergeLabelId, targets, mergeLabelId); + + if (irContext->get_def_use_mgr()->NumUsers(accessChain) == 0) { + irContext->KillInst(accessChain); + } + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + MGLOG_D("[spirv] storage-block array index: lowered a dynamic write to a %u-way switch", + arrayLength); + return LoweringOutcome::Changed; + } + + // A read needs no control flow: load every element through a constant index and + // pick with OpSelect. Reading the elements the shader did not ask for is safe - + // every one of them is a storage block this stage already declares, and an ES + // driver bounds-checks a storage buffer read that lands outside what is bound. + LegalizeStorageBlockArrayIndexPass::LoweringOutcome + LegalizeStorageBlockArrayIndexPass::LowerLoad(Instruction* accessChain, uint32_t arrayLength, + Instruction* load) { + auto* irContext = context(); + uint32_t conditionTypeId = 0; + uint32_t dimension = 0; + if (!TryGetSelectConditionType(irContext, load->type_id(), &conditionTypeId, &dimension)) { + MGLOG_D("[spirv] storage-block array index: element type is not selectable, declining"); + return LoweringOutcome::Declined; + } + const uint32_t boolTypeId = irContext->get_type_mgr()->GetBoolTypeId(); + const uint32_t indexId = accessChain->GetSingleWordInOperand(1); + + InstructionBuilder builder( + irContext, load, IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); + + uint32_t selectedId = 0; + for (uint32_t element = 0; element < arrayLength; ++element) { + const uint32_t constantId = ConstantLikeIndex(irContext, indexId, element); + Instruction* elementChain = + CloneChainWithConstantIndex(builder, irContext, accessChain, constantId); + Instruction* elementLoad = builder.AddLoad(load->type_id(), elementChain->result_id()); + if (element == 0) { + // Element 0 is the else-arm of the whole ladder, so an out-of-range + // index reads it - an undefined element for an undefined index. + selectedId = elementLoad->result_id(); + continue; + } + + Instruction* isElement = + builder.AddBinaryOp(boolTypeId, spv::Op::OpIEqual, indexId, constantId); + uint32_t conditionId = isElement->result_id(); + if (dimension > 1) { + std::vector components(dimension, conditionId); + conditionId = builder.AddCompositeConstruct(conditionTypeId, components)->result_id(); + } + selectedId = builder + .AddSelect(load->type_id(), conditionId, elementLoad->result_id(), + selectedId) + ->result_id(); + } + + irContext->ReplaceAllUsesWith(load->result_id(), selectedId); + irContext->KillInst(load); + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + MGLOG_D("[spirv] storage-block array index: lowered a dynamic read to %u constant-indexed " + "loads", + arrayLength); + return LoweringOutcome::Changed; + } + + spvtools::Optimizer::PassToken + LegalizeStorageBlockArrayIndexPass::CreateMarkLoopsForUnrollPass() { + return spvtools::Optimizer::PassToken( + MakeUnique(Mode::MarkLoopsForUnroll)); + } + + spvtools::Optimizer::PassToken + LegalizeStorageBlockArrayIndexPass::CreateLowerToConstantSwitchPass() { + return spvtools::Optimizer::PassToken( + MakeUnique(Mode::LowerToConstantSwitch)); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.h new file mode 100644 index 00000000..390acc15 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.h @@ -0,0 +1,120 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeStorageBlockArrayIndexPass.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 { + // GL 4.3 lets an ARRAY OF SHADER STORAGE BLOCKS be indexed with any + // dynamically-uniform expression (GL 4.6 core / GLSL 4.30 4.1.9). GLSL ES keeps + // the stricter ES 3.1 rule - the index must be a *constant integral expression* - + // and the Qualcomm ES compiler enforces it to the letter: + // + // '[' : indexing into an SSBO array using a non-constant expression is not + // permitted + // + // glslang keeps the whole array as ONE SPIR-V variable, so SPIRV-Cross prints + // `layout(binding = N, std430) buffer Blk { ... } arr[4];` plus `arr[i]` verbatim + // and the stage never compiles. The backend program then links nothing and every + // draw or dispatch that uses it is a silent no-op, which reads back as "the buffer + // was never written" rather than as an error - the frontend has already published + // GL_LINK_STATUS = TRUE from glslang's own link. + // + // Verified on the device: an Adreno 830 ES probe with no MobileGL in the loop + // rejects the non-constant subscript with AND without GL_EXT_gpu_shader5 (which + // the driver does advertise), and accepts a constant one. So the ES 3.2 + // "dynamically uniform" relaxation is not a way out - every index really has to + // become a compile-time constant. + // + // Two modes, used as two halves of one legalization in + // ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl - the same shape, for + // the same reasons, as LegalizeFragmentOutputIndexPass: + // + // MarkLoopsForUnroll - `for (int i = 0; i < 4; ++i) arr[i].x = ...` is the + // common shape, and full unrolling turns its index into a literal at no cost + // in emitted code. spirv-opt's CreateLoopUnrollPass only touches loops whose + // OpLoopMerge carries the Unroll control, so this mode sets that hint on + // exactly the loops that enclose an offending access chain, and only when + // their trip count is known and small. Must run AFTER ssa-rewrite: both the + // trip-count check and the unroller need the induction variable as an OpPhi. + // + // LowerToConstantSwitch - the fallback for a genuinely dynamic index + // (uniform-sourced, which is what the CTS indirect-addressing and resource-max + // cases use). A write through such a chain becomes an OpSwitch over the + // array's range with one constant-indexed store per case; a read becomes one + // constant-indexed load per element combined with OpSelect. This is what ANGLE + // does for the same ES 3.1 rule. + // + // Storage blocks only. A UNIFORM block array is a different namespace with its own + // (less strictly enforced) rule and no observed failure, so it is deliberately left + // alone rather than lowered on speculation. + // + // DirectGLES transpile path only: the original module is legal for Vulkan, which + // has no such restriction, and DirectVulkan must keep seeing the array as one + // descriptor array. + // + // The pass DECLINES - leaving the module untouched rather than half-transforming + // it - whenever it meets a shape it cannot rewrite exactly: a pointer handed to a + // function or chained further, an atomic or an OpArrayLength through the chain, a + // load carrying memory operands, a spec-constant array length, an index that is + // not a 32-bit integer, or a store sitting in a loop header block (splitting there + // would move the OpLoopMerge away from the back edge's target). + class LegalizeStorageBlockArrayIndexPass final : public spvtools::opt::Pass { + public: + enum class Mode { + MarkLoopsForUnroll, + LowerToConstantSwitch, + }; + + explicit LegalizeStorageBlockArrayIndexPass(Mode mode) : m_mode(mode) {} + + const char* name() const override { + return m_mode == Mode::MarkLoopsForUnroll + ? "mobilegl-mark-storage-block-array-index-loops" + : "mobilegl-lower-storage-block-array-index"; + } + + Status Process() override; + + static spvtools::Optimizer::PassToken CreateMarkLoopsForUnrollPass(); + static spvtools::Optimizer::PassToken CreateLowerToConstantSwitchPass(); + + // The detection half, on a serialized module: true when an array of storage + // blocks is indexed with anything but an OpConstant. Cheap enough to gate the + // whole legalization on (one BuildModule, no serialization) and used again + // after the folding chain to decide whether the fallback has to run at all. + static bool BinaryHasDynamicStorageBlockArrayIndexing(const std::vector& binary); + + private: + enum class LoweringOutcome { + // The shape is not one this pass can rewrite exactly; the module keeps + // the illegal chain rather than a half-transform of it. + Declined, + Changed, + }; + + Status MarkLoopsForUnroll(); + Status LowerToConstantSwitch(); + + LoweringOutcome LowerOneChain(spvtools::opt::Instruction* accessChain, uint32_t arrayLength); + LoweringOutcome LowerStore(spvtools::opt::Instruction* accessChain, uint32_t arrayLength, + spvtools::opt::Instruction* store); + LoweringOutcome LowerLoad(spvtools::opt::Instruction* accessChain, uint32_t arrayLength, + spvtools::opt::Instruction* load); + + Mode m_mode; + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL