diff --git a/CMakeLists.txt b/CMakeLists.txt index 8778865b..a2b72238 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -194,6 +194,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index b4567eb2..c78f1208 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -449,6 +449,20 @@ namespace MobileGL::MG_Backend::DirectGLES { } } } + + // Textures attached only to the READ framebuffer (blit / ReadPixels sources) need + // their content synced too, or the backend reads stale texel data. + const auto& readFBO = + MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(); + if (readFBO && readFBO != currentFBO) { + for (const auto& attachment : readFBO->GetAllAttachmentObjects()) { + if (!attachment.IsTexture()) continue; + auto& textureObject = attachment.GetTexture(); + if (textureObject) { + SyncTextureObjectToBackend(textureObject); + } + } + } } static Bool SupportsLayeredImageBinding(TextureTarget target) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index ccf979fc..c238a01f 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1262,15 +1262,31 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& allAttributes = stateVAOObject->GetAllAttributes(); for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) { const auto& attrib = allAttributes[attribIndex]; + const Uint32 attribBit = 1u << attribIndex; + + // An enabled attrib with neither a buffer object nor a client pointer has no + // source; GL tolerates the state (only draws consuming it are undefined), but + // Adreno's ES driver memcpys the "client array" from address 0 at draw time + // (SIGSEGV). Keep such attribs disabled on the backend VAO and re-enable them + // the moment they gain a source - the mask-vs-current compare below triggers + // the enable even when only the Buffer/Format versions changed. + const Bool unsourceable = attrib.Enabled && !attrib.Buffer && attrib.Offset == 0; + const Bool wasForceDisabled = (m_forceDisabledAttribsMask & attribBit) != 0; + Bool needsSyncSwitch = allAttributeVersions[attribIndex].SwitchVersion != m_syncedAttributeVersions[attribIndex].SwitchVersion; - if (needsSyncSwitch) { - if (attrib.Enabled) { + if (needsSyncSwitch || unsourceable != wasForceDisabled) { + if (attrib.Enabled && !unsourceable) { g_GLESFuncs.glEnableVertexAttribArray(attribIndex); } else { g_GLESFuncs.glDisableVertexAttribArray(attribIndex); } } + if (unsourceable) { + m_forceDisabledAttribsMask |= attribBit; + } else { + m_forceDisabledAttribsMask &= ~attribBit; + } Bool needsSyncFormat = allAttributeVersions[attribIndex].FormatVersion != m_syncedAttributeVersions[attribIndex].FormatVersion; @@ -1278,7 +1294,17 @@ namespace MobileGL::MG_Backend::DirectGLES { m_syncedAttributeVersions[attribIndex].BufferVersion; if (!needsSyncFormat && !needsSyncBuffer) continue; + if (unsourceable) continue; + + // Client-side array with a non-null pointer: the pointer is uploaded and applied + // per draw by SyncClientSideAttributesForDrawArrays. + if (!attrib.Buffer) continue; + if (!BindAttributeBuffer(attrib)) { + if (attrib.Enabled) { + g_GLESFuncs.glDisableVertexAttribArray(attribIndex); + m_forceDisabledAttribsMask |= attribBit; + } continue; } @@ -3287,6 +3313,32 @@ namespace MobileGL::MG_Backend::DirectGLES { effectiveSpirv = &loweredSpirv; } + // GL 3.3 only promises undefined *values* for out-of-bounds array indexing, but + // Adreno's ESSL compiler constant-folds a provably out-of-bounds local-array + // index into poison that corrupts the whole shader's output. Clamp every + // access-chain index to its declared bounds before transpiling. + Vector clampedSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::ClampAccessChainIndicesForEssl(*effectiveSpirv, + clampedSpirv) && + !clampedSpirv.empty()) { + effectiveSpirv = &clampedSpirv; + } else { + MGLOG_W("ClampAccessChainIndicesForEssl failed, continuing with unclamped SPIR-V."); + } + + // SPIRV-Cross emulates 1D samplers as 2D for ES: it widens texelFetch coordinates + // to ivec2 but keeps the ConstOffset operand scalar, which is not a valid ESSL + // texelFetchOffset overload (Adreno rejects it). Fold the constant offset into the + // coordinate instead (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)). + Vector foldedOffsetSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::FoldConstOffsetFor1DFetchForEssl( + *effectiveSpirv, foldedOffsetSpirv) && + !foldedOffsetSpirv.empty()) { + effectiveSpirv = &foldedOffsetSpirv; + } else { + MGLOG_W("FoldConstOffsetFor1DFetchForEssl failed, continuing with unfolded 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 diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 5aad4128..9e2c7f10 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -258,6 +258,11 @@ namespace MobileGL::MG_Backend::DirectGLES { private: Uint m_backendVAOId = 0; Array m_clientAttributeBufferIds; + // Attribs the frontend has Enabled but that have no source at all (no buffer object + // and NULL client pointer). GL keeps such attribs latently enabled, but Adreno's ES + // driver treats them as client arrays and memcpys from address 0 at draw time + // (SIGSEGV), so they are kept disabled on the backend VAO until they gain a source. + Uint32 m_forceDisabledAttribsMask = 0; Bool m_isInitialized = false; Uint16 m_syncedIndexBufferVersion = 0; Array diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 02f513fd..fc6aae79 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -22,6 +22,7 @@ #include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" #include "SpirvPasses/StripNoPerspectivePass.h" #include "SpirvPasses/EmulateNoPerspectivePass.h" +#include "SpirvPasses/FoldConstOffsetFor1DFetchPass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" @@ -323,6 +324,30 @@ namespace MobileGL { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } + bool ShaderCompiler::ClampAccessChainIndicesForEssl(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); + + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(CreateGraphicsRobustAccessPass()); + + return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); + } + + bool ShaderCompiler::FoldConstOffsetFor1DFetchForEssl(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); + + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass()); + + 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 4f282fb8..2f3ef457 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -27,6 +27,22 @@ namespace MobileGL { // Only for backends without native draw-parameter support (DirectGLES). static bool LowerDrawParametersForEssl(const Vector& inputBinary, Vector& outputBinary); + // Clamps every access-chain index to its declared bounds (spirv-tools + // GraphicsRobustAccessPass). GL 3.3 only promises undefined *values* for + // out-of-bounds indexing, but Adreno's ESSL compiler constant-folds a provably + // out-of-bounds local-array index into poison that corrupts the whole shader's + // output; clamping restores the "some value from the array" contract. Only for + // the DirectGLES transpile path. + static bool ClampAccessChainIndicesForEssl(const Vector& inputBinary, + Vector& outputBinary); + // Folds the ConstOffset image operand of Dim1D OpImageFetch into the integer + // coordinate (texelFetchOffset(t,P,l,o) == texelFetch(t,P+o,l)). SPIRV-Cross + // emulates 1D samplers as 2D for ES: it widens the coordinate to ivec2 but keeps + // the scalar offset, and ESSL has no texelFetchOffset(sampler2D, ivec2, int, + // scalar) overload, so Adreno rejects the shader. Only for the DirectGLES + // transpile path. + static bool FoldConstOffsetFor1DFetchForEssl(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/FoldConstOffsetFor1DFetchPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp new file mode 100644 index 00000000..fb9610e3 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.cpp @@ -0,0 +1,131 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.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 "FoldConstOffsetFor1DFetchPass.h" + +#include "spirv.hpp" +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_builder.h" +#include "source/opt/ir_context.h" +#include "source/opt/module.h" +#include "source/util/make_unique.h" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::InstructionBuilder; + using spvtools::opt::IRContext; + using spvtools::opt::Operand; + + // Number of ImageOperands ids that precede the ConstOffset id: one per + // lower-order bit set in the mask, except Grad which carries two ids. + uint32_t CountIdsBeforeConstOffset(uint32_t mask) { + uint32_t count = 0; + if (mask & static_cast(spv::ImageOperandsMask::Bias)) count += 1; + if (mask & static_cast(spv::ImageOperandsMask::Lod)) count += 1; + if (mask & static_cast(spv::ImageOperandsMask::Grad)) count += 2; + return count; + } + } // namespace + + spvtools::opt::Pass::Status FoldConstOffsetFor1DFetchPass::Process() { + auto* irContext = context(); + auto* defUseMgr = irContext->get_def_use_mgr(); + Bool modified = false; + + constexpr uint32_t kConstOffsetBit = + static_cast(spv::ImageOperandsMask::ConstOffset); + + for (auto& function : *get_module()) { + for (auto& block : function) { + for (auto& inst : block) { + if (inst.opcode() != spv::Op::OpImageFetch) continue; + // In-operands: image, coordinate, [ImageOperands mask, ids...]. + if (inst.NumInOperands() < 3) continue; + const uint32_t operandsMask = inst.GetSingleWordInOperand(2); + if ((operandsMask & kConstOffsetBit) == 0) continue; + + Instruction* imageInst = defUseMgr->GetDef(inst.GetSingleWordInOperand(0)); + if (imageInst == nullptr) continue; + Instruction* imageType = defUseMgr->GetDef(imageInst->type_id()); + if (imageType == nullptr || imageType->opcode() != spv::Op::OpTypeImage || + static_cast(imageType->GetSingleWordInOperand(1)) != spv::Dim::Dim1D) { + continue; + } + + const uint32_t offsetOperandIndex = 3 + CountIdsBeforeConstOffset(operandsMask); + const uint32_t offsetId = inst.GetSingleWordInOperand(offsetOperandIndex); + + const uint32_t coordId = inst.GetSingleWordInOperand(1); + Instruction* coordType = defUseMgr->GetDef(defUseMgr->GetDef(coordId)->type_id()); + + InstructionBuilder builder( + irContext, &inst, + IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); + + uint32_t newCoordId = 0; + if (coordType->opcode() == spv::Op::OpTypeVector) { + // Arrayed 1D fetch: component 0 is the texel coordinate, + // component 1 the layer - only component 0 takes the offset. + const uint32_t componentTypeId = coordType->GetSingleWordInOperand(0); + Instruction* extracted = builder.AddCompositeExtract(componentTypeId, coordId, {0}); + Instruction* sum = + builder.AddIAdd(componentTypeId, extracted->result_id(), offsetId); + Instruction* inserted = builder.AddInstruction(spvtools::MakeUnique( + irContext, spv::Op::OpCompositeInsert, coordType->result_id(), + irContext->TakeNextId(), + std::initializer_list{ + {SPV_OPERAND_TYPE_ID, {sum->result_id()}}, + {SPV_OPERAND_TYPE_ID, {coordId}}, + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {0}}})); + newCoordId = inserted->result_id(); + } else { + Instruction* sum = builder.AddIAdd(coordType->result_id(), coordId, offsetId); + newCoordId = sum->result_id(); + } + + const uint32_t newMask = operandsMask & ~kConstOffsetBit; + // 3 fixed operands + the offset id: anything beyond that is another + // image-operand id that must keep the mask word alive. + const Bool otherOperandIdsRemain = inst.NumInOperands() > 4; + + irContext->ForgetUses(&inst); + std::vector newOperands; + newOperands.push_back(inst.GetInOperand(0)); + newOperands.push_back({SPV_OPERAND_TYPE_ID, {newCoordId}}); + if (newMask != 0 || otherOperandIdsRemain) { + Operand maskOperand = inst.GetInOperand(2); + maskOperand.words[0] = newMask; + newOperands.push_back(maskOperand); + for (uint32_t i = 3; i < inst.NumInOperands(); ++i) { + if (i == offsetOperandIndex) continue; + newOperands.push_back(inst.GetInOperand(i)); + } + } + inst.SetInOperands(std::move(newOperands)); + irContext->AnalyzeUses(&inst); + + modified = true; + } + } + } + + return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange; + } + + spvtools::Optimizer::PassToken FoldConstOffsetFor1DFetchPass::CreateFoldConstOffsetFor1DFetchPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.h new file mode 100644 index 00000000..3c4ea1e8 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.h @@ -0,0 +1,36 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FoldConstOffsetFor1DFetchPass.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 emulates 1D textures as 2D for ES targets: it widens the texelFetch + // coordinate to ivec2 but keeps the ConstOffset image operand scalar, and ESSL has + // no texelFetchOffset(sampler2D, ivec2, int, scalar-offset) overload, so drivers + // (Adreno) reject the transpiled shader. This pass folds the constant offset into + // the integer coordinate before the fetch - texelFetchOffset(t, P, l, o) == + // texelFetch(t, P + o, l) per the GLSL spec - and drops the ConstOffset operand, + // so SPIRV-Cross emits a plain texelFetch. For arrayed 1D fetches only coordinate + // component 0 is offset (component 1 is the layer). Only meant for the DirectGLES + // transpile path. + class FoldConstOffsetFor1DFetchPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "fold-const-offset-for-1d-fetch"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateFoldConstOffsetFor1DFetchPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL