mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08: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/DecomposeWorkgroupVec3Pass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.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/OpenGL/Loader.cpp
|
||||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||||
|
|||||||
@@ -2722,6 +2722,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
effectiveSpirv = &loweredSpirv;
|
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::SpvcSession spvcSession(*effectiveSpirv,
|
||||||
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
|
||||||
|
|
||||||
@@ -2732,17 +2746,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
ResolveBackendEsslVersion());
|
ResolveBackendEsslVersion());
|
||||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
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);
|
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);
|
spvcSession.SetOptions(options);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||||
#include "SpirvPasses/LowerDrawParametersPass.h"
|
#include "SpirvPasses/LowerDrawParametersPass.h"
|
||||||
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
#include "SpirvPasses/RebaseInstanceIndexPass.h"
|
||||||
|
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||||
#include "spirv-tools/libspirv.h"
|
#include "spirv-tools/libspirv.h"
|
||||||
#include "spirv-tools/optimizer.hpp"
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
@@ -266,6 +267,19 @@ namespace MobileGL {
|
|||||||
return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options);
|
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,
|
bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary) {
|
Vector<uint32_t>& outputBinary) {
|
||||||
using namespace spvtools;
|
using namespace spvtools;
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ namespace MobileGL {
|
|||||||
// Only for backends without native draw-parameter support (DirectGLES).
|
// Only for backends without native draw-parameter support (DirectGLES).
|
||||||
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
static bool LowerDrawParametersForEssl(const Vector<Uint32>& inputBinary,
|
||||||
Vector<uint32_t>& outputBinary);
|
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
|
// Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so
|
||||||
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
// shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan
|
||||||
// backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex,
|
// 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