From 4322427e78b50c7998fd8d321580baa56da2c9f9 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 19:56:45 -0400 Subject: [PATCH] [Fix] (DirectGLES): lower gl_ClipDistance for Adreno's ESSL compiler - shadow the builtin in a Private array with constant-index flushes before EmitVertex/return, loop-copy gl_in clip distances through dynamic indices (whole-array reads segfault the Qualcomm compiler, constant-index element reads miscompile), strip the SPIRV-Cross redeclaration Adreno rejects, and split const struct-array LUT initializers so they stay dynamically indexable; quirk-gated to Qualcomm with MOBILEGL_QUIRK_CLIP_DISTANCE override --- CMakeLists.txt | 2 + MobileGL/Config.h | 5 + MobileGL/ConfigLoader.cpp | 1 + MobileGL/MG_Backend/DirectGLES/Managers.cpp | 50 ++ MobileGL/MG_Backend/DirectGLES/Utils.cpp | 35 + MobileGL/MG_Backend/DirectGLES/Utils.h | 1 + .../ShaderTranspiler/ShaderCompiler.cpp | 27 + .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 14 + .../DefeatConstStructArrayLutPass.cpp | 175 +++++ .../DefeatConstStructArrayLutPass.h | 36 + .../LowerClipDistanceForEsslPass.cpp | 613 ++++++++++++++++++ .../LowerClipDistanceForEsslPass.h | 44 ++ 12 files changed, 1003 insertions(+) create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.h create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a2b72238..cdd03bc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,6 +195,8 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 7bd5b213..3a4514cd 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -80,6 +80,11 @@ namespace MobileGL::MG_Config { // rewrites the recognized workgroup prefix-scan template on Qualcomm devices with // subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry). QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto; + // MOBILEGL_QUIRK_CLIP_DISTANCE: overrides the DirectGLES quirk that lowers + // gl_ClipDistance for Adreno's ESSL compiler (shadow Private arrays with + // constant-index builtin flushes, dynamic-index gl_in copy loop, redeclaration + // strip, and const struct-array LUT splitting). Auto detects Qualcomm. + QuirkOverride ClipDistanceQuirk = QuirkOverride::Auto; // MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that // strips depth writes from accumulation-blended pipelines (MIN/MAX or additive // ONE+ONE - the multi-pass depth-equality signature) on drivers without diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 5b98d637..42a0584c 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -135,6 +135,7 @@ namespace MobileGL::MG_ConfigLoader { features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING"); features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS"); features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN"); + features.ClipDistanceQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_CLIP_DISTANCE"); features.MagmaDisableBlendedDepthWriteQuirk = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE"); features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS"); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index c238a01f..1ca6c1da 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -3291,6 +3291,19 @@ namespace MobileGL::MG_Backend::DirectGLES { } auto& shaderSpirvs = stateProgramObject->GetGeneratedSpirv(); + // Adreno's ESSL compiler mishandles gl_ClipDistance (rejects redeclarations, + // miscompiles non-constant-index writes and constant-index gl_in element reads, + // crashes on whole-array gl_in reads) and cannot dynamically index the global + // const struct[] LUTs SPIRV-Cross likes to emit. Gate the workarounds to + // Qualcomm; MOBILEGL_QUIRK_CLIP_DISTANCE overrides the device detection. + const MG_Config::QuirkOverride clipDistanceQuirkOverride = + MG_Config::Features.ClipDistanceQuirk; + const Bool applyClipDistanceQuirk = + clipDistanceQuirkOverride == MG_Config::QuirkOverride::ForceOn || + (clipDistanceQuirkOverride == MG_Config::QuirkOverride::Auto && + pActiveBackendObject && + pActiveBackendObject->GetDynamicParameters().GpuVendor == GpuVendorKind::Qualcomm); + for (int index = 0; index < attachedShaders.size(); ++index) { auto& shader = attachedShaders[index]; GLenum glShaderType = MG_Util::ConvertShaderStageToGLEnum(shader->GetShaderStage()); @@ -3339,6 +3352,37 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_W("FoldConstOffsetFor1DFetchForEssl failed, continuing with unfolded SPIR-V."); } + // Adreno quirk: shadow gl_ClipDistance in Private arrays so the transpiled + // ESSL only writes the builtin with literal constant indices (flushed before + // EmitVertex/return) and only reads gl_in clip distances through dynamic loop + // indices - the shapes this driver compiles correctly. Must run after the + // access-chain clamp above so the flush indices stay literal constants. + Vector clipDistanceSpirv; + if (applyClipDistanceQuirk && + (glShaderType == GL_VERTEX_SHADER || glShaderType == GL_GEOMETRY_SHADER)) { + if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerClipDistanceForEssl( + *effectiveSpirv, clipDistanceSpirv) && + !clipDistanceSpirv.empty()) { + effectiveSpirv = &clipDistanceSpirv; + } else { + MGLOG_W("LowerClipDistanceForEssl failed, continuing with unlowered SPIR-V."); + } + } + + // Adreno quirk: split single constant-composite stores of struct arrays so + // SPIRV-Cross does not promote them to global const struct[] LUTs, which this + // driver cannot dynamically index ("Cannot offset into the structure"). + Vector structLutSpirv; + if (applyClipDistanceQuirk) { + if (MG_Util::ShaderTranspiler::ShaderCompiler::DefeatConstStructArrayLutForEssl( + *effectiveSpirv, structLutSpirv) && + !structLutSpirv.empty()) { + effectiveSpirv = &structLutSpirv; + } else { + MGLOG_W("DefeatConstStructArrayLutForEssl failed, continuing with unsplit SPIR-V."); + } + } + // 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 @@ -3397,6 +3441,12 @@ namespace MobileGL::MG_Backend::DirectGLES { source = RebindImageUniformsToFrontendUnits(std::move(source), stateProgramObject); source = RemoveLayoutBinding(source); + if (applyClipDistanceQuirk) { + // Adreno rejects the gl_ClipDistance redeclaration SPIRV-Cross still emits + // ("reserved built-in name") but accepts plain usage with + // GL_EXT_clip_cull_distance required; drop the line, keep the #extension. + source = RemoveClipDistanceRedeclaration(source); + } source = ProcessOutColorLocations(source); source = ForceFlatIntegerVaryings(source, glShaderType); source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType); diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 0a3b7e1c..b6f8ea72 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -342,6 +342,41 @@ namespace MobileGL::MG_Backend::DirectGLES { } return result; } + + String RemoveClipDistanceRedeclaration(const String& glslCode) { +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // Adreno rejects any redeclaration of gl_ClipDistance/gl_CullDistance ("reserved + // built-in name") even with GL_EXT_clip_cull_distance required, but accepts plain + // usage of the builtin. Drop the desktop-style redeclaration line SPIRV-Cross + // prints; the "#extension GL_EXT_clip_cull_distance : require" line stays. + static const std::regex redeclarationRegex( + R"(^\s*(?:out|in)\s+(?:(?:high|medium|low)p\s+)?float\s+gl_(?:Clip|Cull)Distance\[[0-9]+\];\s*$)"); + + String result; + result.reserve(glslCode.size()); + SizeT lineStart = 0; + Bool firstLine = true; + while (lineStart <= glslCode.size()) { + SizeT lineEnd = glslCode.find('\n', lineStart); + const Bool lastLine = lineEnd == String::npos; + String line = glslCode.substr(lineStart, lastLine ? String::npos : lineEnd - lineStart); + + if (!std::regex_match(line, redeclarationRegex)) { + if (!firstLine) { + result += '\n'; + } + result += line; + firstLine = false; + } + if (lastLine) { + break; + } + lineStart = lineEnd + 1; + } + return result; + } } // namespace PrgramImpl namespace Utils { diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index f9d20c93..1c6e8b1e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -105,6 +105,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint32 unormOutputMask); String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType); String RemoveLayoutBinding(const String& glslCode); + String RemoveClipDistanceRedeclaration(const String& glslCode); } // namespace PrgramImpl namespace Utils { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index fc6aae79..d17554cf 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -23,6 +23,8 @@ #include "SpirvPasses/StripNoPerspectivePass.h" #include "SpirvPasses/EmulateNoPerspectivePass.h" #include "SpirvPasses/FoldConstOffsetFor1DFetchPass.h" +#include "SpirvPasses/LowerClipDistanceForEsslPass.h" +#include "SpirvPasses/DefeatConstStructArrayLutPass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" @@ -348,6 +350,31 @@ namespace MobileGL { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } + bool ShaderCompiler::LowerClipDistanceForEssl(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); + + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass()); + + return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); + } + + bool ShaderCompiler::DefeatConstStructArrayLutForEssl(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); + + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass( + DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass()); + + return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); + } + bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(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 2f3ef457..a0b290dc 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -43,6 +43,20 @@ namespace MobileGL { // transpile path. static bool FoldConstOffsetFor1DFetchForEssl(const Vector& inputBinary, Vector& outputBinary); + // Shadows gl_ClipDistance in Private mg_ClipDistance/mg_ClipDistanceIn arrays so + // the decompiled ESSL only writes the builtin with literal constant indices + // (flush before EmitVertex/return) and only reads gl_in clip distances with + // dynamic loop indices (copy loop): the other shapes miscompile or crash + // Adreno's ESSL compiler. Vertex/geometry stages; DirectGLES transpile path on + // Qualcomm only (quirk-gated). See LowerClipDistanceForEsslPass. + static bool LowerClipDistanceForEssl(const Vector& inputBinary, + Vector& outputBinary); + // Splits a Function-storage array-of-structs variable's single constant-composite + // store into per-element stores so SPIRV-Cross does not hoist it into a global + // const struct[] LUT, which Adreno cannot dynamically index. DirectGLES + // transpile path on Qualcomm only (quirk-gated). See DefeatConstStructArrayLutPass. + static bool DefeatConstStructArrayLutForEssl(const Vector& inputBinary, + Vector& outputBinary); // 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/DefeatConstStructArrayLutPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp new file mode 100644 index 00000000..ffaae9af --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.cpp @@ -0,0 +1,175 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.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 "DefeatConstStructArrayLutPass.h" + +#include "spirv.hpp" +#include "source/opt/constants.h" +#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/type_manager.h" +#include "source/opt/types.h" +#include "source/util/make_unique.h" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::BasicBlock; + using spvtools::opt::Function; + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + using spvtools::opt::Operand; + namespace analysis = spvtools::opt::analysis; + + uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) { + analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId); + analysis::Pointer ptr(pointee, sc); + return ctx->get_type_mgr()->GetTypeInstruction(&ptr); + } + + uint32_t SignedIntConstant(IRContext* ctx, uint32_t value) { + analysis::Integer i(32, true); + analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i); + const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value}); + return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id(); + } + + // True when |var| (a Function-storage OpVariable) points to an array of structs. + // Reports the struct type id on success. + bool IsArrayOfStructsVariable(IRContext* ctx, Instruction* var, uint32_t& structTypeId) { + auto* defUse = ctx->get_def_use_mgr(); + Instruction* ptrType = defUse->GetDef(var->type_id()); + if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return false; + Instruction* pointee = defUse->GetDef(ptrType->GetSingleWordInOperand(1)); + if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeArray) return false; + Instruction* element = defUse->GetDef(pointee->GetSingleWordInOperand(0)); + if (element == nullptr || element->opcode() != spv::Op::OpTypeStruct) return false; + structTypeId = element->result_id(); + return true; + } + } // namespace + + spvtools::opt::Pass::Status DefeatConstStructArrayLutPass::Process() { + auto* ctx = context(); + auto* defUse = ctx->get_def_use_mgr(); + bool modified = false; + + for (Function& function : *get_module()) { + if (function.begin() == function.end()) continue; + BasicBlock* entryBlock = &*function.begin(); + + // Candidate variables: Function-storage arrays of structs declared in this + // function's entry block (where OpVariables must live). + struct Candidate { + Instruction* var; + uint32_t structTypeId; + }; + std::vector candidates; + for (Instruction& inst : *entryBlock) { + if (inst.opcode() != spv::Op::OpVariable) break; + // Variables with initializers keep SPIRV-Cross's initializer path; the + // glslang pattern under attack is initializer-free with one OpStore. + if (inst.NumInOperands() > 1) continue; + uint32_t structTypeId = 0; + if (IsArrayOfStructsVariable(ctx, &inst, structTypeId)) { + candidates.push_back({&inst, structTypeId}); + } + } + + for (const Candidate& candidate : candidates) { + Instruction* var = candidate.var; + + // The variable qualifies only when its single write is one direct + // OpStore of an OpConstantComposite; any other write shape already + // defeats SPIRV-Cross's LUT promotion, so it is left untouched. + Instruction* singleStore = nullptr; + bool disqualified = false; + defUse->ForEachUser(var, [&](Instruction* user) { + if (user->opcode() == spv::Op::OpStore && + user->GetSingleWordInOperand(0) == var->result_id()) { + if (singleStore != nullptr) { + disqualified = true; + } else { + singleStore = user; + } + } else if (user->opcode() == spv::Op::OpCopyMemory) { + disqualified = true; + } else if (user->opcode() == spv::Op::OpAccessChain || + user->opcode() == spv::Op::OpInBoundsAccessChain) { + defUse->ForEachUser(user, [&](Instruction* chainUser) { + if (chainUser->opcode() == spv::Op::OpStore || + chainUser->opcode() == spv::Op::OpCopyMemory) { + disqualified = true; + } + }); + } + }); + if (disqualified || singleStore == nullptr) continue; + + Instruction* composite = defUse->GetDef(singleStore->GetSingleWordInOperand(1)); + if (composite == nullptr || + composite->opcode() != spv::Op::OpConstantComposite) { + continue; + } + + // The store must sit in the entry block: that is the only placement + // SPIRV-Cross treats as a LUT initializer. + bool storeInEntryBlock = false; + for (Instruction& inst : *entryBlock) { + if (&inst == singleStore) { + storeInEntryBlock = true; + break; + } + } + if (!storeInEntryBlock) continue; + + // Split the composite store into one constant-index store per element. + const uint32_t ptrFnStruct = + PointerTypeTo(ctx, candidate.structTypeId, spv::StorageClass::Function); + for (uint32_t element = 0; element < composite->NumInOperands(); ++element) { + const uint32_t elementConstId = composite->GetSingleWordInOperand(element); + const uint32_t chainId = ctx->TakeNextId(); + Instruction* chain = + singleStore->InsertBefore(spvtools::MakeUnique( + ctx, spv::Op::OpAccessChain, ptrFnStruct, chainId, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {var->result_id()}}, + {SPV_OPERAND_TYPE_ID, {SignedIntConstant(ctx, element)}}})); + ctx->AnalyzeDefUse(chain); + Instruction* store = + singleStore->InsertBefore(spvtools::MakeUnique( + ctx, spv::Op::OpStore, 0, 0, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {chainId}}, + {SPV_OPERAND_TYPE_ID, {elementConstId}}})); + ctx->AnalyzeDefUse(store); + } + ctx->KillInst(singleStore); + modified = true; + } + } + + if (!modified) { + return Status::SuccessWithoutChange; + } + ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken + DefeatConstStructArrayLutPass::CreateDefeatConstStructArrayLutPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.h new file mode 100644 index 00000000..4688fc2f --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.h @@ -0,0 +1,36 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DefeatConstStructArrayLutPass.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 + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // SPIRV-Cross hoists a Function-storage array variable whose only write is a single + // constant-composite store into a global `const struct[]` LUT (variable_is_lut). + // Adreno's ESSL compiler cannot dynamically index such a global const struct array + // ("Cannot offset into the structure" - device-verified on Adreno 750). Splitting + // the one composite store into per-element constant-index stores makes + // variable_is_lut fail, so SPIRV-Cross keeps the array as an ordinary local that + // Adreno indexes fine. Scalar/vector const arrays are unaffected on Adreno and are + // left alone - only arrays OF STRUCTS are rewritten. Only meant for the DirectGLES + // transpile path on Qualcomm devices. + class DefeatConstStructArrayLutPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "defeat-const-struct-array-lut"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateDefeatConstStructArrayLutPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp new file mode 100644 index 00000000..ba1a7eb0 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.cpp @@ -0,0 +1,613 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.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 "LowerClipDistanceForEsslPass.h" + +#include "spirv.hpp" +#include "source/opt/basic_block.h" +#include "source/opt/constants.h" +#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/type_manager.h" +#include "source/opt/types.h" +#include "source/util/make_unique.h" + +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::BasicBlock; + using spvtools::opt::Function; + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + using spvtools::opt::Operand; + namespace analysis = spvtools::opt::analysis; + + spv::ExecutionModel EntryExecutionModel(IRContext* ctx) { + for (Instruction& ep : ctx->module()->entry_points()) { + return static_cast(ep.GetSingleWordInOperand(0)); + } + return spv::ExecutionModel::Max; + } + + uint32_t EntryFunctionId(IRContext* ctx) { + for (Instruction& ep : ctx->module()->entry_points()) { + // OpEntryPoint "name" + return ep.GetSingleWordInOperand(1); + } + return 0; + } + + uint32_t VariablePointeeType(IRContext* ctx, Instruction* var) { + Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(var->type_id()); + // OpTypePointer + return ptrType->GetSingleWordInOperand(1); + } + + uint32_t PointerTypeTo(IRContext* ctx, uint32_t pointeeId, spv::StorageClass sc) { + analysis::Type* pointee = ctx->get_type_mgr()->GetType(pointeeId); + analysis::Pointer ptr(pointee, sc); + return ctx->get_type_mgr()->GetTypeInstruction(&ptr); + } + + uint32_t IntConstant(IRContext* ctx, bool isSigned, uint32_t value) { + analysis::Integer i(32, isSigned); + analysis::Type* reg = ctx->get_type_mgr()->GetRegisteredType(&i); + const analysis::Constant* c = ctx->get_constant_mgr()->GetConstant(reg, {value}); + return ctx->get_constant_mgr()->GetDefiningInstruction(c)->result_id(); + } + + uint32_t UintType(IRContext* ctx) { + analysis::Integer i(32, false); + return ctx->get_type_mgr()->GetTypeInstruction(&i); + } + + uint32_t BoolType(IRContext* ctx) { + analysis::Bool b; + return ctx->get_type_mgr()->GetTypeInstruction(&b); + } + + // Constant length of OpTypeArray |arrayTypeId| (0 when not a sized constant). + uint32_t ArrayLength(IRContext* ctx, uint32_t arrayTypeId) { + Instruction* arrayType = ctx->get_def_use_mgr()->GetDef(arrayTypeId); + if (arrayType == nullptr || arrayType->opcode() != spv::Op::OpTypeArray) { + return 0; + } + Instruction* length = ctx->get_def_use_mgr()->GetDef(arrayType->GetSingleWordInOperand(1)); + if (length == nullptr || length->opcode() != spv::Op::OpConstant) { + return 0; + } + return length->GetSingleWordInOperand(0); + } + + bool IsConstantWithValue(IRContext* ctx, uint32_t id, uint32_t value) { + Instruction* def = ctx->get_def_use_mgr()->GetDef(id); + return def != nullptr && def->opcode() == spv::Op::OpConstant && + def->GetSingleWordInOperand(0) == value; + } + + bool IsAccessChain(const Instruction* inst) { + return inst->opcode() == spv::Op::OpAccessChain || + inst->opcode() == spv::Op::OpInBoundsAccessChain; + } + + Instruction* AddPrivateVariable(IRContext* ctx, uint32_t pointeeTypeId, const char* name) { + const uint32_t ptrType = PointerTypeTo(ctx, pointeeTypeId, spv::StorageClass::Private); + const uint32_t varId = ctx->TakeNextId(); + ctx->AddGlobalValue(spvtools::MakeUnique( + ctx, spv::Op::OpVariable, ptrType, varId, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, + {static_cast(spv::StorageClass::Private)}}})); + ctx->AddDebug2Inst(spvtools::MakeUnique( + ctx, spv::Op::OpName, 0, 0, + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {varId}}, + {SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)}})); + return ctx->get_def_use_mgr()->GetDef(varId); + } + + // Retargets |chain| onto |newBaseId|, dropping the first |dropIndexCount| index + // operands and switching the result pointer's storage class to Private. + void RetargetChainToPrivate(IRContext* ctx, Instruction* chain, uint32_t newBaseId, + uint32_t dropIndexCount) { + Instruction* chainPtrType = ctx->get_def_use_mgr()->GetDef(chain->type_id()); + const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1); + const uint32_t newPtrType = PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private); + + ctx->ForgetUses(chain); + std::vector newOperands; + newOperands.push_back({SPV_OPERAND_TYPE_ID, {newBaseId}}); + for (uint32_t i = 1 + dropIndexCount; i < chain->NumInOperands(); ++i) { + newOperands.push_back(chain->GetInOperand(i)); + } + chain->SetResultType(newPtrType); + chain->SetInOperands(std::move(newOperands)); + ctx->AnalyzeUses(chain); + } + + // ---- Output side -------------------------------------------------------------- + + struct OutputTarget { + Instruction* var = nullptr; // Output gl_PerVertex block or standalone builtin + bool isBlockMember = false; + uint32_t memberIndex = 0; // valid when isBlockMember + uint32_t arrayTypeId = 0; // float[N] + uint32_t elemTypeId = 0; // float + uint32_t arrayLen = 0; // N + }; + + // Inserts "gl_ClipDistance[k] = mg_ClipDistance[k]" for every literal k before + // |before|. Constant-index writes are the only write shape Adreno links correctly. + void InsertFlushBefore(IRContext* ctx, Instruction* before, const OutputTarget& target, + uint32_t mgVarId) { + const uint32_t ptrPrivElem = + PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Private); + const uint32_t ptrOutElem = + PointerTypeTo(ctx, target.elemTypeId, spv::StorageClass::Output); + for (uint32_t k = 0; k < target.arrayLen; ++k) { + const uint32_t kConst = IntConstant(ctx, true, k); + const uint32_t srcChainId = ctx->TakeNextId(); + before->InsertBefore(spvtools::MakeUnique( + ctx, spv::Op::OpAccessChain, ptrPrivElem, srcChainId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {mgVarId}}, + {SPV_OPERAND_TYPE_ID, {kConst}}})); + const uint32_t valId = ctx->TakeNextId(); + before->InsertBefore(spvtools::MakeUnique( + ctx, spv::Op::OpLoad, target.elemTypeId, valId, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {srcChainId}}})); + const uint32_t dstChainId = ctx->TakeNextId(); + std::vector dstOperands; + dstOperands.push_back({SPV_OPERAND_TYPE_ID, {target.var->result_id()}}); + if (target.isBlockMember) { + dstOperands.push_back( + {SPV_OPERAND_TYPE_ID, {IntConstant(ctx, true, target.memberIndex)}}); + } + dstOperands.push_back({SPV_OPERAND_TYPE_ID, {kConst}}); + before->InsertBefore(spvtools::MakeUnique( + ctx, spv::Op::OpAccessChain, ptrOutElem, dstChainId, dstOperands)); + before->InsertBefore(spvtools::MakeUnique( + ctx, spv::Op::OpStore, 0, 0, + std::initializer_list{{SPV_OPERAND_TYPE_ID, {dstChainId}}, + {SPV_OPERAND_TYPE_ID, {valId}}})); + } + } + + bool LowerOutputClipDistance(IRContext* ctx, bool isGeometry) { + auto* defUse = ctx->get_def_use_mgr(); + + // Collect (struct type, member) pairs decorated BuiltIn ClipDistance and + // standalone variables decorated BuiltIn ClipDistance. + std::vector> memberTargets; // (structId, member) + std::vector plainTargets; // variable ids + for (Instruction& ann : ctx->annotations()) { + if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 && + static_cast(ann.GetSingleWordInOperand(2)) == + spv::Decoration::BuiltIn && + static_cast(ann.GetSingleWordInOperand(3)) == + spv::BuiltIn::ClipDistance) { + memberTargets.emplace_back(ann.GetSingleWordInOperand(0), + ann.GetSingleWordInOperand(1)); + } else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 && + static_cast(ann.GetSingleWordInOperand(1)) == + spv::Decoration::BuiltIn && + static_cast(ann.GetSingleWordInOperand(2)) == + spv::BuiltIn::ClipDistance) { + plainTargets.push_back(ann.GetSingleWordInOperand(0)); + } + } + + std::vector targets; + for (Instruction& inst : ctx->module()->types_values()) { + if (inst.opcode() != spv::Op::OpVariable || + static_cast(inst.GetSingleWordInOperand(0)) != + spv::StorageClass::Output) { + continue; + } + const uint32_t pointee = VariablePointeeType(ctx, &inst); + for (const auto& [structId, member] : memberTargets) { + if (pointee != structId) continue; + Instruction* structType = defUse->GetDef(structId); + if (structType == nullptr || member >= structType->NumInOperands()) continue; + OutputTarget target; + target.var = &inst; + target.isBlockMember = true; + target.memberIndex = member; + target.arrayTypeId = structType->GetSingleWordInOperand(member); + target.arrayLen = ArrayLength(ctx, target.arrayTypeId); + targets.push_back(target); + } + for (const uint32_t varId : plainTargets) { + if (inst.result_id() != varId) continue; + OutputTarget target; + target.var = &inst; + target.isBlockMember = false; + target.arrayTypeId = pointee; + target.arrayLen = ArrayLength(ctx, target.arrayTypeId); + targets.push_back(target); + } + } + + bool changed = false; + for (OutputTarget& target : targets) { + if (target.arrayLen == 0) continue; + Instruction* arrayType = defUse->GetDef(target.arrayTypeId); + target.elemTypeId = arrayType->GetSingleWordInOperand(0); + + // Collect the accesses to redirect. For the block form only chains whose + // leading index selects the ClipDistance member count; for the standalone + // form every chain plus whole-variable loads/stores. + std::vector chains; + std::vector directAccesses; + bool unsupportedUse = false; + defUse->ForEachUser(target.var, [&](Instruction* user) { + if (IsAccessChain(user) && + user->GetSingleWordInOperand(0) == target.var->result_id()) { + if (target.isBlockMember) { + if (user->NumInOperands() >= 2 && + IsConstantWithValue(ctx, user->GetSingleWordInOperand(1), + target.memberIndex)) { + chains.push_back(user); + } + } else { + chains.push_back(user); + } + } else if (!target.isBlockMember) { + if (user->opcode() == spv::Op::OpLoad || + (user->opcode() == spv::Op::OpStore && + user->GetSingleWordInOperand(0) == target.var->result_id())) { + directAccesses.push_back(user); + } else if (user->opcode() == spv::Op::OpCopyMemory) { + unsupportedUse = true; + } + } + }); + if (unsupportedUse || (chains.empty() && directAccesses.empty())) { + continue; + } + + Instruction* mgVar = AddPrivateVariable(ctx, target.arrayTypeId, "mg_ClipDistance"); + const uint32_t mgVarId = mgVar->result_id(); + + for (Instruction* chain : chains) { + const uint32_t dropCount = target.isBlockMember ? 1u : 0u; + if (chain->NumInOperands() == 1 + dropCount) { + // Pointer to the whole float[N]: reuse the private variable itself. + ctx->ReplaceAllUsesWith(chain->result_id(), mgVarId); + ctx->KillInst(chain); + } else { + RetargetChainToPrivate(ctx, chain, mgVarId, dropCount); + } + } + for (Instruction* access : directAccesses) { + ctx->ForgetUses(access); + access->SetInOperand(0, {mgVarId}); + ctx->AnalyzeUses(access); + } + + // Flush the shadow into the real builtin: geometry right before every + // EmitVertex, vertex before every return of the entry point. The flush is + // also what keeps the builtin statically used for cross-stage IO matching. + std::vector flushSites; + if (isGeometry) { + for (Function& function : *ctx->module()) { + function.ForEachInst([&](Instruction* inst) { + if (inst->opcode() == spv::Op::OpEmitVertex) { + flushSites.push_back(inst); + } + }); + } + } else { + const uint32_t entryFuncId = EntryFunctionId(ctx); + for (Function& function : *ctx->module()) { + if (function.result_id() != entryFuncId) continue; + function.ForEachInst([&](Instruction* inst) { + if (inst->opcode() == spv::Op::OpReturn || + inst->opcode() == spv::Op::OpReturnValue) { + flushSites.push_back(inst); + } + }); + } + } + for (Instruction* site : flushSites) { + InsertFlushBefore(ctx, site, target, mgVarId); + } + + changed = true; + } + return changed; + } + + // ---- Input side (geometry gl_in) ---------------------------------------------- + + bool LowerInputClipDistance(IRContext* ctx) { + auto* defUse = ctx->get_def_use_mgr(); + auto* typeMgr = ctx->get_type_mgr(); + + // Locate the gl_in block member decorated ClipDistance. + Instruction* glInVar = nullptr; + uint32_t memberIndex = 0; + uint32_t arrayTypeId = 0; // float[N] + for (Instruction& ann : ctx->annotations()) { + if (ann.opcode() != spv::Op::OpMemberDecorate || ann.NumInOperands() < 4 || + static_cast(ann.GetSingleWordInOperand(2)) != + spv::Decoration::BuiltIn || + static_cast(ann.GetSingleWordInOperand(3)) != + spv::BuiltIn::ClipDistance) { + continue; + } + const uint32_t structId = ann.GetSingleWordInOperand(0); + const uint32_t member = ann.GetSingleWordInOperand(1); + for (Instruction& inst : ctx->module()->types_values()) { + if (inst.opcode() != spv::Op::OpVariable || + static_cast(inst.GetSingleWordInOperand(0)) != + spv::StorageClass::Input) { + continue; + } + const uint32_t pointee = VariablePointeeType(ctx, &inst); + Instruction* pointeeType = defUse->GetDef(pointee); + if (pointeeType == nullptr || pointeeType->opcode() != spv::Op::OpTypeArray || + pointeeType->GetSingleWordInOperand(0) != structId) { + continue; + } + Instruction* structType = defUse->GetDef(structId); + if (structType == nullptr || member >= structType->NumInOperands()) continue; + glInVar = &inst; + memberIndex = member; + arrayTypeId = structType->GetSingleWordInOperand(member); + break; + } + if (glInVar != nullptr) break; + } + if (glInVar == nullptr) { + return false; + } + + const uint32_t clipCount = ArrayLength(ctx, arrayTypeId); + const uint32_t vertexCount = ArrayLength(ctx, VariablePointeeType(ctx, glInVar)); + if (clipCount == 0 || vertexCount == 0) { + return false; + } + + // Every gl_in chain that selects the ClipDistance member: + // (vertex, member) yields a whole float[N], (vertex, member, k) an element. + std::vector chains; + defUse->ForEachUser(glInVar, [&](Instruction* user) { + if (IsAccessChain(user) && user->GetSingleWordInOperand(0) == glInVar->result_id() && + user->NumInOperands() >= 3 && + IsConstantWithValue(ctx, user->GetSingleWordInOperand(2), memberIndex)) { + chains.push_back(user); + } + }); + if (chains.empty()) { + return false; + } + + Instruction* arrayTypeInst = defUse->GetDef(arrayTypeId); + const uint32_t elemTypeId = arrayTypeInst->GetSingleWordInOperand(0); + + // Private mg_ClipDistanceIn = float[vertexCount][clipCount]. + const uint32_t vertexCountConst = IntConstant(ctx, false, vertexCount); + analysis::Type* innerType = typeMgr->GetType(arrayTypeId); + analysis::Array outerArray( + innerType, analysis::Array::LengthInfo{ + vertexCountConst, + {analysis::Array::LengthInfo::kConstant, vertexCount}}); + const uint32_t outerArrayTypeId = typeMgr->GetTypeInstruction(&outerArray); + Instruction* mgInVar = AddPrivateVariable(ctx, outerArrayTypeId, "mg_ClipDistanceIn"); + const uint32_t mgInVarId = mgInVar->result_id(); + + // Copy loop at the top of the entry point: + // for (uint t = 0; t < vertexCount * clipCount; ++t) + // mg_ClipDistanceIn[t / clipCount][t % clipCount] = + // gl_in[t / clipCount].gl_ClipDistance[t % clipCount]; + // Both gl_in indices are loop-derived (dynamic): constant-index element reads + // miscompile and whole-array reads crash the Adreno compiler. + const uint32_t entryFuncId = EntryFunctionId(ctx); + Function* entryFn = nullptr; + for (Function& function : *ctx->module()) { + if (function.result_id() == entryFuncId) { + entryFn = &function; + break; + } + } + if (entryFn == nullptr || entryFn->begin() == entryFn->end()) { + return false; + } + + const uint32_t uintTypeId = UintType(ctx); + const uint32_t boolTypeId = BoolType(ctx); + const uint32_t ptrFnUint = PointerTypeTo(ctx, uintTypeId, spv::StorageClass::Function); + const uint32_t ptrInElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Input); + const uint32_t ptrPrivElem = PointerTypeTo(ctx, elemTypeId, spv::StorageClass::Private); + const uint32_t uint0 = IntConstant(ctx, false, 0); + const uint32_t uint1 = IntConstant(ctx, false, 1); + const uint32_t uintN = IntConstant(ctx, false, clipCount); + const uint32_t uintTotal = IntConstant(ctx, false, vertexCount * clipCount); + const uint32_t memberConst = IntConstant(ctx, true, memberIndex); + + BasicBlock* entryBlock = &*entryFn->begin(); + auto splitPoint = entryBlock->begin(); + while (splitPoint != entryBlock->end() && + splitPoint->opcode() == spv::Op::OpVariable) { + ++splitPoint; + } + + // Loop counter lives with the other function-local variables. + const uint32_t counterVarId = ctx->TakeNextId(); + splitPoint->InsertBefore(spvtools::MakeUnique( + ctx, spv::Op::OpVariable, ptrFnUint, counterVarId, + std::initializer_list{ + {SPV_OPERAND_TYPE_STORAGE_CLASS, + {static_cast(spv::StorageClass::Function)}}})); + + const uint32_t restLabelId = ctx->TakeNextId(); + BasicBlock* restBlock = entryBlock->SplitBasicBlock(ctx, restLabelId, splitPoint); + + const uint32_t headerLabelId = ctx->TakeNextId(); + const uint32_t checkLabelId = ctx->TakeNextId(); + const uint32_t bodyLabelId = ctx->TakeNextId(); + const uint32_t continueLabelId = ctx->TakeNextId(); + + auto makeBlock = [&](uint32_t labelId) { + return spvtools::MakeUnique(spvtools::MakeUnique( + ctx, spv::Op::OpLabel, 0, labelId, std::initializer_list{})); + }; + auto addInst = [&](BasicBlock* block, spv::Op opcode, uint32_t typeId, + uint32_t resultId, std::vector operands) { + block->AddInstruction(spvtools::MakeUnique( + ctx, opcode, typeId, resultId, std::move(operands))); + }; + + // entry: t = 0; branch header + addInst(entryBlock, spv::Op::OpStore, 0, 0, + {{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {uint0}}}); + addInst(entryBlock, spv::Op::OpBranch, 0, 0, {{SPV_OPERAND_TYPE_ID, {headerLabelId}}}); + + // header: structured loop header + auto headerBlock = makeBlock(headerLabelId); + addInst(headerBlock.get(), spv::Op::OpLoopMerge, 0, 0, + {{SPV_OPERAND_TYPE_ID, {restLabelId}}, + {SPV_OPERAND_TYPE_ID, {continueLabelId}}, + {SPV_OPERAND_TYPE_LOOP_CONTROL, + {static_cast(spv::LoopControlMask::MaskNone)}}}); + addInst(headerBlock.get(), spv::Op::OpBranch, 0, 0, + {{SPV_OPERAND_TYPE_ID, {checkLabelId}}}); + + // check: t < vertexCount * clipCount ? + auto checkBlock = makeBlock(checkLabelId); + const uint32_t tCheckId = ctx->TakeNextId(); + addInst(checkBlock.get(), spv::Op::OpLoad, uintTypeId, tCheckId, + {{SPV_OPERAND_TYPE_ID, {counterVarId}}}); + const uint32_t condId = ctx->TakeNextId(); + addInst(checkBlock.get(), spv::Op::OpULessThan, boolTypeId, condId, + {{SPV_OPERAND_TYPE_ID, {tCheckId}}, {SPV_OPERAND_TYPE_ID, {uintTotal}}}); + addInst(checkBlock.get(), spv::Op::OpBranchConditional, 0, 0, + {{SPV_OPERAND_TYPE_ID, {condId}}, + {SPV_OPERAND_TYPE_ID, {bodyLabelId}}, + {SPV_OPERAND_TYPE_ID, {restLabelId}}}); + + // body: mg_ClipDistanceIn[t / N][t % N] = gl_in[t / N].gl_ClipDistance[t % N] + auto bodyBlock = makeBlock(bodyLabelId); + const uint32_t tBodyId = ctx->TakeNextId(); + addInst(bodyBlock.get(), spv::Op::OpLoad, uintTypeId, tBodyId, + {{SPV_OPERAND_TYPE_ID, {counterVarId}}}); + const uint32_t vertexIdxId = ctx->TakeNextId(); + addInst(bodyBlock.get(), spv::Op::OpUDiv, uintTypeId, vertexIdxId, + {{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}}); + const uint32_t clipIdxId = ctx->TakeNextId(); + addInst(bodyBlock.get(), spv::Op::OpUMod, uintTypeId, clipIdxId, + {{SPV_OPERAND_TYPE_ID, {tBodyId}}, {SPV_OPERAND_TYPE_ID, {uintN}}}); + const uint32_t srcChainId = ctx->TakeNextId(); + addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrInElem, srcChainId, + {{SPV_OPERAND_TYPE_ID, {glInVar->result_id()}}, + {SPV_OPERAND_TYPE_ID, {vertexIdxId}}, + {SPV_OPERAND_TYPE_ID, {memberConst}}, + {SPV_OPERAND_TYPE_ID, {clipIdxId}}}); + const uint32_t valId = ctx->TakeNextId(); + addInst(bodyBlock.get(), spv::Op::OpLoad, elemTypeId, valId, + {{SPV_OPERAND_TYPE_ID, {srcChainId}}}); + const uint32_t dstChainId = ctx->TakeNextId(); + addInst(bodyBlock.get(), spv::Op::OpAccessChain, ptrPrivElem, dstChainId, + {{SPV_OPERAND_TYPE_ID, {mgInVarId}}, + {SPV_OPERAND_TYPE_ID, {vertexIdxId}}, + {SPV_OPERAND_TYPE_ID, {clipIdxId}}}); + addInst(bodyBlock.get(), spv::Op::OpStore, 0, 0, + {{SPV_OPERAND_TYPE_ID, {dstChainId}}, {SPV_OPERAND_TYPE_ID, {valId}}}); + addInst(bodyBlock.get(), spv::Op::OpBranch, 0, 0, + {{SPV_OPERAND_TYPE_ID, {continueLabelId}}}); + + // continue: ++t + auto continueBlock = makeBlock(continueLabelId); + const uint32_t tContinueId = ctx->TakeNextId(); + addInst(continueBlock.get(), spv::Op::OpLoad, uintTypeId, tContinueId, + {{SPV_OPERAND_TYPE_ID, {counterVarId}}}); + const uint32_t tIncId = ctx->TakeNextId(); + addInst(continueBlock.get(), spv::Op::OpIAdd, uintTypeId, tIncId, + {{SPV_OPERAND_TYPE_ID, {tContinueId}}, {SPV_OPERAND_TYPE_ID, {uint1}}}); + addInst(continueBlock.get(), spv::Op::OpStore, 0, 0, + {{SPV_OPERAND_TYPE_ID, {counterVarId}}, {SPV_OPERAND_TYPE_ID, {tIncId}}}); + addInst(continueBlock.get(), spv::Op::OpBranch, 0, 0, + {{SPV_OPERAND_TYPE_ID, {headerLabelId}}}); + + BasicBlock* headerPtr = entryFn->InsertBasicBlockBefore(std::move(headerBlock), restBlock); + BasicBlock* checkPtr = entryFn->InsertBasicBlockAfter(std::move(checkBlock), headerPtr); + BasicBlock* bodyPtr = entryFn->InsertBasicBlockAfter(std::move(bodyBlock), checkPtr); + entryFn->InsertBasicBlockAfter(std::move(continueBlock), bodyPtr); + + // Redirect the pre-existing accesses to the shadow copy. + for (Instruction* chain : chains) { + if (chain->NumInOperands() == 3) { + // (vertex, member): whole float[N] of one vertex. + Instruction* chainPtrType = defUse->GetDef(chain->type_id()); + const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1); + const uint32_t newPtrType = + PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private); + ctx->ForgetUses(chain); + std::vector newOperands; + newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}}); + newOperands.push_back(chain->GetInOperand(1)); + chain->SetResultType(newPtrType); + chain->SetInOperands(std::move(newOperands)); + ctx->AnalyzeUses(chain); + } else { + // (vertex, member, k, ...): drop the member index. + Instruction* chainPtrType = defUse->GetDef(chain->type_id()); + const uint32_t pointeeId = chainPtrType->GetSingleWordInOperand(1); + const uint32_t newPtrType = + PointerTypeTo(ctx, pointeeId, spv::StorageClass::Private); + ctx->ForgetUses(chain); + std::vector newOperands; + newOperands.push_back({SPV_OPERAND_TYPE_ID, {mgInVarId}}); + newOperands.push_back(chain->GetInOperand(1)); + for (uint32_t i = 3; i < chain->NumInOperands(); ++i) { + newOperands.push_back(chain->GetInOperand(i)); + } + chain->SetResultType(newPtrType); + chain->SetInOperands(std::move(newOperands)); + ctx->AnalyzeUses(chain); + } + } + + return true; + } + } // namespace + + spvtools::opt::Pass::Status LowerClipDistanceForEsslPass::Process() { + auto* ctx = context(); + const spv::ExecutionModel model = EntryExecutionModel(ctx); + const bool isVertex = model == spv::ExecutionModel::Vertex; + const bool isGeometry = model == spv::ExecutionModel::Geometry; + if (!isVertex && !isGeometry) { + return Status::SuccessWithoutChange; + } + + bool changed = LowerOutputClipDistance(ctx, isGeometry); + if (isGeometry) { + changed |= LowerInputClipDistance(ctx); + } + + if (!changed) { + return Status::SuccessWithoutChange; + } + ctx->InvalidateAnalysesExceptFor(spvtools::opt::IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken + LowerClipDistanceForEsslPass::CreateLowerClipDistanceForEsslPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.h new file mode 100644 index 00000000..3b3d8c51 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.h @@ -0,0 +1,44 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerClipDistanceForEsslPass.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 + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // Adreno's ESSL compiler mishandles gl_ClipDistance (device-verified on Adreno 750): + // - writes through non-constant indices silently fail to link, + // - reads of gl_in[i].gl_ClipDistance[k] with a CONSTANT k >= 1 fail to compile + // ("array indexing out of boundary") while dynamic-index reads work, + // - compiling a whole-array read of gl_in[i].gl_ClipDistance segfaults the + // compiler backend (libllvm-qgl.so). + // This pass shadows the builtin so the decompiled ESSL only ever touches it in the + // shapes Adreno accepts. Output side (vertex + geometry): all accesses to the + // Output ClipDistance (gl_PerVertex member or standalone variable) are redirected + // to a Private mg_ClipDistance array, and a flush writing the real builtin with + // literal constant indices is inserted before every OpEmitVertex (geometry) or + // every return of the entry point (vertex). Input side (geometry): accesses to + // gl_in[...].gl_ClipDistance are redirected to a Private mg_ClipDistanceIn + // array-of-arrays filled once at the top of the entry point by a structured loop + // whose gl_in reads use dynamic (loop-variable) indices. The builtin members stay + // statically referenced by the flush/copy so cross-stage IO matching is intact. + // Only meant for the DirectGLES transpile path on Qualcomm devices. + class LowerClipDistanceForEsslPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "lower-clip-distance-for-essl"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateLowerClipDistanceForEsslPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL