[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
@@ -1548,6 +1548,89 @@ namespace MobileGL {
}
} // namespace
std::set<String> ExtractStorageBlocksWithoutExplicitBinding(const String& source) {
std::set<String> names;
// Fast path: no storage block, nothing to record. `buffer` as a whole token is
// what declares one; samplerBuffer/imageBuffer/textureBuffer tokenize as single
// identifiers and so cannot match below, but this substring test is only a
// cheap pre-filter and is allowed to be generous.
if (source.find("buffer") == String::npos) return names;
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
Int braceDepth = 0;
// Block names seen WITH a binding. Subtracted at the end so a name that is
// declared unqualified in one place and qualified in another is never reported:
// this scans preprocessor-visible text, so both arms of a #if can be present,
// and defaulting a block the active arm binds explicitly would be a regression.
// A name is dropped whenever there is any doubt, never kept.
std::set<String> qualified;
// The binding the qualifier run currently being scanned declared, -1 for none.
// Several layout(...) lists may precede one declaration and the later one wins -
// the same accumulate-then-consume shape FindShaderStorageBindingViolation uses.
long long binding = -1;
long long literal = 0;
for (SizeT pos = 0; pos < count; ++pos) {
const String& text = tokens[pos].text;
if (text == "{") {
++braceDepth;
binding = -1;
continue;
}
if (text == "}") {
if (braceDepth > 0) --braceDepth;
continue;
}
// Only depth-0 declarations are block declarations; `buffer` inside a block
// body or a function is a member qualifier or an identifier.
if (braceDepth != 0) continue;
if (text == "layout" && pos + 1 < count && tokens[pos + 1].text == "(") {
SizeT j = pos + 2;
Int parenDepth = 1;
while (j < count && parenDepth > 0) {
const String& layoutToken = tokens[j].text;
if (layoutToken == "(") {
++parenDepth;
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < count &&
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
}
pos = j - 1;
continue;
}
if (text == "buffer") {
// Recorded ONLY for the fully recognised shape: a block type name
// followed by the body's '{'. The "layout(...) buffer;"
// default-qualifier form declares no block and has no name to key on,
// and anything else here is grammar this scanner does not judge - both
// fall through and keep today's behaviour.
if (pos + 2 < count && IsIdentifierToken(tokens[pos + 1]) &&
tokens[pos + 2].text == "{") {
(binding < 0 ? names : qualified).insert(tokens[pos + 1].text);
}
binding = -1;
continue;
}
// Qualifiers may sit between the layout list and the `buffer` keyword;
// anything else ends the run, so a binding never leaks onto an unrelated
// declaration - and, just as importantly, the ABSENCE of one never does.
if (!IsNonLayoutQualifierKeyword(text)) binding = -1;
}
for (const String& name : qualified) {
names.erase(name);
}
return names;
}
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings) {
// A backend that advertises nothing has no ceiling to enforce.
if (maxBindings <= 0) return std::nullopt;
@@ -7,6 +7,8 @@
// End of Source File Header
#pragma once
#include <set>
#include <Includes.h>
#include <MG_State/GLState/ProgramState/ShaderObject.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
@@ -65,6 +67,24 @@ namespace MobileGL {
// grammar discipline as ExtractExplicitUniformLocations).
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
// The BLOCK TYPE NAMES of the shader storage blocks this source declares WITHOUT a
// layout(binding = N) qualifier. GL 4.3 core 7.8 gives such a block a buffer binding
// of ZERO, which the application may then move with glShaderStorageBlockBinding.
//
// Exists because nothing downstream can still tell. Every shader is parsed as a
// Vulkan client, so glslang's IO mapper allocates a binding for the block out of one
// flat space shared with every sampler, image and uniform block in the program
// (iomapper.cpp resolveBinding: `set = openGl ? resource : ent.newSet`, and openGl is
// 0 here) - and then WRITES IT BACK into the type's qualifier, so the reflection
// reports an auto-assigned number as if the shader had declared it. An unqualified
// block therefore lands on 0 only when nothing else claimed 0 first.
//
// Reported POSITIVELY - only blocks the scanner recognised in full, and recognised as
// carrying no binding - so anything outside its narrow grammar is left to the
// existing behaviour rather than defaulted on a guess. Same discipline as
// ExtractExplicitOpaqueBindings.
std::set<String> ExtractStorageBlocksWithoutExplicitBinding(const String& source);
// A shader storage block whose layout(binding = N) reaches or passes
// GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS is a compile-time error in GL 4.3 core 4.4.5,
// and an arrayed block instance takes CONSECUTIVE points, so the last element is what