mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
[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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user