[Refactor, Test] (ShaderTranspiler, GLState): take what the relaxed parse destroys from glslang instead of scanning the source

This commit is contained in:
2026-08-21 13:51:22 -04:00
parent e5846569ca
commit cbb616093b
19 changed files with 1039 additions and 1040 deletions
@@ -358,6 +358,107 @@ namespace MobileGL {
glslang::SetThreadPoolAllocator(nullptr);
}
namespace {
// glslang reflects an array-of-arrays default-block uniform as ONE RECORD PER
// outer-index tuple, carrying the innermost array type: `float u[2][3]` becomes
// "u[0][0]" and "u[1][0]" (that last "[0]" is EShReflectionBasicArraySuffix). The
// linker resolves such a name by stripping the single trailing "[0]", so it looks
// up "u[1]" - a key the root entry alone cannot answer, and the whole declaration
// silently loses its explicit location.
//
// Emit those pre-flattened keys next to the root, so the result is
// order-independent: each carries the location its own element starts at (element
// i of `float u[2][3]` at location L starts at L + i*3). Identifiers cannot
// contain brackets, so a synthesized key never collides with a real uniform name,
// and a 1-D array needs none of this - stripping "[0]" already reaches the root.
void RecordArrayOfArraysElementLocations(const String& name, const std::vector<int>& dimensions,
const long long baseLocation,
UnorderedMap<String, Int>& locations) {
if (dimensions.size() < 2) return;
// A pathological declaration must not be able to blow up the map; past the cap
// only the root entry stands, which is what every case used to get.
constexpr long long kMaxSynthesizedKeys = 4096;
const long long innerSpan = dimensions.back();
const SizeT outerDimensions = dimensions.size() - 1;
long long elementCount = 1;
for (SizeT d = 0; d < outerDimensions; ++d) {
elementCount *= dimensions[d];
if (elementCount > kMaxSynthesizedKeys) return;
}
for (long long element = 0; element < elementCount; ++element) {
String key = name;
long long remainder = element;
for (SizeT d = 0; d < outerDimensions; ++d) {
long long stride = 1;
for (SizeT inner = d + 1; inner < outerDimensions; ++inner) stride *= dimensions[inner];
key += "[" + std::to_string(remainder / stride) + "]";
remainder %= stride;
}
locations.emplace(key, static_cast<Int>(std::min(baseLocation + element * innerSpan,
static_cast<long long>(INT_MAX / 2))));
}
}
} // namespace
UnorderedMap<String, Int> CollectExplicitUniformLocations(const glslang::TShader& shader) {
UnorderedMap<String, Int> locations;
const glslang::TIntermediate* intermediate = shader.getIntermediate();
if (intermediate == nullptr) return locations;
// Half one: the uniforms the relaxed remap swallowed, out of the snapshot it
// takes on the way past.
for (const glslang::TIntermediate::TUniformLocation& record :
intermediate->getUniformLocations()) {
if (record.location < 0) continue;
// Keep the first sighting. Two records for one name mean the parser saw the
// declaration twice, and the first is the one the symbol table kept.
locations.emplace(record.name, record.location);
RecordArrayOfArraysElementLocations(record.name, record.arraySizes, record.location,
locations);
}
// Half two: the OPAQUE uniforms, which the remap never touches (the guard in
// vkRelaxedRemapUniformVariable admits only types containing something
// non-opaque, atomic_uint, or a sampler inside a struct) and which therefore
// still carry their qualifier here.
//
// They belong in the same map even though reflection could also answer for them,
// and the distinction is not cosmetic: this map is what marks a location as
// SOURCE-EXPLICIT, i.e. API contract under ARB_explicit_uniform_location. A
// location that only reaches DoReflection through glslang's own layoutLocation()
// is treated as implementation-chosen and quietly moved on a collision, which is
// the wrong answer for one the shader declared.
//
// Read BEFORE any link: mapIO writes its own choices into these same qualifiers
// (iomapper.cpp:240), so this is only truthful while the shader is unlinked -
// which is exactly where ShaderCompileTask calls it.
const glslang::TIntermAggregate* linkerObjects = intermediate->findLinkerObjects();
if (linkerObjects == nullptr) return locations;
for (TIntermNode* node : linkerObjects->getSequence()) {
const glslang::TIntermSymbol* symbol = node ? node->getAsSymbolNode() : nullptr;
if (symbol == nullptr) continue;
const glslang::TType& type = symbol->getType();
const glslang::TQualifier& qualifier = type.getQualifier();
if (qualifier.storage != glslang::EvqUniform || !qualifier.hasLocation()) continue;
// A BLOCK has no glGetUniformLocation of its own, and its members are
// addressed through the block. Only loose uniforms take locations.
if (type.getBasicType() == glslang::EbtBlock || type.isBuiltIn()) continue;
std::vector<int> arraySizes;
if (type.isArray() && type.getArraySizes() != nullptr) {
const glslang::TArraySizes& sizes = *type.getArraySizes();
for (int dim = 0; dim < sizes.getNumDims(); ++dim) {
arraySizes.push_back(sizes.getDimSize(dim));
}
}
const String name = symbol->getAccessName().c_str();
const Int location = static_cast<Int>(qualifier.layoutLocation);
locations.emplace(name, location);
RecordArrayOfArraysElementLocations(name, arraySizes, location, locations);
}
return locations;
}
Result<SharedPtr<glslang::TProgram>> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) {
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
for (auto& s : attrib.shaders) {
@@ -383,7 +484,8 @@ namespace MobileGL {
MakeUnique<TMglGlslIoResolver>(*program, (EShLanguage)stage, attrib.explicitVertexInLocations,
attrib.explicitFragmentOutLocations,
attrib.explicitFragmentOutIndices,
attrib.explicitOpaqueUniformBindings);
attrib.explicitOpaqueUniformBindings,
attrib.storageBlocksWithoutBinding);
break;
}
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
@@ -445,6 +445,24 @@ namespace MobileGL {
// identical question for the identical decision.
static Bool ModuleReadsLocatedInput(const Vector<Uint32>& spirv);
};
// The explicit layout(location = N) qualifiers this shader's DEFAULT-BLOCK uniforms
// declared, keyed the way glslang's own reflection will later spell them.
//
// They cannot be read back off the parsed module, and that is not an oversight of
// this function: MobileGL parses every shader as a Vulkan client under relaxed
// rules, which sweeps plain uniforms into MGL_GLOBAL_UBO - where a location
// qualifier has no meaning - and DROPS the qualifier on the way past
// (ParseHelper.cpp vkRelaxedRemapUniformVariable). What this reads is the snapshot
// glslang takes at that exact site, handed over through TIntermediate; the GL
// location assigner in ProgramLinkTask::DoReflection is the only party left that
// can honour the number.
//
// Keyed by declared name (no "[0]" suffix), plus one synthesized key per outer
// index of an array-of-arrays - see the note in the implementation for why
// reflection needs those spelled out. A uniform declared in several stages must
// agree, which the caller enforces across stages.
UnorderedMap<String, Int> CollectExplicitUniformLocations(const glslang::TShader& shader);
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -1621,22 +1621,6 @@ namespace MobileGL {
}
namespace {
// The layout qualifiers a shader storage block may legally carry WITHOUT a
// value. GLSL 4.60 4.4.5: a buffer block's layout list holds the memory-layout
// and matrix-order identifiers below, plus binding/offset/align, which are
// spelled `name = value` and so never reach this test. Anything else bare in
// such a list is not GLSL - on the unexpanded text this scanner reads, it is a
// macro, and a macro may expand to the binding itself.
bool IsBufferBlockLayoutIdentifier(const String& text) {
static const char* kLayoutIdentifiers[] = {
"shared", "packed", "std140", "std430", "row_major", "column_major",
};
for (const char* identifier : kLayoutIdentifiers) {
if (text == identifier) return true;
}
return false;
}
bool IsNonLayoutQualifierKeyword(const String& text) {
static const char* kQualifiers[] = {
"highp", "mediump", "lowp", "precise", "const", "flat",
@@ -1669,266 +1653,8 @@ namespace MobileGL {
return true;
}
// glslang reflects an array-of-arrays default-block uniform as ONE RECORD PER
// outer-index tuple, carrying the innermost array type: `float u[2][3]` becomes
// "u[0][0]" and "u[1][0]" (that last "[0]" is EShReflectionBasicArraySuffix). The
// linker resolves such a name by stripping the single trailing "[0]", so it looks
// up "u[1]" - a key the root entry alone cannot answer, and the whole declaration
// silently loses its explicit location.
//
// Emit those pre-flattened keys here, next to the root, so the result is
// order-independent: each carries the location its own element starts at (element
// i of `float u[2][3]` at location L starts at L + i*3). Identifiers cannot
// contain brackets, so a synthesized key never collides with a real uniform name,
// and a 1-D array needs none of this - stripping "[0]" already reaches the root.
void RecordArrayOfArraysElementLocations(const String& name, const Vector<long long>& dimensions,
long long baseLocation,
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
if (dimensions.size() < 2) return;
// A pathological declaration must not be able to blow up the map; past the cap
// only the root entry stands, which is what every case used to get.
constexpr long long kMaxSynthesizedKeys = 4096;
const long long innerSpan = dimensions.back();
const SizeT outerDimensions = dimensions.size() - 1;
long long elementCount = 1;
for (SizeT d = 0; d < outerDimensions; ++d) {
elementCount *= dimensions[d];
if (elementCount > kMaxSynthesizedKeys) return;
}
for (long long element = 0; element < elementCount; ++element) {
String key = name;
long long remainder = element;
for (SizeT d = 0; d < outerDimensions; ++d) {
long long stride = 1;
for (SizeT inner = d + 1; inner < outerDimensions; ++inner) stride *= dimensions[inner];
key += "[" + std::to_string(remainder / stride) + "]";
remainder %= stride;
}
locations.emplace(key, static_cast<MobileGL::Int>(
std::min(baseLocation + element * innerSpan,
static_cast<long long>(INT_MAX / 2))));
}
}
// Parses one brace-free depth-0 statement [begin, end) and records its
// declarators when it is a uniform declaration carrying an integral
// layout(location = N). Multi-declarator statements assign consecutive
// locations, each declarator advancing by its array element count
// (ARB_explicit_uniform_location rules). Anything the narrow grammar does
// not recognize is skipped, never guessed at.
void RecordUniformDeclarationLocations(const Vector<CodeToken>& tokens, SizeT begin, SizeT end,
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
using MobileGL::Int;
long long location = -1;
long long literal = 0;
bool sawUniform = false;
SizeT declaratorBegin = end;
for (SizeT k = begin; k < end;) {
const String& text = tokens[k].text;
if (text == "layout" && k + 1 < end && tokens[k + 1].text == "(") {
SizeT j = k + 2;
Int parenDepth = 1;
while (j < end && parenDepth > 0) {
const String& layoutToken = tokens[j].text;
if (layoutToken == "(") {
++parenDepth;
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end &&
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
location = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
}
k = j;
continue;
}
if (text == "uniform") {
sawUniform = true;
++k;
continue;
}
if (sawUniform && location >= 0 && IsIdentifierToken(tokens[k]) &&
!IsNonLayoutQualifierKeyword(text)) {
declaratorBegin = k + 1; // 'text' is the type; declarators follow
break;
}
++k;
}
if (!sawUniform || location < 0 || declaratorBegin >= end) return;
long long nextLocation = location;
for (SizeT k = declaratorBegin; k < end;) {
if (!IsIdentifierToken(tokens[k])) return; // malformed; record nothing further
const String& name = tokens[k].text;
++k;
long long span = 1;
Vector<long long> dimensions;
while (k < end && tokens[k].text == "[") {
++k;
long long dimension = 1;
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) {
dimension = literal;
++k;
}
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k;
dimensions.push_back(
std::max(1ll, std::min(dimension, static_cast<long long>(INT_MAX / 2))));
span *= dimensions.back();
}
// Keep the first sighting: a duplicate can only come from alternative
// preprocessor branches declaring the same name.
locations.emplace(name, static_cast<Int>(std::min(
nextLocation, static_cast<long long>(INT_MAX / 2))));
RecordArrayOfArraysElementLocations(name, dimensions, nextLocation, locations);
nextLocation += span;
if (k >= end) break;
if (tokens[k].text == "=") { // skip an initializer up to the declarator comma
Int nestingDepth = 0;
++k;
while (k < end) {
const String& initializerToken = tokens[k].text;
if (initializerToken == "(" || initializerToken == "[") {
++nestingDepth;
} else if (initializerToken == ")" || initializerToken == "]") {
--nestingDepth;
} else if (initializerToken == "," && nestingDepth == 0) {
break;
}
++k;
}
}
if (k >= end) break;
if (tokens[k].text != ",") return;
++k;
}
}
// Parses one brace-free depth-0 statement [begin, end) and records its
// declarators when it is a sampler/image uniform declaration carrying an
// integral layout(binding = N). Such a binding is a GL texture/image unit,
// which the Vulkan-client relaxed parse strips before mapIO can observe it
// (it is not a valid descriptor binding there), so it is extracted lexically
// and restored as the uniform's initial unit. Every declarator in the
// statement shares the qualifier's binding, matching what the GL-client
// mapIO used to capture from the shared type qualifier. Anything the narrow
// grammar does not recognize is skipped, never guessed at.
void RecordOpaqueDeclarationBindings(const Vector<CodeToken>& tokens, SizeT begin, SizeT end,
MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) {
using MobileGL::Int;
long long binding = -1;
long long literal = 0;
bool sawUniform = false;
SizeT declaratorBegin = end;
for (SizeT k = begin; k < end;) {
const String& text = tokens[k].text;
if (text == "layout" && k + 1 < end && tokens[k + 1].text == "(") {
SizeT j = k + 2;
Int parenDepth = 1;
while (j < end && parenDepth > 0) {
const String& layoutToken = tokens[j].text;
if (layoutToken == "(") {
++parenDepth;
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end &&
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
}
k = j;
continue;
}
if (text == "uniform") {
sawUniform = true;
++k;
continue;
}
if (sawUniform && binding >= 0 && IsIdentifierToken(tokens[k]) &&
!IsNonLayoutQualifierKeyword(text)) {
// 'text' is the type. Only sampler/image opaques carry unit
// bindings; on anything else (e.g. atomic_uint, whose binding
// is a counter-buffer index) record nothing.
if (text.find("sampler") == String::npos && text.find("image") == String::npos) return;
declaratorBegin = k + 1;
break;
}
++k;
}
if (!sawUniform || binding < 0 || declaratorBegin >= end) return;
for (SizeT k = declaratorBegin; k < end;) {
if (!IsIdentifierToken(tokens[k])) return; // malformed; record nothing further
const String& name = tokens[k].text;
++k;
while (k < end && tokens[k].text == "[") {
++k;
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) ++k;
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k;
}
bindings[name] = static_cast<MobileGL::Uint>(binding);
if (k >= end) break;
if (tokens[k].text != ",") return; // opaque declarators cannot take initializers
++k;
}
}
} // namespace
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source) {
UnorderedMap<String, Uint> bindings;
// Fast path: without the qualifier keyword there is nothing to extract.
if (source.find("binding") == String::npos) return bindings;
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
Int braceDepth = 0;
SizeT pos = 0;
while (pos < count) {
const String& text = tokens[pos].text;
if (text == "{") {
++braceDepth;
++pos;
continue;
}
if (text == "}") {
if (braceDepth > 0) --braceDepth;
++pos;
continue;
}
if (braceDepth != 0 || text == ";") {
++pos;
continue;
}
// A depth-0 statement runs to its ';'. One that opens a brace instead is
// a function definition or an interface/uniform block: a block's binding
// is a buffer binding point, not a texture unit, so skip both alike.
SizeT statementEnd = pos;
while (statementEnd < count && tokens[statementEnd].text != ";" &&
tokens[statementEnd].text != "{") {
++statementEnd;
}
if (statementEnd >= count || tokens[statementEnd].text == "{") {
pos = statementEnd;
continue;
}
RecordOpaqueDeclarationBindings(tokens, pos, statementEnd, bindings);
pos = statementEnd + 1;
}
return bindings;
}
namespace {
// Binding points a storage-block declaration starting at `bufferPos` occupies.
// One for a scalar instance (and for the "layout(...) buffer;" default-qualifier
@@ -1967,129 +1693,6 @@ 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;
// Set when the run carries something this scanner cannot read as a binding but
// which MAY BE ONE. THE TEXT SCANNED HERE IS NOT MACRO-EXPANDED - MobileGL's
// preprocessing rewrites the source, it does not run the C preprocessor, so
// `#define`s and their uses both survive into it. `binding = SOME_MACRO` is
// therefore the common spelling in real shader packs, not an exotic one
// (Flywheel's indirect engine writes every one of its blocks that way), and
// reading "no literal" as "no binding" DEFAULTS AWAY a binding the shader
// really declared: every such block would be seeded to 0 and alias there. Doubt
// is resolved by dropping the name, which restores the pre-seeding behaviour
// for exactly the declarations this scanner cannot read.
bool unreadableQualifier = false;
long long literal = 0;
const auto endRun = [&binding, &unreadableQualifier]() {
binding = -1;
unreadableQualifier = false;
};
for (SizeT pos = 0; pos < count; ++pos) {
const String& text = tokens[pos].text;
if (text == "{") {
++braceDepth;
endRun();
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) {
// Nested parentheses belong to some entry's value expression,
// which is not a literal - the entry itself is judged below.
} else if (layoutToken == "binding" && j + 2 < count && tokens[j + 1].text == "=") {
if (ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
} else {
// A binding IS declared here; its value is just spelled as
// something this scanner does not evaluate (a macro, a
// const, an expression). The one thing it certainly is not
// is absent.
unreadableQualifier = true;
}
j += 2;
} else if (IsIdentifierToken(tokens[j]) && !IsBufferBlockLayoutIdentifier(layoutToken) &&
!(j + 1 < count && tokens[j + 1].text == "=")) {
// A bare identifier that is none of the layout qualifiers a
// buffer block may legally carry. On unexpanded text that is a
// macro, and a macro may well be the binding
// (`layout(std430, MY_BINDING) buffer B {...}`).
unreadableQualifier = true;
}
++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 == "{") {
// The token immediately before `buffer` is the last of the run. A
// non-keyword identifier there is a macro standing in for the whole
// qualifier list (`SSBO_QUALIFIER buffer B {...}`), so it may carry
// the binding just as an unreadable layout entry may.
const bool macroQualifier = pos > 0 && IsIdentifierToken(tokens[pos - 1]) &&
!IsNonLayoutQualifierKeyword(tokens[pos - 1].text);
const bool unqualified = binding < 0 && !unreadableQualifier && !macroQualifier;
(unqualified ? names : qualified).insert(tokens[pos + 1].text);
}
endRun();
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.
// Ending the run clears the doubt with it: the unreadable qualifier belonged
// to the declaration that just ended, not to whatever follows.
if (!IsNonLayoutQualifierKeyword(text)) endRun();
}
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;
@@ -2102,8 +1705,8 @@ namespace MobileGL {
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
// The binding the qualifier run currently being scanned declared, -1 for none.
// Several layout(...) lists may precede one declaration and the later one wins,
// which is the same accumulate-then-consume shape the extractors above use.
// Several layout(...) lists may precede one declaration and the later one wins:
// accumulate, then consume at the `buffer` keyword.
long long binding = -1;
long long literal = 0;
for (SizeT pos = 0; pos < count; ++pos) {
@@ -2146,137 +1749,6 @@ namespace MobileGL {
return std::nullopt;
}
std::optional<String> FindAtomicCounterOffsetViolation(const String& source) {
// Fast path: both keywords are required for a violation to exist, and the pair is
// absent from every shader-pack source.
if (source.find("atomic_uint") == String::npos || source.find("offset") == String::npos) {
return std::nullopt;
}
constexpr long long kAtomicCounterSize = 4; // one 32-bit word per counter
const long long maxBufferSize = static_cast<long long>(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
// The offset the qualifier run currently being scanned declared, -1 for none.
// Same accumulate-then-consume shape as the storage-binding scan above.
long long offset = -1;
long long literal = 0;
for (SizeT pos = 0; pos < count; ++pos) {
const String& text = tokens[pos].text;
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 == "offset" && j + 2 < count &&
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
offset = literal;
j += 2;
}
++j;
}
pos = j - 1;
continue;
}
if (text == "atomic_uint") {
// How far the declaration reaches: `atomic_uint c[N]` occupies N words
// from the offset. An unparsable or absent declarator (an expression-sized
// array, or the "layout(...) uniform atomic_uint;" default-qualifier form,
// which declares no counter at all) is left alone rather than guessed at -
// over-rejection here would be a compile failure the application cannot
// work around.
long long elements = 1;
SizeT k = pos + 1;
if (k < count && IsIdentifierToken(tokens[k])) {
++k;
if (k < count && tokens[k].text == "[") {
elements = (k + 2 < count && tokens[k + 2].text == "]" &&
ParseGlslIntegerLiteral(tokens[k + 1].text, literal))
? std::max<long long>(1, literal)
: -1;
}
} else {
elements = -1;
}
// Clamped so the byte arithmetic below cannot overflow on an absurd
// literal; any element count at or past the ceiling already fails.
elements = std::min(elements, maxBufferSize);
if (offset >= 0 && elements > 0) {
if (offset % kAtomicCounterSize != 0) {
return "ERROR: invalid value " + std::to_string(offset) +
" for layout specifier 'offset': an atomic counter offset must be a "
"multiple of 4.";
}
if (offset > maxBufferSize - elements * kAtomicCounterSize) {
return "ERROR: invalid value " + std::to_string(offset) +
" for layout specifier 'offset': an atomic counter ending at byte " +
std::to_string(offset + elements * kAtomicCounterSize) +
" passes GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE (" +
std::to_string(maxBufferSize) + ").";
}
}
offset = -1;
continue;
}
// `uniform` and the precision/auxiliary qualifiers may sit between the layout
// list and the type keyword; anything else ends the run, so an offset never
// leaks onto an unrelated declaration.
if (text != "uniform" && !IsNonLayoutQualifierKeyword(text)) offset = -1;
}
return std::nullopt;
}
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source) {
UnorderedMap<String, Int> locations;
// Fast path: without the qualifier keyword there is nothing to extract.
if (source.find("location") == String::npos) return locations;
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
Int braceDepth = 0;
SizeT pos = 0;
while (pos < count) {
const String& text = tokens[pos].text;
if (text == "{") {
++braceDepth;
++pos;
continue;
}
if (text == "}") {
if (braceDepth > 0) --braceDepth;
++pos;
continue;
}
if (braceDepth != 0 || text == ";") {
++pos;
continue;
}
// A depth-0 statement runs to its ';'. One that opens a brace instead is a
// function definition or an interface/uniform block: neither can declare a
// default-block uniform location, so hand the '{' back to the depth tracker.
SizeT statementEnd = pos;
while (statementEnd < count && tokens[statementEnd].text != ";" &&
tokens[statementEnd].text != "{") {
++statementEnd;
}
if (statementEnd >= count || tokens[statementEnd].text == "{") {
pos = statementEnd;
continue;
}
RecordUniformDeclarationLocations(tokens, pos, statementEnd, locations);
pos = statementEnd + 1;
}
return locations;
}
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -45,68 +45,44 @@ namespace MobileGL {
// compile-error text for the first violation, or nullopt for a clean source.
std::optional<String> FindReservedIdentifierViolation(const String& source);
// Explicit layout(location = N) qualifiers on default-block uniform declarations,
// keyed by declared name (no "[0]" suffix). Multi-declarator statements assign
// consecutive locations, advancing by the array element count.
//
// Exists because the single link-compatible parse runs under relaxed Vulkan rules,
// where glslang's vkRelaxedRemapUniformVariable moves plain uniforms into
// MGL_GLOBAL_UBO and DISCARDS their location qualifiers ("ignoring layout qualifier
// for uniform location"); opaque uniforms keep theirs. This lexical side-channel
// restores the discarded locations to the GL location assigner
// (ProgramObject::DoReflection). It scans preprocessor-visible text, so a
// declaration inside an inactive #if branch is still recorded - harmless unless a
// pack declares the same uniform with different explicit locations in alternative
// branches (none observed; explicit uniform locations have zero incidence in the
// shader-pack corpus, this is an ARB_explicit_uniform_location conformance surface).
UnorderedMap<String, Int> ExtractExplicitUniformLocations(const String& source);
// Explicit layout(binding = N) on sampler/image uniforms, i.e. their initial
// texture/image units. The Vulkan-client relaxed parse strips these before
// mapIO can capture them, so they are recovered lexically (same narrow
// 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);
// NO SIDE-CHANNEL EXTRACTORS LIVE HERE ANY MORE. Three of them did - explicit
// default-block uniform locations, explicit sampler/image bindings, and the storage
// blocks that declared no binding - each recovering something MobileGL's
// Vulkan-client relaxed parse destroys. All three are now taken from glslang at the
// point of destruction instead:
// * uniform locations: a snapshot inside vkRelaxedRemapUniformVariable, read back
// through CollectExplicitUniformLocations (ShaderCompiler.h);
// * opaque bindings and unqualified storage blocks:
// TMglGlslIoResolver::reserverResourceSlot, which mapIO calls while the
// qualifier still says what the shader declared.
// The rewrites below stay lexical by construction - they exist to make glslang
// ACCEPT input it would otherwise reject, so they cannot be built on its parse.
// 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
// has to fit. glslang cannot raise it for MobileGL: every shader is parsed as a Vulkan
// client under relaxed rules, where the GL ceilings do not apply, and TBuiltInResource
// has no storage-buffer binding field to check against in the first place. Returns the
// compile-error text for the first violation, or nullopt for a clean source.
// `maxBindings` is what glGetIntegerv answers for that pname; a non-positive value
// means "nothing to check against" and every declaration passes.
// has to fit. Returns the compile-error text for the first violation, or nullopt for
// a clean source. `maxBindings` is what glGetIntegerv answers for that pname; a
// non-positive value means "nothing to check against" and every declaration passes.
//
// THE ONE SCAN THAT COULD NOT MOVE TO GLSLANG, and the reason is structural rather
// than a matter of where the check is written. glslang has no resource limit for this
// ceiling at all - Include/ResourceLimits.h carries maxAtomicCounterBindings,
// maxCombinedTextureImageUnits and forty others, but nothing for uniform-block or
// storage-block binding points - so there is no number for a parse-time check to
// compare against, and the relaxed Vulkan rules MobileGL parses under would exempt it
// anyway (ParseHelper.cpp layoutTypeCheck gates its binding ceilings on
// `spvVersion.vulkan == 0`). Reading the AST post-parse from MobileGL is possible and
// would be strictly better - a macro-spelled binding would finally be checked - but
// the limit is a per-device number that CompileEnv deliberately keeps OUT of
// frontendFingerprint (see its classification), so the L1c parse-verdict key would
// have to grow it before any such verdict could be memoized. That is a cache-key
// change in exchange for a new REJECTION surface, which is the one direction that
// cannot be validated without device time.
//
// Consequence, and it is deliberate: a binding this scanner cannot read as a literal
// is not judged. Under-rejection, never over-rejection.
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings);
// GL 4.6 core 7.7 / ARB_shader_atomic_counters makes it a COMPILE-time error to
// declare an atomic counter at an offset that is not a multiple of 4, or whose last
// byte passes GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE. glslang enforces both in fixOffset(),
// which the Vulkan-relaxed parse never reaches (vkRelaxedRemapUniformVariable folds
// the atomic_uint into a synthesized storage block and returns from declareVariable()
// first), so MobileGL only caught them at LINK - and
// KHR-GL43.shader_atomic_counters.negative-offset-1 never links at all. The
// cross-stage rule (two counters sharing a binding must not overlap) stays at link:
// a single-stage source cannot see it. Returns the compile-error text for the first
// violation, or nullopt for a clean source.
std::optional<String> FindAtomicCounterOffsetViolation(const String& source);
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -26,7 +26,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// 2: L2 gained atomicCounterEsslBindingTop (wave3's atomic-counter block rebinding
// prints it into the emitted ESSL), and L1c was added.
// 3: L2 gained the two interface-block rename maps (wave4's UniquifyIoBlockNames).
constexpr Uint32 kKeyLayoutVersion = 3u;
// 4: the glslang-capture migration. L1 DROPPED explicitOpaqueUniformBindings from its
// key (that map is an output of mapIO, not an input to it), and L1c's PAYLOAD gained
// the explicit uniform locations - so a blob written under 3 describes a differently
// shaped answer at both levels even where the bytes would have matched.
constexpr Uint32 kKeyLayoutVersion = 4u;
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
@@ -128,7 +132,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
builder.NameMap(inputs.explicitVertexInLocations ? *inputs.explicitVertexInLocations : kEmpty);
builder.NameMap(inputs.explicitFragmentOutLocations ? *inputs.explicitFragmentOutLocations : kEmpty);
builder.NameMap(inputs.explicitFragmentOutIndices ? *inputs.explicitFragmentOutIndices : kEmpty);
builder.NameMap(inputs.explicitOpaqueUniformBindings ? *inputs.explicitOpaqueUniformBindings : kEmpty);
static const Vector<String> kNoXfb;
builder.TextList(inputs.requestedXfbVaryings ? *inputs.requestedXfbVaryings : kNoXfb);
builder.Value(inputs.xfbBufferMode);
@@ -146,7 +149,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
return MakeTranslationCacheKey(builder);
}
SizeT ShaderParseVerdictBytes(const ShaderParseVerdict& verdict) { return verdict.infoLog.size(); }
SizeT ShaderParseVerdictBytes(const ShaderParseVerdict& verdict) {
SizeT bytes = verdict.infoLog.size();
for (const auto& [name, location] : verdict.explicitUniformLocations) {
bytes += name.size() + sizeof(Int);
}
return bytes;
}
// Leaked for the same exit-order reason as the other two; see the note below.
BoundedTranslationCache<ShaderParseVerdict>& GetShaderParseVerdictCache() {
@@ -365,11 +365,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// covers backend identity and the advertised extension vector;
// * per stage, in link order: the GL stage enum and the FULL preprocessed
// source, which is literally the text ParseShaderSource was given;
// * the four link-time request maps mapIO resolves against
// * the three link-time request maps mapIO resolves against
// (glBindAttribLocation / glBindFragDataLocation /
// glBindFragDataLocationIndexed, and the merged layout(binding=) opaque
// units) - these steer TMglGlslIoResolver and therefore the Locations and
// Bindings baked into every module;
// glBindFragDataLocationIndexed) - these steer TMglGlslIoResolver and
// therefore the Locations and Bindings baked into every module. NOT the
// merged layout(binding=) opaque units, which used to sit here: they are
// an OUTPUT of mapIO (TMglGlslIoResolver writes that map and never reads
// it), so they are a pure function of the stage sources already in this
// key and keying on them discriminated nothing;
// * the ShaderCompileBits the parse ran under (always 0 in production; in
// the key so a future non-zero value cannot alias);
// * the SPIR-V validation switch (byte-identical output either way, but it
@@ -393,7 +396,6 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
const UnorderedMap<String, Uint>* explicitVertexInLocations = nullptr;
const UnorderedMap<String, Uint>* explicitFragmentOutLocations = nullptr;
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
const UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
Uint32 shaderCompileFlags = 0;
Bool enableSpirvValidation = false;
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
@@ -476,6 +478,16 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
// successful parse, so a successful compile's observable log is empty no matter what
// glslang wrote into it. Stored rather than assumed so the two cannot drift.
String infoLog;
// The explicit default-block uniform locations the parse recovered
// (CollectExplicitUniformLocations), empty when `parsed` is false.
//
// IN THE PAYLOAD BECAUSE A HIT SKIPS THE PARSE. These used to come from a lexical scan
// of the source, which ran in the half a hit still executes; they now come from the
// glslang snapshot, which a hit never produces. They belong to the same key as the
// verdict itself - a pure function of (front-end env, stage, preprocessed source) - so
// no key widening is needed, only this field. Without it an L1c hit would publish a
// shader with no explicit locations at all and the program would first-fit them from 0.
UnorderedMap<String, Int> explicitUniformLocations;
};
using ShaderParseVerdictPtr = SharedPtr<const ShaderParseVerdict>;
@@ -69,7 +69,13 @@ namespace MobileGL {
// Dual-source blend color index per fragment output (glBindFragDataLocationIndexed) ->
// emitted as layout(index = N).
UnorderedMap<String, Uint> explicitFragmentOutIndices;
// ---- OUT parameters, written by TMglGlslIoResolver during mapIO ----
// Neither is an input: the resolver only ever writes them. They exist because
// the IO mapper's collect callback is the last point at which a resource's
// qualifier still says what the SHADER declared rather than what glslang
// assigned - see the comment on TMglGlslIoResolver::reserverResourceSlot.
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
std::set<String>* storageBlocksWithoutBinding = nullptr;
};
struct ProgramBinaryAttrib {
@@ -12,6 +12,10 @@
#include "TMglGlslIoResolver.h"
#include <cstring>
#include <MG_Util/ShaderTranspiler/Types.h>
namespace MobileGL {
bool TMglGlslIoResolver::ShouldAssignPlainUniformLocation(const glslang::TType& type) const {
if (!doAutoLocationMapping()) {
@@ -149,12 +153,47 @@ namespace MobileGL {
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
}
// THE COLLECT CALLBACK IS THE CAPTURE POINT, and the reason is a matter of ten lines of
// glslang. mapIO gathers every declared symbol of every stage and calls this on each of
// them (iomapper.cpp addStage -> TSlotCollector) BEFORE it resolves anything; only
// afterwards, in doMap(), does it write the slots it chose back into the types
// (iomapper.cpp:240, `layoutBinding = at->second.newBinding`). Up to here
// `qualifier.hasBinding()` still answers "did the SHADER say so?"; past it, every resource
// carries a number and the question can no longer be asked at all.
//
// Both captures below used to be lexical scans of the shader source, which had to run
// before the preprocessor's macros were expanded and therefore could not read
// `binding = SOME_MACRO` - the spelling Flywheel's indirect engine uses for every one of
// its storage blocks. Asking the AST instead makes the macro case ordinary.
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
const glslang::TType& type = ent.symbol->getType();
const glslang::TQualifier& qualifier = type.getQualifier();
// getAccessName() is the BLOCK TYPE name for a block and the declared name for
// everything else (IntermTraverse.cpp TIntermSymbol::getAccessName) - which is exactly
// the key both consumers want.
const glslang::TString& name = ent.symbol->getAccessName();
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
type.getQualifier().hasBinding()) {
const glslang::TString& name = ent.symbol->getAccessName();
(*m_explicitOpaqueUniformBindings)[name.c_str()] = type.getQualifier().layoutBinding;
qualifier.hasBinding()) {
(*m_explicitOpaqueUniformBindings)[name.c_str()] = qualifier.layoutBinding;
}
// A storage block that declared no binding. UNION across stages by construction - one
// resolver serves the whole program - which is what GLSL's "every stage must declare
// the same block identically" rule makes correct.
//
// NOT the atomic-counter blocks glslang SYNTHESIZES, which are storage blocks by every
// structural test available here and are still not what this set means. Relaxed parsing
// folds each atomic_uint into a "gl_AtomicCounterBlock_<GL binding>" block
// (ParseContextBase::growAtomicCounterBlock) and leaves it unbound because MobileGL asks
// for auto-mapped bindings - so it arrives looking exactly like an unqualified
// application block. Seeding one to GL binding 0 would overwrite the counter buffer's
// real binding, which is the trailing number in that very name.
if (m_storageBlocksWithoutBinding != nullptr && type.getBasicType() == glslang::EbtBlock &&
qualifier.storage == glslang::EvqBuffer && !qualifier.hasBinding() &&
name.compare(0, std::strlen(MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX),
MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX) != 0) {
m_storageBlocksWithoutBinding->insert(name.c_str());
}
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
@@ -12,6 +12,7 @@
#pragma once
#include <set>
#include <vector>
#include <unordered_map>
#include <glslang/Public/ShaderLang.h>
@@ -27,14 +28,17 @@ namespace MobileGL {
using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
ExplicitVarSlotMap* opaqueUniformBindings)
ExplicitVarSlotMap* opaqueUniformBindings,
std::set<String>* storageBlocksWithoutBinding = nullptr)
: TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts),
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings) {}
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings),
m_storageBlocksWithoutBinding(storageBlocksWithoutBinding) {}
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings)
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings,
std::set<String>* storageBlocksWithoutBinding = nullptr)
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices,
opaqueUniformBindings) {}
opaqueUniformBindings, storageBlocksWithoutBinding) {}
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
@@ -47,7 +51,17 @@ namespace MobileGL {
const ExplicitVarSlotMap& m_explicitVertexIns;
const ExplicitVarSlotMap& m_explicitFragOuts;
const ExplicitVarSlotMap& m_explicitFragOutIndices;
// Two OUT channels, both filled from reserverResourceSlot and never read back by this
// resolver. They exist because the collect callback is the LAST place the shader's own
// declaration is still legible: ten lines later (iomapper.cpp:240) mapIO writes its
// auto-assigned binding into the very qualifier that says whether the shader declared
// one. Anything downstream that needs "as DECLARED" rather than "as ASSIGNED" has to be
// handed it from here.
ExplicitVarSlotMap* m_explicitOpaqueUniformBindings = nullptr;
// Block TYPE names of the shader storage blocks that reached mapIO carrying NO
// layout(binding = N). GL 4.3 core 7.8 gives such a block binding ZERO; see
// ProgramLinkTask::SeedDefaultStorageBlockBindings for what is done with them.
std::set<String>* m_storageBlocksWithoutBinding = nullptr;
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
std::map<glslang::TString, int> m_plainUniformLocationByName;
bool m_plainUniformLocationsAssigned = false;