[Fix, Test] (GLState, ShaderTranspiler): report GL's default binding of zero for an unqualified uniform block

This commit is contained in:
2026-08-22 12:41:18 -04:00
parent 6a2e9dc791
commit 0eb5d54bb8
8 changed files with 103 additions and 13 deletions
@@ -627,7 +627,8 @@ namespace MobileGL::MG_State::GLState {
.explicitFragmentOutLocations = in.explicitFragDataLocation,
.explicitFragmentOutIndices = in.explicitFragDataIndex,
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding};
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding,
.uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding};
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
auto result = ShaderCompiler::LinkProgram(attrib);
@@ -1573,7 +1574,25 @@ namespace MobileGL::MG_State::GLState {
// (DirectGLES.cpp / UniformManager.cpp), all 14 elements also read the same
// buffer. This is the rule the storage-block path in ProgramInterface.cpp
// already applies, and whose comment there claims uniform blocks follow.
const Int declaredBinding = ubo.getBinding();
//
// "Declared" cannot be read back off the reflection, though. MobileGL asks glslang
// to auto-map bindings, so mapIO writes an invented one into every block's
// qualifier before reflection ever runs and ubo.getBinding() is never negative;
// worse, glslang packs uniform blocks into the SAME slot space as samplers and
// images (setEnvClient(EShClientVulkan) leaves spvVersion.openGl at 0, so
// TDefaultGlslIoResolver::resolveBinding keys every resource kind on set 0), so a
// block declared after an unbound image gets 1. GL 4.6 core 7.6.2 says an
// unqualified block reports ZERO. The set below is the shader's own answer,
// captured during mapIO while the qualifier still meant it - the same mechanism
// SeedDefaultStorageBlockBindings uses for storage blocks, and the aliasing at 0
// that results is GL's, not a bug: unqualified blocks collide there until the
// application rebinds them.
//
// Only this GL-visible binding POINT changes. The backends' descriptor lookups run
// off glslang's assignment through uniformBlockIndexByBinding, which is untouched.
const String blockTypeName = StripArrayElementSuffix(ubo.name);
const Int declaredBinding =
artifacts.uniformBlocksWithoutBinding.contains(blockTypeName) ? 0 : ubo.getBinding();
artifacts.uniformBlockBinding[i] =
declaredBinding < 0 ? declaredBinding : declaredBinding + BlockArrayElement(ubo.name);
MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i,
@@ -369,6 +369,7 @@ namespace MobileGL::MG_State::GLState {
// link, so a stale set would otherwise default a block the new sources do declare a
// binding for.
artifacts.storageBlocksWithoutBinding.clear();
artifacts.uniformBlocksWithoutBinding.clear();
artifacts.attribs.clear();
artifacts.attribTypes.clear();
artifacts.activeUniformCount = 0;
@@ -1024,8 +1024,10 @@ namespace MobileGL::MG_State::GLState {
Uint32 GetBlockBindingVersion() const { return m_blockBindingVersion; }
// Set by glUniformBlockBinding. The vector is seeded at link with each block's DECLARED
// binding (layout(binding=N), else -1), so an untouched program already reports what its
// shaders asked for.
// binding (layout(binding=N)), and with GL's default of 0 for a block that declared none
// - which the reflection cannot tell apart on its own, so the seeder consults
// uniformBlocksWithoutBinding. Either way an untouched program already reports what GL
// says it should.
void SetUniformBlockBinding(Uint index, Uint binding) {
if (index >= Artifacts().uniformBlockBinding.size() || Artifacts().uniformBlockBinding[index] == static_cast<Int>(binding)) {
return;
@@ -1287,6 +1289,11 @@ namespace MobileGL::MG_State::GLState {
// binding from an invented one - and, unlike the per-shader lexer this replaced,
// sees the declaration with its macros expanded.
std::set<String> storageBlocksWithoutBinding;
// The same list for UNIFORM blocks, and it is needed for the same reason: glslang's
// auto-mapper assigns every uniform block a binding whether or not the shader asked
// for one, so uniformBlockBinding below cannot tell "declared 1" from "invented 1".
// GL 4.6 core 7.6.2 requires an unqualified block to report ZERO.
std::set<String> uniformBlocksWithoutBinding;
Uint activeUniformCount = 0;
Uint maxUniformLocation = 0;