[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
@@ -579,8 +579,9 @@ namespace MobileGL::MG_State::GLState {
if (!ValidateAttachedShaders()) return;
// The two merges below read the COMPILE snapshots only - no parsed shader - so they
// run before the L1 probe, which needs the merged opaque bindings in its key.
// Reads the COMPILE snapshots only - no parsed shader - so it runs before the L1
// probe: a conflicting explicit uniform location must fail the link whether or not
// the memo has an answer for this program's sources.
MergeShaderSideChannels();
if (!artifacts.infoLog.empty()) return; // a conflicting explicit uniform location
@@ -617,11 +618,16 @@ namespace MobileGL::MG_State::GLState {
}
}
// The last two are OUT parameters that mapIO fills, not requests it honours: 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, so both captures
// have to be taken from inside the link. See TMglGlslIoResolver::reserverResourceSlot.
ProgramAttrib attrib{.shaders = Move(shaders),
.explicitVertexInLocations = in.explicitAttribLocations,
.explicitFragmentOutLocations = in.explicitFragDataLocation,
.explicitFragmentOutIndices = in.explicitFragDataIndex,
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings};
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding};
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
auto result = ShaderCompiler::LinkProgram(attrib);
@@ -639,6 +645,7 @@ namespace MobileGL::MG_State::GLState {
return;
}
// A compute program must have a fixed local group size, and GL states that as a
// property of the PROGRAM: "at least one" of its compute shaders declares it (GL 4.6
// core 7.13 / GLSL 4.30 4.4.1.4). MobileGL used to answer that question per SHADER,
@@ -814,7 +821,6 @@ namespace MobileGL::MG_State::GLState {
keyInputs.explicitVertexInLocations = &in.explicitAttribLocations;
keyInputs.explicitFragmentOutLocations = &in.explicitFragDataLocation;
keyInputs.explicitFragmentOutIndices = &in.explicitFragDataIndex;
keyInputs.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings;
// In the key ONLY because the payload now carries the reflection: transform feedback
// is resolved by reading the linked intermediates and never perturbs the generated
// SPIR-V, but it does shape xfbVaryings / xfbStrides / xfbBufferMode /
@@ -826,17 +832,18 @@ namespace MobileGL::MG_State::GLState {
return BuildSpirvTranslationKey(keyInputs);
}
// The link rejections that need nothing but the compile snapshots. They run before the
// The one link rejection that needs nothing but the compile snapshots. It runs before the
// L1 memo is consulted, so a hit can never paper over a program that must fail to link.
// The two lexical side channels the relaxed parse cannot provide, merged across stages:
// explicit default-block uniform locations (which must agree, or the link fails) and
// sampler/image layout(binding = N) initial units. Reads the COMPILE snapshots only, so
// it is legal - and necessary - before any shader is parsed: the merged bindings are part
// of the L1 memo key.
//
// Only the explicit default-block uniform locations are merged here, and only because they
// are the one piece of relaxed-parse wreckage that has to be recovered at COMPILE time:
// the snapshot is taken inside the parse, so it is per-shader by construction, and the
// same uniform declared in several stages must agree or the program cannot link. The
// opaque bindings and the unqualified storage blocks used to be merged alongside them;
// both now arrive from mapIO during LinkProgram below, straight into `artifacts`, which is
// both later and strictly better informed - the IO mapper sees macro-expanded declarations
// and a per-shader lexer never could.
void ProgramLinkTask::MergeShaderSideChannels() {
// Merge the shaders' lexically extracted explicit uniform locations. The same
// uniform declared in several stages must agree on its location (config-A glslang
// enforced this at mapIO; the relaxed parse no longer sees the qualifiers).
for (const auto& shader : in.shaders) {
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
for (const auto& [name, location] : compiled.explicitUniformLocations) {
@@ -850,21 +857,7 @@ namespace MobileGL::MG_State::GLState {
return;
}
}
// Sampler/image layout(binding = N) initial units, likewise invisible to the
// relaxed parse. Stage order matches the old per-stage mapIO capture, so a
// name declared in several stages keeps the last stage's binding as before.
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());
}
}
// An L1 hit: the entire front end, published without constructing a TShader or a
@@ -1117,7 +1110,7 @@ namespace MobileGL::MG_State::GLState {
artifacts.activeUniformCount, tProgramUniformCount);
// Effective explicit location per TProgram uniform, from two sources:
// - the lexical side-channel for default-block uniforms - the relaxed parse
// - the parse-time snapshot for default-block uniforms - the relaxed parse
// dropped their layout(location = N) qualifiers when collecting them into
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
// - glslang's layoutLocation() for opaque uniforms, where the qualifier
@@ -1681,9 +1674,13 @@ namespace MobileGL::MG_State::GLState {
// 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 blocks are named by TMglGlslIoResolver at mapIO's collect callback, which runs over
// every declared block of every stage BEFORE the write-back above happens - so "declared no
// binding" is a fact read off the AST, not a guess made about the text. The lexical scanner
// this replaced could only report positively, dropping any declaration whose grammar it did
// not fully recognise, and could not read `binding = SOME_MACRO` at all (it ran on
// macro-unexpanded source, and reading "no literal" as "no binding" once aliased eight
// Flywheel storage blocks onto 0).
//
// 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
@@ -1205,9 +1205,10 @@ namespace MobileGL::MG_State::GLState {
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
// Per-link merged snapshot of the attached shaders' lexically extracted
// layout(location = N) default-block uniform qualifiers (the relaxed parse drops
// them from reflection; the DoReflection assigner restores them from here).
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
// shaders' default-block uniforms declared, as glslang recorded them at the point
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
// DoReflection assigner restores them from here).
UnorderedMap<String, Int> linkedExplicitUniformLocations;
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
// declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform
@@ -1232,6 +1233,10 @@ namespace MobileGL::MG_State::GLState {
Vector<Int> uniformIndexInTProgram;
// ditto. Will be set at glUniform1i
Vector<Int> uniformSamplerOrImageUnitIndex;
// Sampler/image layout(binding = N) initial texture/image units, captured by
// TMglGlslIoResolver at mapIO's collect callback - the last point at which the
// qualifier still says what the shader declared. An OUTPUT of the link, not an
// input to it: nothing supplies this map, the resolver fills it.
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
// Ordered by uniform block index
@@ -1256,9 +1261,10 @@ namespace MobileGL::MG_State::GLState {
// 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.
// layout(binding = N). Input to the seeding above; filled during mapIO by
// TMglGlslIoResolver, which is the last observer that can still tell a declared
// binding from an invented one - and, unlike the per-shader lexer this replaced,
// sees the declaration with its macros expanded.
std::set<String> storageBlocksWithoutBinding;
Uint activeUniformCount = 0;
@@ -154,10 +154,16 @@ namespace {
}
// The half of a compile that depends on nothing but the source text, the stage and the
// environment snapshot: preprocessing, the three lexical rejections, and the two lexical
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
// Deliberately free of any per-object state so the memo is sound.
// environment snapshot: preprocessing and the three lexical rejections. Split out so P0b
// layer 2 can memoize exactly this and nothing else - the glslang parse stays per-object
// because its TShader is consume-once. Deliberately free of any per-object state so the
// memo is sound.
//
// The side-channel EXTRACTIONS that used to live here are gone: what the relaxed parse
// destroys is now recovered from glslang itself, at the two points where it is destroyed
// (see ShaderCompileArtifacts::explicitUniformLocations and
// TMglGlslIoResolver::reserverResourceSlot). They could not stay here anyway - none of
// them is a function of the unexpanded source text, which is all this half can see.
//
// The compute local-size verdict reads `env` rather than the live backend, and
// env.fingerprint is part of the P0b cache key, so a memo can never be returned against
@@ -195,28 +201,21 @@ namespace {
return result;
}
if (const std::optional<String> counterOffsetError =
FindAtomicCounterOffsetViolation(result.preprocessedSource)) {
result.outcome = ShaderPreprocessOutcome::AtomicCounterOffsetRejected;
result.infoLog = *counterOffsetError;
return result;
}
// NO ATOMIC-COUNTER OFFSET SCAN HERE ANY MORE: glslang raises both rules itself now, at
// the site where its relaxed remap folds the counter into a synthesized block
// (ParseHelper.cpp atomicCounterOffsetCheck, called from vkRelaxedRemapUniformVariable).
// A violation is an ordinary parse failure, so it reaches GL through the same path every
// other compile error does - and, unlike a scan of unexpanded text, it sees an offset
// spelled as a macro or a const expression.
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
// what the backends' SPIR-V is generated from - there is no second, GL-client
// parse. The GL frontend semantics the relaxed parse cannot provide are restored
// on top: explicit default-block uniform locations through the lexical
// side-channels below, dead-uniform/global-UBO filtering in
// on top, all of them out of glslang: explicit default-block uniform locations from
// the snapshot the parse takes, opaque bindings and unqualified storage blocks from
// the IO mapper's collect callback, dead-uniform/global-UBO filtering in
// 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;
}
@@ -319,10 +318,14 @@ namespace MobileGL::MG_State::GLState {
Bool parsedOk = false;
String parseLog;
SharedPtr<glslang::TShader> parsedShader;
UnorderedMap<String, Int> explicitUniformLocations;
if (verdict) {
parsedOk = verdict->parsed;
parseLog = verdict->infoLog;
// From the verdict, not from a parse - see ShaderParseVerdict for why they had to
// move into the payload when their origin moved into glslang.
explicitUniformLocations = verdict->explicitUniformLocations;
MGLOG_D("ShaderCompileTask: shader %u (stage %d) L1c hit - the glslang parse was skipped; "
"compileStatus = %d",
externalIndex, static_cast<Int>(stage), static_cast<Int>(parsedOk));
@@ -335,6 +338,7 @@ namespace MobileGL::MG_State::GLState {
parsedOk = result.has_value();
if (parsedOk) {
parsedShader = result.value();
explicitUniformLocations = CollectExplicitUniformLocations(*parsedShader);
} else {
parseLog = result.error().log;
}
@@ -344,6 +348,7 @@ namespace MobileGL::MG_State::GLState {
// Empty on success by construction, matching what the publish below does with
// the artifacts' own log; the diagnostic the application reads on failure.
freshVerdict->infoLog = parseLog;
freshVerdict->explicitUniformLocations = explicitUniformLocations;
const SizeT verdictBytes = ShaderParseVerdictBytes(*freshVerdict);
GetShaderParseVerdictCache().Insert(parseKey, ShaderParseVerdictPtr(Move(freshVerdict)),
verdictBytes);
@@ -359,9 +364,7 @@ namespace MobileGL::MG_State::GLState {
// `fresh` is about to be handed to the cache. Populated on the hit path too - it
// is what ClaimParsedShader's deferred parse consumes.
artifacts.preprocessedSource = shared.preprocessedSource;
artifacts.explicitUniformLocations = shared.explicitUniformLocations;
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
artifacts.storageBlocksWithoutBinding = shared.storageBlocksWithoutBinding;
artifacts.explicitUniformLocations = Move(explicitUniformLocations);
artifacts.infoLog.clear();
if (shouldPopulateCache) {
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
@@ -386,9 +389,6 @@ namespace MobileGL::MG_State::GLState {
if (shouldPopulateCache) {
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
fresh->infoLog = artifacts.infoLog;
fresh->explicitUniformLocations.clear();
fresh->explicitOpaqueBindings.clear();
fresh->storageBlocksWithoutBinding.clear();
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
}
}
@@ -60,19 +60,23 @@ namespace MobileGL::MG_State::GLState {
// ClaimParsedShader's re-parse so a later link never depends on the preprocessor
// being deterministic across backend-state changes.
String preprocessedSource;
// The explicit layout(location = N) qualifiers this stage's default-block uniforms
// declared, as glslang recorded them at the point its Vulkan-relaxed remap dropped
// them (CollectExplicitUniformLocations).
//
// Populated on the L1c HIT path too, out of the cached verdict rather than out of a
// parse - which is why the verdict carries them. Everything else the relaxed parse
// destroys is recovered at LINK instead, from the IO mapper's collect callback, and so
// has no field here at all.
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;
};
// The unit of asynchronous shader compilation: one glCompileShader's worth of pure CPU
// work - preprocess, the two lexical rejections, the two lexical extractions, and (unless
// the translation memo's compile half already knows the answer) the glslang parse - with
// every input it needs owned by the node itself.
// work - preprocess, the lexical rejections, and (unless the translation memo's compile
// half already knows the answer) the glslang parse plus the explicit-uniform-location
// snapshot it yields - with every input it needs owned by the node itself.
//
// That ownership is the whole point. The node reads no GL-thread state (the source is a
// SharedPtr<const String> snapshot, the device limits come from the CompileEnv snapshot,
@@ -101,24 +101,11 @@ namespace MobileGL {
const SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
const String& GetInfoLog() const { return Compiled().infoLog; }
// Explicit layout(location = N) qualifiers on this shader's default-block
// uniforms, captured lexically at Compile() because the relaxed parse drops
// them from reflection (see ExtractExplicitUniformLocations).
// uniforms, as glslang recorded them at the point its Vulkan-relaxed remap
// discarded them (see CollectExplicitUniformLocations).
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
return Compiled().explicitUniformLocations;
}
// Explicit layout(binding = N) on sampler/image uniforms - their initial
// texture/image units - captured lexically for the same reason (see
// ExtractExplicitOpaqueBindings).
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; }
@@ -29,9 +29,6 @@ namespace MobileGL::MG_State::GLState {
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
ResourceBindingRejected,
// FindAtomicCounterOffsetViolation rejected it: an atomic counter declared a
// layout(offset =) that is misaligned or reaches past GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE.
AtomicCounterOffsetRejected,
// The source-only half was clean but glslang rejected the preprocessed source.
// Memoizing this saves the parse itself on every later object with that source.
ParseFailed,
@@ -39,18 +36,23 @@ namespace MobileGL::MG_State::GLState {
// Everything ShaderObject::Compile() derives from the source text alone, i.e.
// everything that is identical for two shader objects holding byte-identical source.
//
// "The source text alone" is now literally true: the preprocessed text, an accept/reject
// verdict, and the log that explains a rejection. Anything that needs to know what the
// shader MEANS is derived from the parse instead - see the note on the missing fields.
struct ShaderPreprocessResult {
ShaderPreprocessOutcome outcome = ShaderPreprocessOutcome::Preprocessed;
// Valid unless the preprocessor itself never ran; kept even for the rejection
// outcomes because that is the text the diagnostics refer to.
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;
// NO EXTRACTED SIDE CHANNELS ANY MORE, and their absence is the point. Explicit
// uniform locations, explicit opaque bindings and unqualified storage blocks used to be
// lexed out of the text here, which meant reading MACRO-UNEXPANDED source: MobileGL's
// preprocessor rewrites the text, it does not run the C preprocessor, so
// `binding = SOME_MACRO` reached the scanners verbatim. All three now come from
// glslang - the first from a snapshot taken inside the parse, the other two from the
// IO mapper's collect callback - and none of them is a function of the source text
// ALONE any more, which is the only thing this struct is allowed to hold.
// The compile info log to publish; empty when outcome == Preprocessed.
String infoLog;