[Fix] (MG_Backend/DirectGLES, MG_Util/ShaderTranspiler): make the uniform-block cross-stage precision fix surgical - revert the global SPVC ES highp-default options (they changed emission for EVERY fragment shader: sampling code that used to inherit the effective highp default was suddenly printed as explicit mediump, regressing KHR-GL3x.texture_repeat_mode NPOT mip cases on device) and instead strip RelaxedPrecision member decorations from uniform-block-reachable structs in a DirectGLES-only SPIR-V pass, so matched blocks declare identical (highp) member precision in both stages and every other shader keeps its previous emission byte-for-byte

This commit is contained in:
2026-07-16 16:36:28 -04:00
parent 254cf1dc21
commit 8bf8f6f906
6 changed files with 198 additions and 11 deletions
+1
View File
@@ -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
+14 -11
View File
@@ -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<unsigned int> 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);
@@ -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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary) {
using namespace spvtools;
@@ -27,6 +27,12 @@ namespace MobileGL {
// Only for backends without native draw-parameter support (DirectGLES).
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& 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,
@@ -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 <unordered_set>
#include <vector>
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<uint32_t>& 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<uint32_t> blockStructIds;
for (Instruction& annotation : irContext->module()->annotations()) {
if (annotation.opcode() != spv::Op::OpDecorate) continue;
if (static_cast<spv::Decoration>(annotation.GetSingleWordInOperand(1)) != spv::Decoration::Block) {
continue;
}
blockStructIds.insert(annotation.GetSingleWordInOperand(0));
}
if (blockStructIds.empty()) return Status::SuccessWithoutChange;
std::unordered_set<uint32_t> structTypeIds;
for (Instruction& variable : irContext->module()->types_values()) {
if (variable.opcode() != spv::Op::OpVariable) continue;
if (static_cast<spv::StorageClass>(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<Instruction*> decorationsToRemove;
for (Instruction& annotation : irContext->module()->annotations()) {
if (annotation.opcode() != spv::Op::OpMemberDecorate) continue;
if (static_cast<spv::Decoration>(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<StripUboMemberRelaxedPrecisionPass>());
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -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 <Includes.h>
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