diff --git a/CMakeLists.txt b/CMakeLists.txt index bae83295..88a02fe5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.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 1a2d120e..bf3aaa7d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2722,6 +2722,20 @@ namespace MobileGL::MG_Backend::DirectGLES { effectiveSpirv = &loweredSpirv; } + // 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 + // ForceSupporterOutput swaps the fragment header to highp, that member reads + // back as highp and the ES driver refuses to link ("definitions of uniform + // block ... do not match"). Strip the hint from block structs so both stages + // declare the member highp; nothing else about emission changes. + Vector uboPrecisionSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl( + *effectiveSpirv, uboPrecisionSpirv) && + !uboPrecisionSpirv.empty()) { + effectiveSpirv = &uboPrecisionSpirv; + } + MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv, MG_Util::ShaderTranspiler::SessionUsageBit::Transpile); @@ -2732,17 +2746,6 @@ namespace MobileGL::MG_Backend::DirectGLES { ResolveBackendEsslVersion()); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE); spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE); - // Emit against highp default precision in every stage. SPIRV-Cross's fragment - // default is mediump, under which a RelaxedPrecision struct member prints with - // NO qualifier; ForceSupporterOutput later swaps the header to highp, silently - // flipping such members to highp. A uniform-block member that stays explicitly - // "mediump" in the vertex stage then mismatches, and the ES driver refuses to - // link ("definitions of uniform block ... do not match"). With highp defaults - // every relaxed member is printed with an explicit qualifier in both stages. - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES_DEFAULT_FLOAT_PRECISION_HIGHP, - SPVC_TRUE); - spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES_DEFAULT_INT_PRECISION_HIGHP, - SPVC_TRUE); spvcSession.SetOptions(options); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index b9e6f100..4833d1d0 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -14,6 +14,7 @@ #include "SpirvPasses/DecomposeWorkgroupVec3Pass.h" #include "SpirvPasses/LowerDrawParametersPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h" +#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" #include "spirv-tools/libspirv.h" #include "spirv-tools/optimizer.hpp" @@ -266,6 +267,19 @@ namespace MobileGL { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } + bool ShaderCompiler::StripUboMemberRelaxedPrecisionForEssl(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); + + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass( + StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass()); + + return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); + } + bool ShaderCompiler::RebaseInstanceIndexForVulkan(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 7917acda..6fb1abbf 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -27,6 +27,12 @@ namespace MobileGL { // Only for backends without native draw-parameter support (DirectGLES). static bool LowerDrawParametersForEssl(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. + // Only for the DirectGLES transpile path. + static bool StripUboMemberRelaxedPrecisionForEssl(const Vector& inputBinary, + Vector& outputBinary); // 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/StripUboMemberRelaxedPrecisionPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp new file mode 100644 index 00000000..8cc02ec1 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp @@ -0,0 +1,119 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.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 "StripUboMemberRelaxedPrecisionPass.h" + +#include "spirv.hpp" +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_context.h" +#include "source/opt/module.h" + +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + + // Marks `typeId` and every struct type reachable through its members + // (following arrays) for decoration stripping. + void CollectStructTypes(IRContext* context, uint32_t typeId, + std::unordered_set& structTypeIds) { + Instruction* typeInst = context->get_def_use_mgr()->GetDef(typeId); + if (typeInst == nullptr) return; + + switch (typeInst->opcode()) { + case spv::Op::OpTypeStruct: { + if (!structTypeIds.insert(typeId).second) return; // already visited + for (uint32_t member = 0; member < typeInst->NumInOperands(); ++member) { + CollectStructTypes(context, typeInst->GetSingleWordInOperand(member), structTypeIds); + } + break; + } + case spv::Op::OpTypeArray: + case spv::Op::OpTypeRuntimeArray: + CollectStructTypes(context, typeInst->GetSingleWordInOperand(0), structTypeIds); + break; + default: + break; + } + } + } // namespace + + spvtools::opt::Pass::Status StripUboMemberRelaxedPrecisionPass::Process() { + auto* irContext = context(); + auto* defUseMgr = irContext->get_def_use_mgr(); + + // Uniform blocks: StorageClass Uniform variables whose pointee struct carries + // the Block decoration (BufferBlock/StorageBuffer SSBOs are left alone - they + // are not stage-matched by member precision in this pipeline's ESSL output). + std::unordered_set blockStructIds; + for (Instruction& annotation : irContext->module()->annotations()) { + if (annotation.opcode() != spv::Op::OpDecorate) continue; + if (static_cast(annotation.GetSingleWordInOperand(1)) != spv::Decoration::Block) { + continue; + } + blockStructIds.insert(annotation.GetSingleWordInOperand(0)); + } + if (blockStructIds.empty()) return Status::SuccessWithoutChange; + + std::unordered_set structTypeIds; + for (Instruction& variable : irContext->module()->types_values()) { + if (variable.opcode() != spv::Op::OpVariable) continue; + if (static_cast(variable.GetSingleWordInOperand(0)) != + spv::StorageClass::Uniform) { + continue; + } + + Instruction* pointerType = defUseMgr->GetDef(variable.type_id()); + if (pointerType == nullptr || pointerType->opcode() != spv::Op::OpTypePointer) continue; + uint32_t pointeeId = pointerType->GetSingleWordInOperand(1); + + // Instance-arrayed blocks: unwrap the array around the block struct. + Instruction* pointee = defUseMgr->GetDef(pointeeId); + while (pointee != nullptr && (pointee->opcode() == spv::Op::OpTypeArray || + pointee->opcode() == spv::Op::OpTypeRuntimeArray)) { + pointeeId = pointee->GetSingleWordInOperand(0); + pointee = defUseMgr->GetDef(pointeeId); + } + if (pointee == nullptr || pointee->opcode() != spv::Op::OpTypeStruct) continue; + if (blockStructIds.find(pointeeId) == blockStructIds.end()) continue; + + CollectStructTypes(irContext, pointeeId, structTypeIds); + } + if (structTypeIds.empty()) return Status::SuccessWithoutChange; + + std::vector decorationsToRemove; + for (Instruction& annotation : irContext->module()->annotations()) { + if (annotation.opcode() != spv::Op::OpMemberDecorate) continue; + if (static_cast(annotation.GetSingleWordInOperand(2)) != + spv::Decoration::RelaxedPrecision) { + continue; + } + if (structTypeIds.find(annotation.GetSingleWordInOperand(0)) == structTypeIds.end()) continue; + decorationsToRemove.push_back(&annotation); + } + if (decorationsToRemove.empty()) return Status::SuccessWithoutChange; + + for (Instruction* decoration : decorationsToRemove) { + irContext->KillInst(decoration); + } + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken + StripUboMemberRelaxedPrecisionPass::CreateStripUboMemberRelaxedPrecisionPass() { + return spvtools::Optimizer::PassToken(MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.h new file mode 100644 index 00000000..a39c0ddb --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.h @@ -0,0 +1,44 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.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 { + // Removes RelaxedPrecision member decorations from every struct type reachable + // from a uniform-block variable (the block struct itself and any structs nested + // in it through members or arrays). + // + // Rationale: ESSL requires matched uniform blocks to declare members with + // identical precision in every stage, but SPIRV-Cross prints a member's + // qualifier relative to the stage's DEFAULT precision (highp in the vertex + // stage, mediump in the fragment stage). A RelaxedPrecision member therefore + // comes out as an explicit "mediump" in the vertex shader but UNQUALIFIED in + // the fragment shader - and once ForceSupporterOutput swaps the fragment + // header to "precision highp float;", that unqualified member reads back as + // highp and the ES driver refuses to link ("definitions of uniform block ... + // do not match", GL CTS KHR-GL33.shaders.uniform_block struct sub-groups). + // Dropping the hint promotes the member to highp in BOTH stages, which is + // always conformant and matches the std140 data layout either way. Only meant + // for the DirectGLES transpile path - block member precision is a per-member + // hint with no layout effect, and no other emission behavior changes. + class StripUboMemberRelaxedPrecisionPass : public spvtools::opt::Pass { + public: + const char* name() const override { return "strip-ubo-member-relaxed-precision"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateStripUboMemberRelaxedPrecisionPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL