[Fix, Test] (GLImpl, GLState): give a storage block with no binding qualifier GL's default binding of zero

This commit is contained in:
2026-08-21 05:13:58 -04:00
parent 21a4c8aa95
commit d9def5c1bb
11 changed files with 312 additions and 0 deletions
@@ -828,6 +828,13 @@ namespace MobileGL::MG_State::GLState {
for (const auto& [name, binding] : compiled.explicitOpaqueBindings) {
artifacts.explicitOpaqueUniformBindings[name] = binding;
}
// Storage blocks declared with NO layout(binding = N), which GL puts on binding 0.
// A UNION across stages, unlike the maps above: a block that any stage declared
// unqualified is unqualified, because GLSL requires every stage declaring the same
// block to declare it identically - so the stages cannot disagree, and a stage whose
// grammar the scanner did not recognise simply contributes nothing.
artifacts.storageBlocksWithoutBinding.insert(compiled.storageBlocksWithoutBinding.begin(),
compiled.storageBlocksWithoutBinding.end());
}
}
@@ -1505,6 +1512,7 @@ namespace MobileGL::MG_State::GLState {
for (Int i = 0; i < blockCount; ++i) {
artifacts.blockReflection.push_back(MakeResourceReflection(program.getUniformBlock(i)));
}
SeedDefaultStorageBlockBindings();
const Int uniformCount = program.getNumUniformVariables();
artifacts.uniformReflection.clear();
@@ -1553,6 +1561,63 @@ namespace MobileGL::MG_State::GLState {
artifacts.pipeInputReflection.size(), artifacts.pipeOutputReflection.size());
}
// GL 4.3 core 7.8: a shader storage block declared without a layout(binding = N) qualifier
// has a buffer binding of ZERO. MobileGL could not report that, because by the time this
// reflection is built the number in the block's qualifier is one glslang INVENTED.
//
// Every shader is parsed as a Vulkan client, so glslang's IO mapper takes the `set = openGl
// ? resource : ent.newSet` branch with openGl == 0 (iomapper.cpp resolveBinding) - i.e. it
// allocates out of ONE flat binding space shared by every sampler, image, uniform block,
// storage block and the synthesized MGL_GLOBAL_UBO - and then writes the result back into
// the type's qualifier (iomapper.cpp, `base->getWritableType().getQualifier().layoutBinding =
// at->second.newBinding`). getBinding() therefore answers with the auto-assigned slot and
// cannot be distinguished from a declared one. An unqualified block lands on 0 only when
// nothing else in the program claimed 0 first, which is why a lone storage block in a
// trivial shader looked correct and KHR-GL43.compute_shader.resource-ubo - whose shader also
// declares twelve uniform blocks - wrote everything to a binding nothing was bound at.
//
// THE FLAT SPACE IS LEFT ALONE. It is load-bearing: DirectVulkan indexes bindingKinds[],
// uniformBlockIndexByBinding[] and storageBlockIndexByBinding[] by that one number and
// asserts when two resources collide on it, so forcing the SPIR-V decoration to 0 would
// collide an unqualified block with the global UBO and take working programs down. What is
// repaired is the GL-VISIBLE binding, through the record GL already has for exactly this -
// the same per-name map glShaderStorageBlockBinding writes, which both backends already
// consult (ProgramInterface's GL_BUFFER_BINDING, DirectGLES's SPIRV-Cross binding rewrite,
// DirectVulkan's GetShaderStorageBlockBinding). Seeding it here means the default and a
// later rebind travel the same path, and basic-noBindingLayout - which rebinds all three of
// its unqualified blocks - keeps working because a rebind simply overwrites the seed.
//
// Seeded INSIDE `artifacts`, so an L1 translation-cache hit that republishes the artifacts
// wholesale carries it too; a seed applied outside them would silently vanish on a hit.
//
// Only blocks the lexical scanner recognised in full AND recognised as unqualified are
// seeded. A block whose grammar it did not understand keeps today's behaviour rather than
// being defaulted on a guess - the shape of the input decides, never an assumption about it.
//
// THE COLLISION IS DELIBERATE, and it is GL's. Several unqualified blocks all default to 0
// and alias there until the application rebinds them; a real GL driver does the same, which
// is why every program that has more than one either rebinds or uses one of them.
// basic-noBindingLayout is that regression test - it rebinds all three of its blocks
// immediately after linking, and the DirectGLES transpile is lazy (first use, not link), so
// the ESSL it eventually emits already carries the rebound 0/1/2 and never the aliased seed.
// What this replaces was not a safer arrangement, only an accidental one: the three blocks
// got glslang's 0/1/2 and an application that rebound them to anything else still wrote to
// the wrong buffers.
void ProgramLinkTask::SeedDefaultStorageBlockBindings() {
if (artifacts.storageBlocksWithoutBinding.empty()) return;
for (const ProgramObject::BlockReflection& block : artifacts.blockReflection) {
if (!block.type.isBuffer) continue;
// An instance array reflects as "B[0]", "B[1]", ... and each element is its own GL
// resource with its own binding; the scanner keys on the block TYPE name, so the
// subscript is stripped before the lookup. GL gives element k of an unqualified
// array binding 0 + k, the same base + element rule a declared binding follows.
const String base = StripArrayElementSuffix(block.name);
if (!artifacts.storageBlocksWithoutBinding.contains(base)) continue;
// First writer wins: never overwrite a binding the application has already chosen.
artifacts.shaderStorageBlockBinding.emplace(block.name, BlockArrayElement(block.name));
}
}
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
if (!artifacts.program) return false;
// The pipe-output list is the output interface of the program's LAST stage. Only a
@@ -189,6 +189,11 @@ namespace MobileGL::MG_State::GLState {
// Copies every reflection record the GL query surface reads out of the glslang
// TProgram into LinkArtifacts own owned tables. Runs at the tail of DoReflection.
void SnapshotGlslangReflection();
// Gives every storage block whose shader declared no layout(binding = N) the binding
// GL 4.3 core 7.8 says it has - zero - because glslang's IO mapper has by then invented
// one and overwritten the qualifier. See the definition for why the invented binding is
// deliberately left in place for the backends' own use.
void SeedDefaultStorageBlockBindings();
Bool ValidateFragmentOutputLocations();
Bool ResolveTransformFeedbackVaryings();
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
@@ -344,6 +344,10 @@ namespace MobileGL::MG_State::GLState {
artifacts.uniformBlockIndexByName.clear();
artifacts.uniformBlockBinding.clear();
artifacts.shaderStorageBlockBinding.clear();
// Cleared with it: the seed above is re-derived from the newly attached shaders on every
// link, so a stale set would otherwise default a block the new sources do declare a
// binding for.
artifacts.storageBlocksWithoutBinding.clear();
artifacts.attribs.clear();
artifacts.attribTypes.clear();
artifacts.activeUniformCount = 0;
@@ -1125,7 +1125,20 @@ namespace MobileGL::MG_State::GLState {
Vector<Int> uniformBlockBinding;
// glShaderStorageBlockBinding overrides, keyed by GL block name. See
// SetShaderStorageBlockBinding for why this one is by name and not by index.
//
// ALSO SEEDED AT LINK, by ProgramLinkTask::SeedDefaultStorageBlockBindings, with the
// GL-mandated binding 0 for every storage block whose shader declared no
// layout(binding = N). Those blocks have no other way to be told apart from a block
// that declared one: glslang's IO mapper invents a binding and writes it into the
// qualifier, so the reflection reports the invention. A seed is therefore "GL's
// default binding for this block", and a later glShaderStorageBlockBinding simply
// overwrites it - default and rebind travel one path.
UnorderedMap<String, Int> shaderStorageBlockBinding;
// Block type names of the storage blocks the program's shaders declared with NO
// layout(binding = N), merged across stages by MergeShaderSideChannels. Input to the
// seeding above; see ExtractStorageBlocksWithoutExplicitBinding for why it has to be
// captured lexically.
std::set<String> storageBlocksWithoutBinding;
Uint activeUniformCount = 0;
Uint maxUniformLocation = 0;
@@ -211,6 +211,12 @@ namespace {
// ProgramObject::DoReflection.
result.explicitUniformLocations = ExtractExplicitUniformLocations(result.preprocessedSource);
result.explicitOpaqueBindings = ExtractExplicitOpaqueBindings(result.preprocessedSource);
// The third side-channel, and the one that restores a GL DEFAULT rather than an
// application-declared value: a storage block with no layout(binding = N) has buffer
// binding 0 in GL, and by the time the reflection is built glslang's IO mapper has
// already invented one and written it into the qualifier.
result.storageBlocksWithoutBinding =
ExtractStorageBlocksWithoutExplicitBinding(result.preprocessedSource);
result.outcome = ShaderPreprocessOutcome::Preprocessed;
return result;
}
@@ -355,6 +361,7 @@ namespace MobileGL::MG_State::GLState {
artifacts.preprocessedSource = shared.preprocessedSource;
artifacts.explicitUniformLocations = shared.explicitUniformLocations;
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
artifacts.storageBlocksWithoutBinding = shared.storageBlocksWithoutBinding;
artifacts.infoLog.clear();
if (shouldPopulateCache) {
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
@@ -381,6 +388,7 @@ namespace MobileGL::MG_State::GLState {
fresh->infoLog = artifacts.infoLog;
fresh->explicitUniformLocations.clear();
fresh->explicitOpaqueBindings.clear();
fresh->storageBlocksWithoutBinding.clear();
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
}
}
@@ -62,6 +62,9 @@ namespace MobileGL::MG_State::GLState {
String preprocessedSource;
UnorderedMap<String, Int> explicitUniformLocations;
UnorderedMap<String, Uint> explicitOpaqueBindings;
// Block type names of this stage's shader storage blocks that declared no
// layout(binding = N), i.e. the ones GL puts on binding 0.
std::set<String> storageBlocksWithoutBinding;
String infoLog;
Bool compileStatus = false;
};
@@ -112,6 +112,13 @@ namespace MobileGL {
const UnorderedMap<String, Uint>& GetExplicitOpaqueBindings() const {
return Compiled().explicitOpaqueBindings;
}
// Block type names of this shader's storage blocks that declared NO
// layout(binding = N), captured lexically because glslang's IO mapper invents one
// and overwrites the qualifier before anything can ask (see
// ExtractStorageBlocksWithoutExplicitBinding).
const std::set<String>& GetStorageBlocksWithoutBinding() const {
return Compiled().storageBlocksWithoutBinding;
}
Bool GetCompileStatus() const { return Compiled().compileStatus; }
Bool GetDeleteStatus() const { return m_deleteStatus; }
@@ -46,6 +46,11 @@ namespace MobileGL::MG_State::GLState {
String preprocessedSource;
UnorderedMap<String, Int> explicitUniformLocations;
UnorderedMap<String, Uint> explicitOpaqueBindings;
// Block type names of the shader storage blocks declared here with NO
// layout(binding = N). GL gives such a block binding 0; nothing downstream can still
// tell, because glslang's IO mapper auto-assigns one and writes it into the qualifier.
// See ExtractStorageBlocksWithoutExplicitBinding.
std::set<String> storageBlocksWithoutBinding;
// The compile info log to publish; empty when outcome == Preprocessed.
String infoLog;