mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
[Refactor, Test] (ShaderTranspiler, GLState): take what the relaxed parse destroys from glslang instead of scanning the source
This commit is contained in:
@@ -579,8 +579,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
if (!ValidateAttachedShaders()) return;
|
if (!ValidateAttachedShaders()) return;
|
||||||
|
|
||||||
// The two merges below read the COMPILE snapshots only - no parsed shader - so they
|
// Reads the COMPILE snapshots only - no parsed shader - so it runs before the L1
|
||||||
// run before the L1 probe, which needs the merged opaque bindings in its key.
|
// probe: a conflicting explicit uniform location must fail the link whether or not
|
||||||
|
// the memo has an answer for this program's sources.
|
||||||
MergeShaderSideChannels();
|
MergeShaderSideChannels();
|
||||||
if (!artifacts.infoLog.empty()) return; // a conflicting explicit uniform location
|
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),
|
ProgramAttrib attrib{.shaders = Move(shaders),
|
||||||
.explicitVertexInLocations = in.explicitAttribLocations,
|
.explicitVertexInLocations = in.explicitAttribLocations,
|
||||||
.explicitFragmentOutLocations = in.explicitFragDataLocation,
|
.explicitFragmentOutLocations = in.explicitFragDataLocation,
|
||||||
.explicitFragmentOutIndices = in.explicitFragDataIndex,
|
.explicitFragmentOutIndices = in.explicitFragDataIndex,
|
||||||
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings};
|
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
|
||||||
|
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding};
|
||||||
|
|
||||||
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
|
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
|
||||||
auto result = ShaderCompiler::LinkProgram(attrib);
|
auto result = ShaderCompiler::LinkProgram(attrib);
|
||||||
@@ -639,6 +645,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// A compute program must have a fixed local group size, and GL states that as a
|
// 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
|
// 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,
|
// 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.explicitVertexInLocations = &in.explicitAttribLocations;
|
||||||
keyInputs.explicitFragmentOutLocations = &in.explicitFragDataLocation;
|
keyInputs.explicitFragmentOutLocations = &in.explicitFragDataLocation;
|
||||||
keyInputs.explicitFragmentOutIndices = &in.explicitFragDataIndex;
|
keyInputs.explicitFragmentOutIndices = &in.explicitFragDataIndex;
|
||||||
keyInputs.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings;
|
|
||||||
// In the key ONLY because the payload now carries the reflection: transform feedback
|
// 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
|
// is resolved by reading the linked intermediates and never perturbs the generated
|
||||||
// SPIR-V, but it does shape xfbVaryings / xfbStrides / xfbBufferMode /
|
// SPIR-V, but it does shape xfbVaryings / xfbStrides / xfbBufferMode /
|
||||||
@@ -826,17 +832,18 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return BuildSpirvTranslationKey(keyInputs);
|
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.
|
// 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
|
// Only the explicit default-block uniform locations are merged here, and only because they
|
||||||
// sampler/image layout(binding = N) initial units. Reads the COMPILE snapshots only, so
|
// are the one piece of relaxed-parse wreckage that has to be recovered at COMPILE time:
|
||||||
// it is legal - and necessary - before any shader is parsed: the merged bindings are part
|
// the snapshot is taken inside the parse, so it is per-shader by construction, and the
|
||||||
// of the L1 memo key.
|
// 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() {
|
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) {
|
for (const auto& shader : in.shaders) {
|
||||||
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
const ShaderCompileArtifacts& compiled = CompiledArtifacts(shader.compiled);
|
||||||
for (const auto& [name, location] : compiled.explicitUniformLocations) {
|
for (const auto& [name, location] : compiled.explicitUniformLocations) {
|
||||||
@@ -850,21 +857,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return;
|
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
|
// 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);
|
artifacts.activeUniformCount, tProgramUniformCount);
|
||||||
|
|
||||||
// Effective explicit location per TProgram uniform, from two sources:
|
// 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
|
// dropped their layout(location = N) qualifiers when collecting them into
|
||||||
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
|
// MGL_GLOBAL_UBO, so reflection cannot provide them ("source-explicit");
|
||||||
// - glslang's layoutLocation() for opaque uniforms, where the qualifier
|
// - 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
|
// 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.
|
// 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
|
// The blocks are named by TMglGlslIoResolver at mapIO's collect callback, which runs over
|
||||||
// seeded. A block whose grammar it did not understand keeps today's behaviour rather than
|
// every declared block of every stage BEFORE the write-back above happens - so "declared no
|
||||||
// being defaulted on a guess - the shape of the input decides, never an assumption about it.
|
// 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
|
// 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
|
// 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".
|
// BuildGlobalUboRouting read as "member of the synthesized global UBO".
|
||||||
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
|
Vector<Int> glUniformBlockIndexToBlock; // GL uniform-block index -> block index
|
||||||
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
|
Vector<Int> blockIndexToGlUniformBlock; // block index -> GL uniform-block index (-1)
|
||||||
// Per-link merged snapshot of the attached shaders' lexically extracted
|
// Per-link merged snapshot of the layout(location = N) qualifiers the attached
|
||||||
// layout(location = N) default-block uniform qualifiers (the relaxed parse drops
|
// shaders' default-block uniforms declared, as glslang recorded them at the point
|
||||||
// them from reflection; the DoReflection assigner restores them from here).
|
// its relaxed remap dropped them (the relaxed parse drops them from reflection; the
|
||||||
|
// DoReflection assigner restores them from here).
|
||||||
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
UnorderedMap<String, Int> linkedExplicitUniformLocations;
|
||||||
// Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders
|
// 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
|
// 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;
|
Vector<Int> uniformIndexInTProgram;
|
||||||
// ditto. Will be set at glUniform1i
|
// ditto. Will be set at glUniform1i
|
||||||
Vector<Int> uniformSamplerOrImageUnitIndex;
|
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;
|
UnorderedMap<String, Uint> explicitOpaqueUniformBindings;
|
||||||
|
|
||||||
// Ordered by uniform block index
|
// Ordered by uniform block index
|
||||||
@@ -1256,9 +1261,10 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// overwrites it - default and rebind travel one path.
|
// overwrites it - default and rebind travel one path.
|
||||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||||
// Block type names of the storage blocks the program's shaders declared with NO
|
// 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
|
// layout(binding = N). Input to the seeding above; filled during mapIO by
|
||||||
// seeding above; see ExtractStorageBlocksWithoutExplicitBinding for why it has to be
|
// TMglGlslIoResolver, which is the last observer that can still tell a declared
|
||||||
// captured lexically.
|
// binding from an invented one - and, unlike the per-shader lexer this replaced,
|
||||||
|
// sees the declaration with its macros expanded.
|
||||||
std::set<String> storageBlocksWithoutBinding;
|
std::set<String> storageBlocksWithoutBinding;
|
||||||
|
|
||||||
Uint activeUniformCount = 0;
|
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
|
// 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
|
// environment snapshot: preprocessing and the three lexical rejections. Split out so P0b
|
||||||
// side-channel extractions. Split out so P0b layer 2 can memoize exactly this and
|
// layer 2 can memoize exactly this and nothing else - the glslang parse stays per-object
|
||||||
// nothing else - the glslang parse stays per-object because its TShader is consume-once.
|
// because its TShader is consume-once. Deliberately free of any per-object state so the
|
||||||
// Deliberately free of any per-object state so the memo is sound.
|
// 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
|
// 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
|
// env.fingerprint is part of the P0b cache key, so a memo can never be returned against
|
||||||
@@ -195,28 +201,21 @@ namespace {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const std::optional<String> counterOffsetError =
|
// NO ATOMIC-COUNTER OFFSET SCAN HERE ANY MORE: glslang raises both rules itself now, at
|
||||||
FindAtomicCounterOffsetViolation(result.preprocessedSource)) {
|
// the site where its relaxed remap folds the counter into a synthesized block
|
||||||
result.outcome = ShaderPreprocessOutcome::AtomicCounterOffsetRejected;
|
// (ParseHelper.cpp atomicCounterOffsetCheck, called from vkRelaxedRemapUniformVariable).
|
||||||
result.infoLog = *counterOffsetError;
|
// A violation is an ordinary parse failure, so it reaches GL through the same path every
|
||||||
return result;
|
// 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
|
// 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
|
// 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
|
// 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
|
// parse. The GL frontend semantics the relaxed parse cannot provide are restored
|
||||||
// on top: explicit default-block uniform locations through the lexical
|
// on top, all of them out of glslang: explicit default-block uniform locations from
|
||||||
// side-channels below, dead-uniform/global-UBO filtering in
|
// 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.
|
// 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;
|
result.outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -319,10 +318,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Bool parsedOk = false;
|
Bool parsedOk = false;
|
||||||
String parseLog;
|
String parseLog;
|
||||||
SharedPtr<glslang::TShader> parsedShader;
|
SharedPtr<glslang::TShader> parsedShader;
|
||||||
|
UnorderedMap<String, Int> explicitUniformLocations;
|
||||||
|
|
||||||
if (verdict) {
|
if (verdict) {
|
||||||
parsedOk = verdict->parsed;
|
parsedOk = verdict->parsed;
|
||||||
parseLog = verdict->infoLog;
|
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; "
|
MGLOG_D("ShaderCompileTask: shader %u (stage %d) L1c hit - the glslang parse was skipped; "
|
||||||
"compileStatus = %d",
|
"compileStatus = %d",
|
||||||
externalIndex, static_cast<Int>(stage), static_cast<Int>(parsedOk));
|
externalIndex, static_cast<Int>(stage), static_cast<Int>(parsedOk));
|
||||||
@@ -335,6 +338,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
parsedOk = result.has_value();
|
parsedOk = result.has_value();
|
||||||
if (parsedOk) {
|
if (parsedOk) {
|
||||||
parsedShader = result.value();
|
parsedShader = result.value();
|
||||||
|
explicitUniformLocations = CollectExplicitUniformLocations(*parsedShader);
|
||||||
} else {
|
} else {
|
||||||
parseLog = result.error().log;
|
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
|
// Empty on success by construction, matching what the publish below does with
|
||||||
// the artifacts' own log; the diagnostic the application reads on failure.
|
// the artifacts' own log; the diagnostic the application reads on failure.
|
||||||
freshVerdict->infoLog = parseLog;
|
freshVerdict->infoLog = parseLog;
|
||||||
|
freshVerdict->explicitUniformLocations = explicitUniformLocations;
|
||||||
const SizeT verdictBytes = ShaderParseVerdictBytes(*freshVerdict);
|
const SizeT verdictBytes = ShaderParseVerdictBytes(*freshVerdict);
|
||||||
GetShaderParseVerdictCache().Insert(parseKey, ShaderParseVerdictPtr(Move(freshVerdict)),
|
GetShaderParseVerdictCache().Insert(parseKey, ShaderParseVerdictPtr(Move(freshVerdict)),
|
||||||
verdictBytes);
|
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
|
// `fresh` is about to be handed to the cache. Populated on the hit path too - it
|
||||||
// is what ClaimParsedShader's deferred parse consumes.
|
// is what ClaimParsedShader's deferred parse consumes.
|
||||||
artifacts.preprocessedSource = shared.preprocessedSource;
|
artifacts.preprocessedSource = shared.preprocessedSource;
|
||||||
artifacts.explicitUniformLocations = shared.explicitUniformLocations;
|
artifacts.explicitUniformLocations = Move(explicitUniformLocations);
|
||||||
artifacts.explicitOpaqueBindings = shared.explicitOpaqueBindings;
|
|
||||||
artifacts.storageBlocksWithoutBinding = shared.storageBlocksWithoutBinding;
|
|
||||||
artifacts.infoLog.clear();
|
artifacts.infoLog.clear();
|
||||||
if (shouldPopulateCache) {
|
if (shouldPopulateCache) {
|
||||||
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
||||||
@@ -386,9 +389,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
if (shouldPopulateCache) {
|
if (shouldPopulateCache) {
|
||||||
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
|
fresh->outcome = ShaderPreprocessOutcome::ParseFailed;
|
||||||
fresh->infoLog = artifacts.infoLog;
|
fresh->infoLog = artifacts.infoLog;
|
||||||
fresh->explicitUniformLocations.clear();
|
|
||||||
fresh->explicitOpaqueBindings.clear();
|
|
||||||
fresh->storageBlocksWithoutBinding.clear();
|
|
||||||
cache->Insert(stage, sourceHash, *source, compileEnv.fingerprint, Move(fresh));
|
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
|
// ClaimParsedShader's re-parse so a later link never depends on the preprocessor
|
||||||
// being deterministic across backend-state changes.
|
// being deterministic across backend-state changes.
|
||||||
String preprocessedSource;
|
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, 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;
|
String infoLog;
|
||||||
Bool compileStatus = false;
|
Bool compileStatus = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The unit of asynchronous shader compilation: one glCompileShader's worth of pure CPU
|
// 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
|
// work - preprocess, the lexical rejections, and (unless the translation memo's compile
|
||||||
// the translation memo's compile half already knows the answer) the glslang parse - with
|
// half already knows the answer) the glslang parse plus the explicit-uniform-location
|
||||||
// every input it needs owned by the node itself.
|
// 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
|
// 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,
|
// 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 SharedPtr<glslang::TShader>& GetCompiledShader() const { return Compiled().shader; }
|
||||||
const String& GetInfoLog() const { return Compiled().infoLog; }
|
const String& GetInfoLog() const { return Compiled().infoLog; }
|
||||||
// Explicit layout(location = N) qualifiers on this shader's default-block
|
// Explicit layout(location = N) qualifiers on this shader's default-block
|
||||||
// uniforms, captured lexically at Compile() because the relaxed parse drops
|
// uniforms, as glslang recorded them at the point its Vulkan-relaxed remap
|
||||||
// them from reflection (see ExtractExplicitUniformLocations).
|
// discarded them (see CollectExplicitUniformLocations).
|
||||||
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
|
const UnorderedMap<String, Int>& GetExplicitUniformLocations() const {
|
||||||
return Compiled().explicitUniformLocations;
|
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 GetCompileStatus() const { return Compiled().compileStatus; }
|
||||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
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
|
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
|
||||||
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
|
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
|
||||||
ResourceBindingRejected,
|
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.
|
// 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.
|
// Memoizing this saves the parse itself on every later object with that source.
|
||||||
ParseFailed,
|
ParseFailed,
|
||||||
@@ -39,18 +36,23 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
// Everything ShaderObject::Compile() derives from the source text alone, i.e.
|
// Everything ShaderObject::Compile() derives from the source text alone, i.e.
|
||||||
// everything that is identical for two shader objects holding byte-identical source.
|
// 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 {
|
struct ShaderPreprocessResult {
|
||||||
ShaderPreprocessOutcome outcome = ShaderPreprocessOutcome::Preprocessed;
|
ShaderPreprocessOutcome outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||||
// Valid unless the preprocessor itself never ran; kept even for the rejection
|
// Valid unless the preprocessor itself never ran; kept even for the rejection
|
||||||
// outcomes because that is the text the diagnostics refer to.
|
// outcomes because that is the text the diagnostics refer to.
|
||||||
String preprocessedSource;
|
String preprocessedSource;
|
||||||
UnorderedMap<String, Int> explicitUniformLocations;
|
// NO EXTRACTED SIDE CHANNELS ANY MORE, and their absence is the point. Explicit
|
||||||
UnorderedMap<String, Uint> explicitOpaqueBindings;
|
// uniform locations, explicit opaque bindings and unqualified storage blocks used to be
|
||||||
// Block type names of the shader storage blocks declared here with NO
|
// lexed out of the text here, which meant reading MACRO-UNEXPANDED source: MobileGL's
|
||||||
// layout(binding = N). GL gives such a block binding 0; nothing downstream can still
|
// preprocessor rewrites the text, it does not run the C preprocessor, so
|
||||||
// tell, because glslang's IO mapper auto-assigns one and writes it into the qualifier.
|
// `binding = SOME_MACRO` reached the scanners verbatim. All three now come from
|
||||||
// See ExtractStorageBlocksWithoutExplicitBinding.
|
// glslang - the first from a snapshot taken inside the parse, the other two from the
|
||||||
std::set<String> storageBlocksWithoutBinding;
|
// 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.
|
// The compile info log to publish; empty when outcome == Preprocessed.
|
||||||
String infoLog;
|
String infoLog;
|
||||||
|
|
||||||
|
|||||||
@@ -2131,9 +2131,11 @@ void main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The case the old masker actually broke: an apostrophe in real (non-comment) text. Everything after
|
// The case the old masker actually broke: an apostrophe in real (non-comment) text. Everything after
|
||||||
// it looked like string interior, so ExtractExplicitUniformLocations tokenized a blank source and
|
// it looked like string interior, so the rewriter's own scans went blind past it - which is still
|
||||||
// handed the GL location assigner an empty map - the uniform silently lost its explicit location.
|
// what this pins, now that the explicit location itself is recovered from the parse rather than
|
||||||
TEST_F(ProgramUtilTest, PreprocessApostropheInDirectiveKeepsLaterCodeVisibleToExtractors) {
|
// from a scan. The two halves have to agree end to end: the preprocessed text must still declare
|
||||||
|
// the uniform, AND the parse must still hand its location back.
|
||||||
|
TEST_F(ProgramUtilTest, PreprocessApostropheInDirectiveKeepsLaterCodeVisibleToTheParse) {
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
String source = R"(#version 460 core
|
String source = R"(#version 460 core
|
||||||
@@ -2148,15 +2150,15 @@ void main() {
|
|||||||
)";
|
)";
|
||||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||||
|
|
||||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
|
||||||
ASSERT_EQ(locations.count("tint"), 1u) << "extractor went blind past the apostrophe:\n" << source;
|
|
||||||
EXPECT_EQ(locations.at("tint"), 7);
|
|
||||||
|
|
||||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||||
auto res = ShaderCompiler::CompileShader(attrib);
|
auto res = ShaderCompiler::CompileShader(attrib);
|
||||||
if (!res) {
|
if (!res) {
|
||||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*res.value());
|
||||||
|
ASSERT_EQ(locations.count("tint"), 1u) << "the rewriter went blind past the apostrophe:\n" << source;
|
||||||
|
EXPECT_EQ(locations.at("tint"), 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PreprocessShaderSource used to rediscover "where does the #version directive end?" once per
|
// PreprocessShaderSource used to rediscover "where does the #version directive end?" once per
|
||||||
@@ -2340,8 +2342,7 @@ namespace {
|
|||||||
auto result = MakeShared<ShaderPreprocessResult>();
|
auto result = MakeShared<ShaderPreprocessResult>();
|
||||||
result->outcome = ShaderPreprocessOutcome::Preprocessed;
|
result->outcome = ShaderPreprocessOutcome::Preprocessed;
|
||||||
result->preprocessedSource = preprocessed;
|
result->preprocessedSource = preprocessed;
|
||||||
result->explicitUniformLocations["uMarker"] = 7;
|
result->infoLog = "marker:" + preprocessed;
|
||||||
result->explicitOpaqueBindings["sMarker"] = 3;
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -2358,12 +2359,9 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRoundTripsAndSeparatesStages) {
|
|||||||
ASSERT_NE(hit, nullptr);
|
ASSERT_NE(hit, nullptr);
|
||||||
EXPECT_TRUE(hit->Preprocessed());
|
EXPECT_TRUE(hit->Preprocessed());
|
||||||
EXPECT_EQ(hit->preprocessedSource, "vertex-preprocessed");
|
EXPECT_EQ(hit->preprocessedSource, "vertex-preprocessed");
|
||||||
const auto uniformIt = hit->explicitUniformLocations.find("uMarker");
|
// The whole payload round-trips, not just the text: every field the entry carries has to
|
||||||
ASSERT_NE(uniformIt, hit->explicitUniformLocations.end());
|
// come back, or a hit would publish a half-populated result.
|
||||||
EXPECT_EQ(uniformIt->second, 7);
|
EXPECT_EQ(hit->infoLog, "marker:vertex-preprocessed");
|
||||||
const auto bindingIt = hit->explicitOpaqueBindings.find("sMarker");
|
|
||||||
ASSERT_NE(bindingIt, hit->explicitOpaqueBindings.end());
|
|
||||||
EXPECT_EQ(bindingIt->second, 3u);
|
|
||||||
|
|
||||||
// Byte-identical source, different stage: a different key, so still a miss. Two
|
// Byte-identical source, different stage: a different key, so still a miss. Two
|
||||||
// stages sharing one entry would hand a fragment shader a vertex preprocess.
|
// stages sharing one entry would hand a fragment shader a vertex preprocess.
|
||||||
@@ -4418,267 +4416,6 @@ TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
|
|||||||
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
|
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
// KHR-GL43.shader_atomic_counters.negative-offset-1: an atomic counter whose layout(offset = N)
|
|
||||||
// puts its last byte past GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE is a COMPILE-time error, and the CTS
|
|
||||||
// never links the shader at all. MobileGL only had the rule at link, because the Vulkan-relaxed
|
|
||||||
// parse never reaches glslang's fixOffset().
|
|
||||||
TEST_F(ProgramUtilTest, AtomicCounterOffsetCeilingIsCheckedAtCompile) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const auto violation = [](const String& body) {
|
|
||||||
return FindAtomicCounterOffsetViolation("#version 430 core\n" + body + "void main() {}\n");
|
|
||||||
};
|
|
||||||
const String maxSize = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
|
||||||
const String lastLegal = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 4);
|
|
||||||
|
|
||||||
// The boundary itself: the last counter that still fits, and the first that does not.
|
|
||||||
EXPECT_FALSE(violation("layout(binding = 0, offset = " + lastLegal + ") uniform atomic_uint c;\n").has_value());
|
|
||||||
EXPECT_TRUE(violation("layout(binding = 0, offset = " + maxSize + ") uniform atomic_uint c;\n").has_value());
|
|
||||||
|
|
||||||
// An array occupies one word per element, so what has to fit is the LAST one.
|
|
||||||
EXPECT_FALSE(violation("layout(offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 16) +
|
|
||||||
") uniform atomic_uint c[4];\n")
|
|
||||||
.has_value());
|
|
||||||
EXPECT_TRUE(violation("layout(offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 8) +
|
|
||||||
") uniform atomic_uint c[4];\n")
|
|
||||||
.has_value());
|
|
||||||
|
|
||||||
// An offset that is not a multiple of 4 (GL 4.6 core 7.7), and one that is.
|
|
||||||
EXPECT_TRUE(violation("layout(offset = 2) uniform atomic_uint c;\n").has_value());
|
|
||||||
EXPECT_FALSE(violation("layout(offset = 8) uniform atomic_uint c;\n").has_value());
|
|
||||||
|
|
||||||
// Things the scanner must NOT judge: a counter with no explicit offset, an `offset` that is
|
|
||||||
// an ordinary identifier rather than a layout qualifier, an array sized by an expression,
|
|
||||||
// and an offset qualifier that belongs to a different declaration.
|
|
||||||
EXPECT_FALSE(violation("uniform atomic_uint c;\nconst int offset = 99999;\n").has_value());
|
|
||||||
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(offset = " + maxSize +
|
|
||||||
") uniform atomic_uint c[kCount];\n")
|
|
||||||
.has_value());
|
|
||||||
EXPECT_FALSE(violation("layout(offset = " + maxSize + ") uniform Block { int x; };\n"
|
|
||||||
"uniform atomic_uint c;\n")
|
|
||||||
.has_value());
|
|
||||||
// A source with no counter at all never pays for the scan and never reports one.
|
|
||||||
EXPECT_FALSE(FindAtomicCounterOffsetViolation("#version 430 core\nvoid main() {}\n").has_value());
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style, so
|
|
||||||
// layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The extractor used to accept
|
|
||||||
// a base-10 digit run and nothing else: the hex spelling failed the test entirely and the
|
|
||||||
// declaration silently lost its explicit location, while the octal one was read as decimal 10.
|
|
||||||
// The identical defect sat on every array dimension and on layout(binding = N).
|
|
||||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsReadsNonDecimalIntegerLiterals) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const String source = R"(#version 430 core
|
|
||||||
layout(location = 0xA) uniform vec4 hexLower;
|
|
||||||
layout(location = 0X1f) uniform vec4 hexUpper;
|
|
||||||
layout(location = 010) uniform vec4 octal;
|
|
||||||
layout(location = 3u) uniform vec4 unsignedSuffix;
|
|
||||||
layout(location = 0x2) uniform float hexArray[0x3];
|
|
||||||
layout(location = 1.0) uniform vec4 notAnInteger;
|
|
||||||
layout(location = 7f) uniform vec4 unknownSuffix;
|
|
||||||
void main() {}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
|
||||||
ASSERT_EQ(locations.count("hexLower"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("hexLower"), 10);
|
|
||||||
ASSERT_EQ(locations.count("hexUpper"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("hexUpper"), 31);
|
|
||||||
ASSERT_EQ(locations.count("octal"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("octal"), 8) << "a leading zero is octal in GLSL, not decimal";
|
|
||||||
ASSERT_EQ(locations.count("unsignedSuffix"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("unsignedSuffix"), 3);
|
|
||||||
ASSERT_EQ(locations.count("hexArray"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("hexArray"), 2);
|
|
||||||
|
|
||||||
// Still never guessed at: a float and an unknown suffix are skipped, not rounded.
|
|
||||||
EXPECT_EQ(locations.count("notAnInteger"), 0u);
|
|
||||||
EXPECT_EQ(locations.count("unknownSuffix"), 0u);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A hexadecimal array dimension has to size the declarator's span too, or the declarator after it
|
|
||||||
// in the same statement starts at the wrong location.
|
|
||||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsSpansANonDecimalArrayDimension) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(
|
|
||||||
"#version 430 core\nlayout(location = 50) uniform float first[0x3], second;\nvoid main() {}\n");
|
|
||||||
ASSERT_EQ(locations.count("first"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("first"), 50);
|
|
||||||
ASSERT_EQ(locations.count("second"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("second"), 53) << "0x3 is three elements, not zero and not three hundred";
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL43.explicit_uniform_location.uniform-loc-array-of-arrays: glslang reflects
|
|
||||||
// `float u[2][3]` as "u[0][0]" and "u[1][0]", and the linker resolves such a name by stripping the
|
|
||||||
// single trailing "[0]" - so the map has to answer "u[1]", not just "u". Without the pre-flattened
|
|
||||||
// keys both records missed the map entirely and were first-fitted from location 0.
|
|
||||||
TEST_F(ProgramUtilTest, ExtractExplicitUniformLocationsExpandsArrayOfArraysElements) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const String source = R"(#version 430 core
|
|
||||||
layout(location = 2) uniform float two_d[2][3];
|
|
||||||
layout(location = 20) uniform float three_d[2][2][4];
|
|
||||||
layout(location = 40) uniform float one_d[3];
|
|
||||||
void main() {}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const UnorderedMap<String, Int> locations = ExtractExplicitUniformLocations(source);
|
|
||||||
|
|
||||||
// The root entry is unchanged - the synthesized keys are additional, never a replacement.
|
|
||||||
ASSERT_EQ(locations.count("two_d"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("two_d"), 2);
|
|
||||||
// One key per outer index, each starting a run of the innermost dimension (3 here).
|
|
||||||
ASSERT_EQ(locations.count("two_d[0]"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("two_d[0]"), 2);
|
|
||||||
ASSERT_EQ(locations.count("two_d[1]"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("two_d[1]"), 5);
|
|
||||||
|
|
||||||
// Three dimensions: glslang expands all but the innermost, so both outer indices are spelled.
|
|
||||||
ASSERT_EQ(locations.count("three_d"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("three_d"), 20);
|
|
||||||
ASSERT_EQ(locations.count("three_d[0][0]"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("three_d[0][0]"), 20);
|
|
||||||
ASSERT_EQ(locations.count("three_d[0][1]"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("three_d[0][1]"), 24);
|
|
||||||
ASSERT_EQ(locations.count("three_d[1][0]"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("three_d[1][0]"), 28);
|
|
||||||
ASSERT_EQ(locations.count("three_d[1][1]"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("three_d[1][1]"), 32);
|
|
||||||
|
|
||||||
// A 1-D array needs no expansion: stripping "[0]" already reaches the root.
|
|
||||||
ASSERT_EQ(locations.count("one_d"), 1u);
|
|
||||||
EXPECT_EQ(locations.at("one_d"), 40);
|
|
||||||
EXPECT_EQ(locations.count("one_d[0]"), 0u);
|
|
||||||
|
|
||||||
// The declarator after an array-of-arrays still advances by the WHOLE element count.
|
|
||||||
const UnorderedMap<String, Int> pair = ExtractExplicitUniformLocations(
|
|
||||||
"#version 430 core\nlayout(location = 0) uniform float a[2][3], b;\nvoid main() {}\n");
|
|
||||||
ASSERT_EQ(pair.count("b"), 1u);
|
|
||||||
EXPECT_EQ(pair.at("b"), 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL43.explicit_uniform_location: layout(binding = 0x2) on a sampler is the same literal defect
|
|
||||||
// as the location one, and losing it costs the sampler its initial texture unit.
|
|
||||||
TEST_F(ProgramUtilTest, ExtractExplicitOpaqueBindingsReadsNonDecimalIntegerLiterals) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const String source = R"(#version 430 core
|
|
||||||
layout(binding = 0x2) uniform sampler2D hexUnit;
|
|
||||||
layout(binding = 012) uniform sampler2D octalUnit;
|
|
||||||
layout(binding = 1u) uniform sampler2D suffixedUnit;
|
|
||||||
void main() {}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const UnorderedMap<String, Uint> bindings = ExtractExplicitOpaqueBindings(source);
|
|
||||||
ASSERT_EQ(bindings.count("hexUnit"), 1u);
|
|
||||||
EXPECT_EQ(bindings.at("hexUnit"), 2u);
|
|
||||||
ASSERT_EQ(bindings.count("octalUnit"), 1u);
|
|
||||||
EXPECT_EQ(bindings.at("octalUnit"), 10u) << "012 is octal ten, not twelve";
|
|
||||||
ASSERT_EQ(bindings.count("suffixedUnit"), 1u);
|
|
||||||
EXPECT_EQ(bindings.at("suffixedUnit"), 1u);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GL 4.3 core 7.8 puts a storage block with no layout(binding = N) on binding ZERO. Nothing
|
|
||||||
// downstream can still tell which blocks those are, because glslang's IO mapper auto-assigns a
|
|
||||||
// binding out of one flat space and writes it into the qualifier - so the reflection reports the
|
|
||||||
// invention. This scanner is the only surviving record, and it reports POSITIVELY: a block is
|
|
||||||
// named only when it was recognised in full AND recognised as unqualified.
|
|
||||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingNamesOnlyUnqualifiedBlocks) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
// KHR-GL43.compute_shader.resource-ubo's own shape: an unqualified storage block alongside
|
|
||||||
// the uniform blocks whose presence is what pushes it off binding 0 today.
|
|
||||||
const String source = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std140) uniform InputBuffer { vec4 data[4]; } g_in_buffer[12];
|
|
||||||
layout(std430) buffer OutputBuffer { vec4 data0[4]; } g_out_buffer;
|
|
||||||
layout(std430, binding = 3) buffer BoundBlock { vec4 data1[4]; } g_bound;
|
|
||||||
layout(binding = 5, std430) buffer BoundFirst { vec4 data2[4]; } g_bound_first;
|
|
||||||
void main() { g_out_buffer.data0[0] = g_in_buffer[0].data[0]; }
|
|
||||||
)";
|
|
||||||
|
|
||||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
|
||||||
EXPECT_EQ(unqualified.count("OutputBuffer"), 1u)
|
|
||||||
<< "the block the test binds at 0 with glBindBufferBase must be recognised";
|
|
||||||
EXPECT_EQ(unqualified.count("BoundBlock"), 0u)
|
|
||||||
<< "a declared binding must never be defaulted away";
|
|
||||||
EXPECT_EQ(unqualified.count("BoundFirst"), 0u)
|
|
||||||
<< "the binding may appear anywhere in the layout list, not only last";
|
|
||||||
// A UNIFORM block is a different binding space with its own glUniformBlockBinding path, and
|
|
||||||
// its default is already handled where uniformBlockBinding is seeded. Naming it here would
|
|
||||||
// make the seeder default a resource it does not own.
|
|
||||||
EXPECT_EQ(unqualified.count("InputBuffer"), 0u) << "uniform blocks are out of scope";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The scanner must not mistake a member qualifier, a buffer-typed sampler, or the
|
|
||||||
// "layout(...) buffer;" default-qualifier form for a block declaration - and must record nothing
|
|
||||||
// at all for grammar it does not fully recognise, so that anything surprising keeps today's
|
|
||||||
// behaviour instead of being defaulted on a guess.
|
|
||||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingIgnoresNonBlockBufferTokens) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const String source = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
uniform samplerBuffer texelSampler;
|
|
||||||
layout(std430) buffer;
|
|
||||||
layout(std430) buffer Real { vec4 v[4]; } realInstance;
|
|
||||||
void main() { realInstance.v[0] = texelFetch(texelSampler, 0); }
|
|
||||||
)";
|
|
||||||
|
|
||||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
|
||||||
EXPECT_EQ(unqualified.count("Real"), 1u);
|
|
||||||
// samplerBuffer is one identifier token, so it can never match the `buffer` keyword; and the
|
|
||||||
// default-qualifier form declares no block, so there is no name to record.
|
|
||||||
EXPECT_EQ(unqualified.size(), 1u)
|
|
||||||
<< "only the one real block declaration may be recorded";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The dangerous direction, because a false positive here DEFAULTS AWAY a binding the shader
|
|
||||||
// really declared. Memory qualifiers may sit between the layout list and the `buffer` keyword in
|
|
||||||
// either order, and the binding has to survive them.
|
|
||||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingKeepsBindingsAcrossMemoryQualifiers) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const String source = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std430, binding = 1) coherent restrict buffer AfterLayout { uint a; } afterLayout;
|
|
||||||
readonly layout(std430, binding = 2) buffer BeforeLayout { uint b; } beforeLayout;
|
|
||||||
writeonly buffer NoBindingAtAll { uint c; } noBinding;
|
|
||||||
void main() { noBinding.c = afterLayout.a + beforeLayout.b; }
|
|
||||||
)";
|
|
||||||
|
|
||||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
|
||||||
EXPECT_EQ(unqualified.count("AfterLayout"), 0u)
|
|
||||||
<< "coherent/restrict must not break the qualifier run and lose the binding";
|
|
||||||
EXPECT_EQ(unqualified.count("BeforeLayout"), 0u)
|
|
||||||
<< "a qualifier may precede the layout list too";
|
|
||||||
EXPECT_EQ(unqualified.count("NoBindingAtAll"), 1u)
|
|
||||||
<< "a memory-qualified block with no binding is still an unqualified block";
|
|
||||||
}
|
|
||||||
|
|
||||||
// This scans preprocessor-visible text, so a block can be declared twice - once with a binding
|
|
||||||
// and once without. Reporting it as unqualified would default away a binding the active
|
|
||||||
// declaration carries, so any name seen WITH a binding is dropped outright.
|
|
||||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingDropsNamesSeenBothWays) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const String source = R"(#version 430 core
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std430, binding = 4) buffer Ambiguous { uint a; } bound;
|
|
||||||
layout(std430) buffer Ambiguous { uint a; } unbound;
|
|
||||||
layout(std430) buffer Clear { uint b; } clearInstance;
|
|
||||||
void main() { clearInstance.b = 0u; }
|
|
||||||
)";
|
|
||||||
|
|
||||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
|
||||||
EXPECT_EQ(unqualified.count("Ambiguous"), 0u)
|
|
||||||
<< "seen both ways is a doubt, and a doubt must not become a default";
|
|
||||||
EXPECT_EQ(unqualified.count("Clear"), 1u)
|
|
||||||
<< "the unambiguous block alongside it is still recognised";
|
|
||||||
}
|
|
||||||
|
|
||||||
// KHR-GL43.shader_image_size.advanced-nonMS-* is nothing but its passing twin basic-nonMS-* plus a
|
// KHR-GL43.shader_image_size.advanced-nonMS-* is nothing but its passing twin basic-nonMS-* plus a
|
||||||
// GLSL subroutine, and glslang refuses the keyword outright when the target is SPIR-V ("subroutine
|
// GLSL subroutine, and glslang refuses the keyword outright when the target is SPIR-V ("subroutine
|
||||||
// : not allowed when generating SPIR-V"), so every stage of those shaders failed to compile. The
|
// : not allowed when generating SPIR-V"), so every stage of those shaders failed to compile. The
|
||||||
@@ -4819,64 +4556,3 @@ subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
|
|||||||
<< "an inactive #if arm must not have an unconditional forwarding body appended for it";
|
<< "an inactive #if arm must not have an unconditional forwarding body appended for it";
|
||||||
}
|
}
|
||||||
|
|
||||||
// THE TEXT THIS SCANS IS NOT MACRO-EXPANDED. MobileGL's preprocessing rewrites the source, it
|
|
||||||
// does not run the C preprocessor, so a `#define` and every use of it both survive into what
|
|
||||||
// RunSourceOnlyPipeline hands here. `binding = SOME_MACRO` is therefore the ordinary spelling in
|
|
||||||
// real shader packs - Flywheel's indirect engine writes every one of its storage blocks that way
|
|
||||||
// - and reading "no integer literal" as "no binding" defaulted all of them onto binding 0 at
|
|
||||||
// once, where they aliased and the whole engine drew nothing
|
|
||||||
// (minecraft-1.21.1-neoforge-create-indirect-in-world, both backends).
|
|
||||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingKeepsAMacroSpelledBinding) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
// Flywheel's own shape, verbatim in structure: the binding is a macro, the block carries
|
|
||||||
// memory qualifiers, and the macro's definition is still sitting in the text above it.
|
|
||||||
const String source = R"(#version 460 core
|
|
||||||
#define _FLW_MODEL_BUFFER_BINDING 3
|
|
||||||
#define _FLW_DRAW_BUFFER_BINDING 4
|
|
||||||
layout(local_size_x = 32) in;
|
|
||||||
layout(std430, binding = _FLW_MODEL_BUFFER_BINDING) restrict readonly buffer ModelBuffer {
|
|
||||||
uint models[];
|
|
||||||
};
|
|
||||||
layout(std430, binding = _FLW_DRAW_BUFFER_BINDING) restrict buffer DrawBuffer {
|
|
||||||
uint draws[];
|
|
||||||
};
|
|
||||||
layout(std430) buffer ReallyUnqualified { uint u; } reallyUnqualified;
|
|
||||||
void main() { draws[0] = models[0] + reallyUnqualified.u; }
|
|
||||||
)";
|
|
||||||
|
|
||||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
|
||||||
EXPECT_EQ(unqualified.count("ModelBuffer"), 0u)
|
|
||||||
<< "a binding spelled as a macro is still a declared binding, never an absent one";
|
|
||||||
EXPECT_EQ(unqualified.count("DrawBuffer"), 0u)
|
|
||||||
<< "every block of the engine would otherwise be defaulted onto 0 together";
|
|
||||||
EXPECT_EQ(unqualified.count("ReallyUnqualified"), 1u)
|
|
||||||
<< "a block that truly declares no binding is still recognised in the same shader";
|
|
||||||
}
|
|
||||||
|
|
||||||
// The other two ways an unexpanded macro can hide a binding: as a whole layout entry, and as the
|
|
||||||
// whole qualifier run. Both have to read as doubt for the same reason the macro-valued binding
|
|
||||||
// does - what the scanner cannot expand, it must not claim is absent.
|
|
||||||
TEST_F(ProgramUtilTest, ExtractStorageBlocksWithoutExplicitBindingDoubtsAMacroQualifier) {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
|
|
||||||
const String source = R"(#version 430 core
|
|
||||||
#define FLW_BINDING binding = 2
|
|
||||||
#define SSBO_QUALIFIER layout(std430, binding = 6) restrict
|
|
||||||
layout(local_size_x = 1) in;
|
|
||||||
layout(std430, FLW_BINDING) buffer EntryMacro { uint a; } entryMacro;
|
|
||||||
SSBO_QUALIFIER buffer RunMacro { uint b; } runMacro;
|
|
||||||
layout(std430, row_major) buffer PlainLayout { uint c; } plainLayout;
|
|
||||||
void main() { plainLayout.c = entryMacro.a + runMacro.b; }
|
|
||||||
)";
|
|
||||||
|
|
||||||
const std::set<String> unqualified = ExtractStorageBlocksWithoutExplicitBinding(source);
|
|
||||||
EXPECT_EQ(unqualified.count("EntryMacro"), 0u)
|
|
||||||
<< "an unreadable layout entry may itself be the binding";
|
|
||||||
EXPECT_EQ(unqualified.count("RunMacro"), 0u)
|
|
||||||
<< "a macro standing in for the whole qualifier run may carry the binding";
|
|
||||||
// The counterweight: recognising doubt must not swallow the layout identifiers a buffer
|
|
||||||
// block legally carries, or nothing would ever be defaulted again.
|
|
||||||
EXPECT_EQ(unqualified.count("PlainLayout"), 1u)
|
|
||||||
<< "std430/row_major are GLSL, not macros, and leave no doubt behind";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ add_executable(
|
|||||||
LegalizeResourceArrayIndexTest.cpp
|
LegalizeResourceArrayIndexTest.cpp
|
||||||
FlattenAtomicCounterBlockTest.cpp
|
FlattenAtomicCounterBlockTest.cpp
|
||||||
WidenImageFormatsTest.cpp
|
WidenImageFormatsTest.cpp
|
||||||
|
GlslangCaptureTest.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(SpirvPassTest PRIVATE
|
target_include_directories(SpirvPassTest PRIVATE
|
||||||
|
|||||||
@@ -0,0 +1,678 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/GlslangCaptureTest.cpp
|
||||||
|
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||||
|
// Licensed under the GNU Lesser General Public License v3.0:
|
||||||
|
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
|
// End of Source File Header
|
||||||
|
|
||||||
|
// WHAT SURVIVES MOBILEGL'S PARSE, ASKED OF GLSLANG ITSELF.
|
||||||
|
//
|
||||||
|
// Every shader is parsed as an EShClientVulkan client under
|
||||||
|
// setEnvInputVulkanRulesRelaxed(), which destroys some of the GL declarations MobileGL
|
||||||
|
// still has to answer for. Which ones it destroys - and WHERE - decides whether a piece of
|
||||||
|
// information can be captured from glslang at all or has to be reconstructed. That question
|
||||||
|
// used to be answered by comments; these cases answer it by running the real pipeline and
|
||||||
|
// reading the real qualifiers back.
|
||||||
|
//
|
||||||
|
// The probe drives ShaderCompiler::CompileShader (the production parse configuration, byte
|
||||||
|
// for byte) and then the production mapIO, with a resolver that snapshots every entity's
|
||||||
|
// qualifier AT THE COLLECT CALLBACK - which is before glslang's IO mapper writes its
|
||||||
|
// auto-assigned bindings back into the types (iomapper.cpp:240). That callback is the last
|
||||||
|
// moment at which "the shader declared this" and "glslang invented this" are still
|
||||||
|
// different statements.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// One entity as the collect callback sees it.
|
||||||
|
struct ProbedEntity {
|
||||||
|
Bool hasBinding = false;
|
||||||
|
Uint binding = 0;
|
||||||
|
Bool hasLocation = false;
|
||||||
|
Int location = 0;
|
||||||
|
Bool isBlock = false;
|
||||||
|
Bool isBufferBlock = false;
|
||||||
|
Bool isSampler = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A pass-through resolver that records instead of deciding. It derives from the SAME
|
||||||
|
// base MobileGL ships (TDefaultGlslIoResolver) so the callbacks fire in the same order
|
||||||
|
// and with the same arguments the production resolver sees.
|
||||||
|
class ProbeResolver : public glslang::TDefaultGlslIoResolver {
|
||||||
|
public:
|
||||||
|
explicit ProbeResolver(const glslang::TProgram& program, const EShLanguage stage)
|
||||||
|
: TDefaultGlslIoResolver(*program.getIntermediate(stage)) {}
|
||||||
|
|
||||||
|
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override {
|
||||||
|
Record(ent);
|
||||||
|
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
|
||||||
|
}
|
||||||
|
|
||||||
|
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override {
|
||||||
|
Record(ent);
|
||||||
|
TDefaultGlslIoResolver::reserverStorageSlot(ent, infoSink);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<String, ProbedEntity> probed;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Record(const glslang::TVarEntryInfo& ent) {
|
||||||
|
const glslang::TType& type = ent.symbol->getType();
|
||||||
|
const glslang::TQualifier& qualifier = type.getQualifier();
|
||||||
|
ProbedEntity& record = probed[ent.symbol->getAccessName().c_str()];
|
||||||
|
record.hasBinding = qualifier.hasBinding();
|
||||||
|
record.binding = qualifier.hasBinding() ? qualifier.layoutBinding : 0u;
|
||||||
|
record.hasLocation = qualifier.hasLocation();
|
||||||
|
record.location = qualifier.hasLocation() ? static_cast<Int>(qualifier.layoutLocation) : -1;
|
||||||
|
record.isBlock = type.getBasicType() == glslang::EbtBlock;
|
||||||
|
record.isBufferBlock = record.isBlock && qualifier.storage == glslang::EvqBuffer;
|
||||||
|
record.isSampler = type.getBasicType() == glslang::EbtSampler;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parses `source` exactly as production does, links it, and returns what the collect
|
||||||
|
// callback saw. Fails the calling test (through the ASSERT_* the caller applies to the
|
||||||
|
// optional) rather than throwing.
|
||||||
|
std::optional<std::map<String, ProbedEntity>> ProbeShader(const GLenum stage, const String& source,
|
||||||
|
String& outLog) {
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
if (!shaderResult) {
|
||||||
|
outLog = shaderResult.error().log;
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto program = MakeShared<glslang::TProgram>();
|
||||||
|
program->addShader(shaderResult.value().get());
|
||||||
|
if (!program->link(EShMsgDefault)) {
|
||||||
|
outLog = program->getInfoLog();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EShLanguage lang = MG_Util::ConvertGLEnumToEShLanguage(stage);
|
||||||
|
ProbeResolver resolver(*program, lang);
|
||||||
|
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
|
||||||
|
if (!program->mapIO(&resolver, ioMapper.get())) {
|
||||||
|
outLog = program->getInfoLog();
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return resolver.probed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// What ONE production link captures, taken through the real entry points rather than
|
||||||
|
// through a probe: ShaderCompiler::CompileShader and ShaderCompiler::LinkProgram with the
|
||||||
|
// same ProgramAttrib ProgramLinkTask builds. `captureEnabled` false leaves both OUT
|
||||||
|
// pointers null, which is the negative control every capture case below pairs itself with.
|
||||||
|
struct LinkCapture {
|
||||||
|
Bool linked = false;
|
||||||
|
String log;
|
||||||
|
UnorderedMap<String, Uint> opaqueBindings;
|
||||||
|
std::set<String> storageBlocksWithoutBinding;
|
||||||
|
UnorderedMap<String, Int> uniformLocations;
|
||||||
|
};
|
||||||
|
|
||||||
|
LinkCapture CaptureFromLink(const Vector<Pair<GLenum, String>>& stages, const Bool captureEnabled = true) {
|
||||||
|
LinkCapture capture;
|
||||||
|
ProgramAttrib programAttrib;
|
||||||
|
for (const auto& [stage, source] : stages) {
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
if (!shaderResult) {
|
||||||
|
capture.log = shaderResult.error().log;
|
||||||
|
return capture;
|
||||||
|
}
|
||||||
|
for (const auto& [name, location] : CollectExplicitUniformLocations(*shaderResult.value())) {
|
||||||
|
capture.uniformLocations.emplace(name, location);
|
||||||
|
}
|
||||||
|
programAttrib.shaders.push_back(shaderResult.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (captureEnabled) {
|
||||||
|
programAttrib.explicitOpaqueUniformBindings = &capture.opaqueBindings;
|
||||||
|
programAttrib.storageBlocksWithoutBinding = &capture.storageBlocksWithoutBinding;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
if (!programResult) {
|
||||||
|
capture.log = programResult.error().log;
|
||||||
|
return capture;
|
||||||
|
}
|
||||||
|
capture.linked = true;
|
||||||
|
return capture;
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkCapture CaptureFromCompute(const String& source, const Bool captureEnabled = true) {
|
||||||
|
return CaptureFromLink({{GL_COMPUTE_SHADER, source}}, captureEnabled);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class GlslangCaptureProbeTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override { MobileGL::Initialize(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// THE HEADLINE ANSWER, and it contradicts what ExtractExplicitOpaqueBindings' header claimed
|
||||||
|
// for years ("the Vulkan-client relaxed parse strips these before mapIO can capture them").
|
||||||
|
//
|
||||||
|
// A PLAIN sampler/image uniform never enters vkRelaxedRemapUniformVariable's body at all: the
|
||||||
|
// guard at ParseHelper.cpp:8255-8259 admits only types that containsNonOpaque(), atomic_uint,
|
||||||
|
// or a sampler inside a STRUCT. So the binding is still on the qualifier when the IO mapper
|
||||||
|
// collects it, and it is glslang - not a lexer - that knows the answer.
|
||||||
|
//
|
||||||
|
// The default-block uniform LOCATION is the opposite verdict, and this case pins both halves
|
||||||
|
// side by side so neither can be assumed from the other: it is stripped inside that same
|
||||||
|
// function (ParseHelper.cpp:8261-8263, `layoutLocation = layoutLocationEnd`), which is why
|
||||||
|
// recovering it needs a snapshot taken INSIDE glslang rather than a resolver callback.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, OpaqueBindingsSurviveTheRelaxedParseButPlainUniformLocationsDoNot) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(binding = 3) uniform sampler2D probeSampler;
|
||||||
|
layout(binding = 5, rgba32f) uniform image2D probeImage;
|
||||||
|
layout(location = 7) uniform vec4 probeUniform;
|
||||||
|
layout(std430, binding = 2) buffer BoundBlock { uint bound; } boundInstance;
|
||||||
|
layout(std430) buffer UnboundBlock { uint unbound; } unboundInstance;
|
||||||
|
void main() {
|
||||||
|
unboundInstance.unbound = boundInstance.bound + uint(texture(probeSampler, vec2(0)).x) +
|
||||||
|
uint(imageLoad(probeImage, ivec2(0)).x) + uint(probeUniform.x);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
String log;
|
||||||
|
const auto probed = ProbeShader(GL_COMPUTE_SHADER, source, log);
|
||||||
|
ASSERT_TRUE(probed.has_value()) << log;
|
||||||
|
|
||||||
|
ASSERT_TRUE(probed->contains("probeSampler"));
|
||||||
|
EXPECT_TRUE(probed->at("probeSampler").hasBinding)
|
||||||
|
<< "a plain sampler's layout(binding=) is NOT stripped by the relaxed parse";
|
||||||
|
EXPECT_EQ(probed->at("probeSampler").binding, 3u);
|
||||||
|
|
||||||
|
ASSERT_TRUE(probed->contains("probeImage"));
|
||||||
|
EXPECT_TRUE(probed->at("probeImage").hasBinding)
|
||||||
|
<< "images take the same path as samplers (both are EbtSampler)";
|
||||||
|
EXPECT_EQ(probed->at("probeImage").binding, 5u);
|
||||||
|
|
||||||
|
// The default-block uniform is gone from the entity list entirely - it was swept into
|
||||||
|
// MGL_GLOBAL_UBO - and even if it were here it would carry layoutLocationEnd. That is
|
||||||
|
// exactly why the location capture has to happen inside glslang.
|
||||||
|
if (probed->contains("probeUniform")) {
|
||||||
|
EXPECT_FALSE(probed->at("probeUniform").hasLocation)
|
||||||
|
<< "vkRelaxedRemapUniformVariable strips a default-block uniform's location";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other half of the storage-block question: at the collect callback, "declared no
|
||||||
|
// binding" is still distinguishable from "glslang picked one", which is what makes
|
||||||
|
// TMglGlslIoResolver the right place to recover GL's binding-0 default. Ten lines later
|
||||||
|
// (iomapper.cpp:240) both blocks carry a number and nothing can tell them apart.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, StorageBlockBindingPresenceIsStillTruthfulAtTheCollectCallback) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(std430, binding = 2) buffer BoundBlock { uint bound; } boundInstance;
|
||||||
|
layout(std430) buffer UnboundBlock { uint unbound; } unboundInstance;
|
||||||
|
layout(std140) uniform UniformBlock { uint u; } uniformInstance;
|
||||||
|
void main() { unboundInstance.unbound = boundInstance.bound + uniformInstance.u; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
String log;
|
||||||
|
const auto probed = ProbeShader(GL_COMPUTE_SHADER, source, log);
|
||||||
|
ASSERT_TRUE(probed.has_value()) << log;
|
||||||
|
|
||||||
|
ASSERT_TRUE(probed->contains("BoundBlock"));
|
||||||
|
EXPECT_TRUE(probed->at("BoundBlock").isBufferBlock);
|
||||||
|
EXPECT_TRUE(probed->at("BoundBlock").hasBinding);
|
||||||
|
EXPECT_EQ(probed->at("BoundBlock").binding, 2u);
|
||||||
|
|
||||||
|
ASSERT_TRUE(probed->contains("UnboundBlock"));
|
||||||
|
EXPECT_TRUE(probed->at("UnboundBlock").isBufferBlock);
|
||||||
|
EXPECT_FALSE(probed->at("UnboundBlock").hasBinding)
|
||||||
|
<< "an unqualified storage block must still read as unqualified here";
|
||||||
|
|
||||||
|
// A uniform block is a different binding space with its own default path; the capture
|
||||||
|
// must be able to tell the two apart, which storage == EvqBuffer does.
|
||||||
|
ASSERT_TRUE(probed->contains("UniformBlock"));
|
||||||
|
EXPECT_FALSE(probed->at("UniformBlock").isBufferBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================================================================================
|
||||||
|
// THE CAPTURES THEMSELVES.
|
||||||
|
//
|
||||||
|
// Every case below is the SCENARIO of a scan these captures replaced, re-pointed at the new
|
||||||
|
// mechanism. Keeping the scenarios is the point: the interesting inputs were found the
|
||||||
|
// expensive way (a production regression, a CTS failure), and they are still the inputs that
|
||||||
|
// decide whether the recovery is right - what changed is only who answers.
|
||||||
|
//
|
||||||
|
// Every capture also has a NEGATIVE CONTROL: the same shader with the capture switched off,
|
||||||
|
// asserting the answer disappears. Without one, a case that passes proves only that SOMETHING
|
||||||
|
// produced the number.
|
||||||
|
// ===========================================================================================
|
||||||
|
|
||||||
|
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style,
|
||||||
|
// so layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The lexical extractor
|
||||||
|
// this replaces had to implement that rule itself, got it wrong for both spellings, and was
|
||||||
|
// then fixed - twice. glslang has always had it, because it is the GLSL lexer.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, UniformLocationsCarryNonDecimalIntegerLiterals) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(location = 0xA) uniform vec4 hexLower;
|
||||||
|
layout(location = 0X1f) uniform vec4 hexUpper;
|
||||||
|
layout(location = 010) uniform vec4 octal;
|
||||||
|
layout(location = 3u) uniform vec4 unsignedSuffix;
|
||||||
|
layout(location = 0x2) uniform float hexArray[0x3];
|
||||||
|
void main() {
|
||||||
|
gl_Position = hexLower + hexUpper + octal + unsignedSuffix + vec4(hexArray[2]);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||||
|
|
||||||
|
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||||
|
ASSERT_EQ(locations.count("hexLower"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("hexLower"), 10);
|
||||||
|
ASSERT_EQ(locations.count("hexUpper"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("hexUpper"), 31);
|
||||||
|
ASSERT_EQ(locations.count("octal"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("octal"), 8) << "a leading zero is octal in GLSL, not decimal";
|
||||||
|
ASSERT_EQ(locations.count("unsignedSuffix"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("unsignedSuffix"), 3);
|
||||||
|
ASSERT_EQ(locations.count("hexArray"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("hexArray"), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The counterweight the lexical version needed a rule for: a location that is not an integer
|
||||||
|
// literal at all. glslang REJECTS those outright rather than skipping them, which is what GLSL
|
||||||
|
// says should happen - the scanner could only decline to record them and let the declaration
|
||||||
|
// compile with no location.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, ANonIntegralUniformLocationIsRejectedRatherThanIgnored) {
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER,
|
||||||
|
.sourceStr = "#version 430 core\nlayout(location = 1.0) uniform vec4 notAnInteger;\n"
|
||||||
|
"void main() { gl_Position = notAnInteger; }\n"};
|
||||||
|
EXPECT_FALSE(ShaderCompiler::CompileShader(attrib).has_value())
|
||||||
|
<< "a float location is a compile-time error, not a declaration without a location";
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.explicit_uniform_location.uniform-loc-array-of-arrays: glslang reflects
|
||||||
|
// `float u[2][3]` as "u[0][0]" and "u[1][0]", and the location assigner resolves such a name by
|
||||||
|
// stripping the single trailing "[0]" - so the map has to answer "u[1]", not just "u". The
|
||||||
|
// synthesized keys are the one piece of the old extractor that survived the migration, because
|
||||||
|
// they are a REFLECTION-NAME mapping rather than a reading of the source.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, UniformLocationsExpandArrayOfArraysElements) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(location = 2) uniform float two_d[2][3];
|
||||||
|
layout(location = 20) uniform float three_d[2][2][4];
|
||||||
|
layout(location = 40) uniform float one_d[3];
|
||||||
|
void main() { gl_Position = vec4(two_d[1][2] + three_d[1][1][3] + one_d[2]); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||||
|
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||||
|
|
||||||
|
// The root entry is unchanged - the synthesized keys are additional, never a replacement.
|
||||||
|
ASSERT_EQ(locations.count("two_d"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("two_d"), 2);
|
||||||
|
// One key per outer index, each starting a run of the innermost dimension (3 here).
|
||||||
|
ASSERT_EQ(locations.count("two_d[0]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("two_d[0]"), 2);
|
||||||
|
ASSERT_EQ(locations.count("two_d[1]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("two_d[1]"), 5);
|
||||||
|
|
||||||
|
// Three dimensions: glslang expands all but the innermost, so both outer indices are spelled.
|
||||||
|
ASSERT_EQ(locations.count("three_d"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d"), 20);
|
||||||
|
ASSERT_EQ(locations.count("three_d[0][0]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d[0][0]"), 20);
|
||||||
|
ASSERT_EQ(locations.count("three_d[0][1]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d[0][1]"), 24);
|
||||||
|
ASSERT_EQ(locations.count("three_d[1][0]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d[1][0]"), 28);
|
||||||
|
ASSERT_EQ(locations.count("three_d[1][1]"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("three_d[1][1]"), 32);
|
||||||
|
|
||||||
|
// A 1-D array needs no expansion: stripping "[0]" already reaches the root.
|
||||||
|
ASSERT_EQ(locations.count("one_d"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("one_d"), 40);
|
||||||
|
EXPECT_EQ(locations.count("one_d[0]"), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A DELIBERATE BEHAVIOUR CHANGE, recorded here because it is the one place the migration does
|
||||||
|
// not reproduce the old answer.
|
||||||
|
//
|
||||||
|
// The lexical extractor advanced the location across the declarators of one statement, so
|
||||||
|
// `layout(location = 50) uniform float first[3], second;` gave second = 53. GLSL has no such
|
||||||
|
// rule: 4.60 4.4 says a layout qualifier applies to THE DECLARATION, i.e. identically to every
|
||||||
|
// declarator in it, and 4.4.3 then makes two uniforms sharing a location an error. glslang - the
|
||||||
|
// reference front end - assigns 50 to both, and the CTS never exercises the form at all (its
|
||||||
|
// generator emits one uniform per declaration, es31cExplicitUniformLocationTest.cpp
|
||||||
|
// streamDefinition). The advance was an invention of the scanner; this is what the parser says.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, EveryDeclaratorOfOneStatementCarriesTheQualifiersLocation) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(location = 50) uniform float first[0x3], second;
|
||||||
|
void main() { gl_Position = vec4(first[2] + second); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||||
|
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||||
|
|
||||||
|
ASSERT_EQ(locations.count("first"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("first"), 50) << "0x3 is three elements, not zero and not three hundred";
|
||||||
|
ASSERT_EQ(locations.count("second"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("second"), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE NEGATIVE CONTROL for the uniform-location capture: nothing else in the parsed module
|
||||||
|
// knows the number. If the snapshot inside vkRelaxedRemapUniformVariable were removed, this is
|
||||||
|
// the state the location assigner would be left with - no qualifier, no reflection entry, and
|
||||||
|
// therefore a first-fit location that has nothing to do with what the shader declared.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, WithoutTheSnapshotAPlainUniformsLocationIsNowhereInTheModule) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(location = 7) uniform vec4 tint;
|
||||||
|
void main() { gl_Position = tint; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||||
|
|
||||||
|
// The capture, on.
|
||||||
|
const UnorderedMap<String, Int> locations = CollectExplicitUniformLocations(*shaderResult.value());
|
||||||
|
ASSERT_EQ(locations.count("tint"), 1u);
|
||||||
|
EXPECT_EQ(locations.at("tint"), 7);
|
||||||
|
|
||||||
|
// The capture, off - i.e. everything the module itself can still say. The uniform is a
|
||||||
|
// member of MGL_GLOBAL_UBO by now, and no symbol in the module carries location 7.
|
||||||
|
auto program = MakeShared<glslang::TProgram>();
|
||||||
|
program->addShader(shaderResult.value().get());
|
||||||
|
ASSERT_TRUE(program->link(EShMsgDefault)) << program->getInfoLog();
|
||||||
|
ASSERT_TRUE(program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
|
||||||
|
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO));
|
||||||
|
for (Int i = 0; i < program->getNumUniformVariables(); ++i) {
|
||||||
|
const auto& uniform = program->getUniform(i);
|
||||||
|
if (uniform.name != "tint") continue;
|
||||||
|
// Copied out: layoutLocationEnd is a static const with no out-of-line definition, so
|
||||||
|
// binding it to EXPECT_EQ's const reference would ODR-use it and fail to link.
|
||||||
|
const Uint noLocation = glslang::TQualifier::layoutLocationEnd;
|
||||||
|
EXPECT_EQ(uniform.layoutLocation(), noLocation)
|
||||||
|
<< "if reflection could answer this, the glslang patch would be unnecessary";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.explicit_uniform_location: layout(binding = 0x2) on a sampler is its initial texture
|
||||||
|
// unit. Same scenario the lexical extractor carried, now answered by the IO resolver - which
|
||||||
|
// gets the C-style literal rules for free, and sees a binding no scanner could have read.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, OpaqueBindingsAreCapturedIncludingNonDecimalAndMacroSpellings) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
#define UNIT_FROM_A_MACRO 5
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(binding = 0x2) uniform sampler2D hexUnit;
|
||||||
|
layout(binding = 012) uniform sampler2D octalUnit;
|
||||||
|
layout(binding = 1u) uniform sampler2D suffixedUnit;
|
||||||
|
layout(binding = UNIT_FROM_A_MACRO) uniform sampler2D macroUnit;
|
||||||
|
layout(binding = 6) uniform sampler2D arrayUnits[3];
|
||||||
|
uniform sampler2D noUnit;
|
||||||
|
layout(std430, binding = 0) buffer Out { vec4 v; } o;
|
||||||
|
void main() {
|
||||||
|
o.v = texture(hexUnit, vec2(0)) + texture(octalUnit, vec2(0)) + texture(suffixedUnit, vec2(0)) +
|
||||||
|
texture(macroUnit, vec2(0)) + texture(arrayUnits[1], vec2(0)) + texture(noUnit, vec2(0));
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const LinkCapture capture = CaptureFromCompute(source);
|
||||||
|
ASSERT_TRUE(capture.linked) << capture.log;
|
||||||
|
|
||||||
|
ASSERT_EQ(capture.opaqueBindings.count("hexUnit"), 1u);
|
||||||
|
EXPECT_EQ(capture.opaqueBindings.at("hexUnit"), 2u);
|
||||||
|
ASSERT_EQ(capture.opaqueBindings.count("octalUnit"), 1u);
|
||||||
|
EXPECT_EQ(capture.opaqueBindings.at("octalUnit"), 10u) << "012 is octal ten, not twelve";
|
||||||
|
ASSERT_EQ(capture.opaqueBindings.count("suffixedUnit"), 1u);
|
||||||
|
EXPECT_EQ(capture.opaqueBindings.at("suffixedUnit"), 1u);
|
||||||
|
// The whole reason the capture moved: the AST sees expanded text.
|
||||||
|
ASSERT_EQ(capture.opaqueBindings.count("macroUnit"), 1u)
|
||||||
|
<< "a unit spelled as a macro is a declared unit like any other";
|
||||||
|
EXPECT_EQ(capture.opaqueBindings.at("macroUnit"), 5u);
|
||||||
|
// An array is keyed by its declared name, which is what the reflection lookup strips "[0]"
|
||||||
|
// to reach.
|
||||||
|
ASSERT_EQ(capture.opaqueBindings.count("arrayUnits"), 1u);
|
||||||
|
EXPECT_EQ(capture.opaqueBindings.at("arrayUnits"), 6u);
|
||||||
|
// Reported POSITIVELY: a sampler that declared no unit must not appear at all, or it would
|
||||||
|
// be given one it never asked for.
|
||||||
|
EXPECT_EQ(capture.opaqueBindings.count("noUnit"), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE NEGATIVE CONTROL for the opaque-binding capture.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, OpaqueBindingsDisappearWhenTheResolverCaptureIsOff) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(binding = 3) uniform sampler2D unit;
|
||||||
|
layout(std430, binding = 0) buffer Out { vec4 v; } o;
|
||||||
|
void main() { o.v = texture(unit, vec2(0)); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
ASSERT_EQ(CaptureFromCompute(source).opaqueBindings.count("unit"), 1u);
|
||||||
|
const LinkCapture off = CaptureFromCompute(source, /*captureEnabled=*/false);
|
||||||
|
ASSERT_TRUE(off.linked) << off.log;
|
||||||
|
EXPECT_TRUE(off.opaqueBindings.empty())
|
||||||
|
<< "nothing but the resolver fills this map; a non-empty result would mean the capture "
|
||||||
|
"is being shadowed by a leftover path";
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.compute_shader.resource-ubo's own shape: an unqualified storage block alongside the
|
||||||
|
// uniform blocks whose presence is what pushes it off binding 0. GL 4.3 core 7.8 puts such a
|
||||||
|
// block on binding ZERO; by the time reflection is built glslang has invented a number and
|
||||||
|
// written it into the qualifier, so this capture is the only surviving record.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, UnqualifiedStorageBlocksAreNamedAndQualifiedOnesAreNot) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(std140) uniform InputBuffer { vec4 data[4]; } g_in_buffer[12];
|
||||||
|
layout(std430) buffer OutputBuffer { vec4 data0[4]; } g_out_buffer;
|
||||||
|
layout(std430, binding = 3) buffer BoundBlock { vec4 data1[4]; } g_bound;
|
||||||
|
layout(binding = 5, std430) buffer BoundFirst { vec4 data2[4]; } g_bound_first;
|
||||||
|
void main() {
|
||||||
|
g_out_buffer.data0[0] = g_in_buffer[0].data[0] + g_bound.data1[0] + g_bound_first.data2[0];
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const LinkCapture capture = CaptureFromCompute(source);
|
||||||
|
ASSERT_TRUE(capture.linked) << capture.log;
|
||||||
|
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("OutputBuffer"), 1u)
|
||||||
|
<< "the block the test binds at 0 with glBindBufferBase must be recognised";
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("BoundBlock"), 0u)
|
||||||
|
<< "a declared binding must never be defaulted away";
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("BoundFirst"), 0u)
|
||||||
|
<< "the binding may appear anywhere in the layout list, not only last";
|
||||||
|
// A UNIFORM block is a different binding space with its own glUniformBlockBinding path, and
|
||||||
|
// its default is already handled where uniformBlockBinding is seeded. Naming it here would
|
||||||
|
// make the seeder default a resource it does not own.
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("InputBuffer"), 0u)
|
||||||
|
<< "uniform blocks are out of scope";
|
||||||
|
}
|
||||||
|
|
||||||
|
// The capture must not mistake a buffer-typed SAMPLER or a member qualifier for a block, and
|
||||||
|
// memory qualifiers in either order must not cost a block its binding - the dangerous
|
||||||
|
// direction, because a false positive here DEFAULTS AWAY a binding the shader really declared.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, StorageBlockCaptureSurvivesMemoryQualifiersAndIgnoresBufferSamplers) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
uniform samplerBuffer texelSampler;
|
||||||
|
layout(std430, binding = 1) coherent restrict buffer AfterLayout { uint a; } afterLayout;
|
||||||
|
readonly layout(std430, binding = 2) buffer BeforeLayout { uint b; } beforeLayout;
|
||||||
|
writeonly buffer NoBindingAtAll { uint c; } noBinding;
|
||||||
|
void main() { noBinding.c = afterLayout.a + beforeLayout.b + uint(texelFetch(texelSampler, 0).x); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
const LinkCapture capture = CaptureFromCompute(source);
|
||||||
|
ASSERT_TRUE(capture.linked) << capture.log;
|
||||||
|
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("AfterLayout"), 0u)
|
||||||
|
<< "coherent/restrict must not break the qualifier run and lose the binding";
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("BeforeLayout"), 0u)
|
||||||
|
<< "a qualifier may precede the layout list too";
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("NoBindingAtAll"), 1u)
|
||||||
|
<< "a memory-qualified block with no binding is still an unqualified block";
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("texelSampler"), 0u)
|
||||||
|
<< "a samplerBuffer is not a buffer block";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE REGRESSION THIS MIGRATION EXISTS FOR (7de7cfc6,
|
||||||
|
// minecraft-1.21.1-neoforge-create-indirect-in-world, both backends). Flywheel's indirect
|
||||||
|
// engine spells every storage-block binding as a macro, and the scan that used to answer this
|
||||||
|
// question ran on MACRO-UNEXPANDED text: MobileGL's preprocessing rewrites the source, it does
|
||||||
|
// not run the C preprocessor, so `binding = _FLW_MODEL_BUFFER_BINDING` reached the scanner
|
||||||
|
// verbatim and "no integer literal" was read as "no binding". All eight blocks were defaulted
|
||||||
|
// onto binding 0 at once, aliased there, and the engine drew nothing.
|
||||||
|
//
|
||||||
|
// It passes here for a structural reason rather than a grammatical one: the IO mapper sees the
|
||||||
|
// declaration the PARSER built, and the parser ran the preprocessor first. No rule about macro
|
||||||
|
// spellings exists anywhere in this path, and none can be forgotten.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, AMacroSpelledStorageBlockBindingIsADeclaredBinding) {
|
||||||
|
// Flywheel's own shape, verbatim in structure: the binding is a macro, the block carries
|
||||||
|
// memory qualifiers, and the macro's definition is still sitting in the text above it.
|
||||||
|
const String source = R"(#version 460 core
|
||||||
|
#define _FLW_MODEL_BUFFER_BINDING 3
|
||||||
|
#define _FLW_DRAW_BUFFER_BINDING 4
|
||||||
|
#define FLW_BINDING binding = 2
|
||||||
|
#define SSBO_QUALIFIER layout(std430, binding = 6) restrict
|
||||||
|
layout(local_size_x = 32) in;
|
||||||
|
layout(std430, binding = _FLW_MODEL_BUFFER_BINDING) restrict readonly buffer ModelBuffer {
|
||||||
|
uint models[];
|
||||||
|
};
|
||||||
|
layout(std430, binding = _FLW_DRAW_BUFFER_BINDING) restrict buffer DrawBuffer {
|
||||||
|
uint draws[];
|
||||||
|
};
|
||||||
|
layout(std430, FLW_BINDING) buffer EntryMacro { uint a; } entryMacro;
|
||||||
|
SSBO_QUALIFIER buffer RunMacro { uint b; } runMacro;
|
||||||
|
layout(std430, row_major) buffer PlainLayout { uint c; } plainLayout;
|
||||||
|
layout(std430) buffer ReallyUnqualified { uint u; } reallyUnqualified;
|
||||||
|
void main() {
|
||||||
|
draws[0] = models[0] + reallyUnqualified.u + entryMacro.a + runMacro.b + plainLayout.c;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const LinkCapture capture = CaptureFromCompute(source);
|
||||||
|
ASSERT_TRUE(capture.linked) << capture.log;
|
||||||
|
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("ModelBuffer"), 0u)
|
||||||
|
<< "a binding spelled as a macro is still a declared binding, never an absent one";
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("DrawBuffer"), 0u)
|
||||||
|
<< "every block of the engine would otherwise be defaulted onto 0 together";
|
||||||
|
// The two shapes the scanner could only treat as DOUBT - a macro standing in for a whole
|
||||||
|
// layout entry, and one standing in for the whole qualifier run - are now ordinary.
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("EntryMacro"), 0u)
|
||||||
|
<< "a macro that expands to `binding = N` declares a binding";
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("RunMacro"), 0u)
|
||||||
|
<< "a macro standing in for the whole qualifier run carries its binding too";
|
||||||
|
// The counterweight: doubt must not swallow the layout identifiers a buffer block legally
|
||||||
|
// carries, or nothing would ever be defaulted again.
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("PlainLayout"), 1u)
|
||||||
|
<< "std430/row_major are layout identifiers, not bindings";
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("ReallyUnqualified"), 1u)
|
||||||
|
<< "a block that truly declares no binding is still recognised in the same shader";
|
||||||
|
}
|
||||||
|
|
||||||
|
// THE NEGATIVE CONTROL for the storage-block capture.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, UnqualifiedStorageBlocksDisappearWhenTheResolverCaptureIsOff) {
|
||||||
|
const String source = R"(#version 430 core
|
||||||
|
layout(local_size_x = 1) in;
|
||||||
|
layout(std430) buffer Unbound { uint u; } unbound;
|
||||||
|
void main() { unbound.u = 1u; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
ASSERT_EQ(CaptureFromCompute(source).storageBlocksWithoutBinding.count("Unbound"), 1u);
|
||||||
|
const LinkCapture off = CaptureFromCompute(source, /*captureEnabled=*/false);
|
||||||
|
ASSERT_TRUE(off.linked) << off.log;
|
||||||
|
EXPECT_TRUE(off.storageBlocksWithoutBinding.empty())
|
||||||
|
<< "nothing but the resolver fills this set; a non-empty result would mean the capture "
|
||||||
|
"is being shadowed by a leftover path";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A block declared in two stages contributes ONCE, and the capture is a union across them -
|
||||||
|
// which is what one resolver serving the whole program gives for free. GLSL requires every
|
||||||
|
// stage that declares a block to declare it identically, so the stages cannot disagree.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, TheStorageBlockCaptureIsAUnionAcrossStages) {
|
||||||
|
const String vertex = R"(#version 430 core
|
||||||
|
layout(std430) buffer SharedBlock { uint u; } sharedInstance;
|
||||||
|
layout(std430) buffer VertexOnly { uint v; } vertexOnly;
|
||||||
|
void main() { gl_Position = vec4(float(sharedInstance.u + vertexOnly.v)); }
|
||||||
|
)";
|
||||||
|
const String fragment = R"(#version 430 core
|
||||||
|
layout(std430) buffer SharedBlock { uint u; } sharedInstance;
|
||||||
|
layout(std430, binding = 4) buffer FragmentBound { uint f; } fragmentBound;
|
||||||
|
out vec4 colour;
|
||||||
|
void main() { colour = vec4(float(sharedInstance.u + fragmentBound.f)); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
const LinkCapture capture =
|
||||||
|
CaptureFromLink({{GL_VERTEX_SHADER, vertex}, {GL_FRAGMENT_SHADER, fragment}});
|
||||||
|
ASSERT_TRUE(capture.linked) << capture.log;
|
||||||
|
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("SharedBlock"), 1u);
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("VertexOnly"), 1u);
|
||||||
|
EXPECT_EQ(capture.storageBlocksWithoutBinding.count("FragmentBound"), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
// KHR-GL43.shader_atomic_counters.negative-offset-1: an atomic counter at a misaligned offset,
|
||||||
|
// or one whose last byte passes GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, is a COMPILE-time error
|
||||||
|
// (GL 4.6 core 7.7). glslang enforces the alignment rule in fixOffset(), which the relaxed
|
||||||
|
// parse never reaches - vkRelaxedRemapUniformVariable folds the counter into a synthesized
|
||||||
|
// block and returns from declareVariable() first - and it never enforced the size ceiling at
|
||||||
|
// all. Both now run at that fold (ParseHelper.cpp atomicCounterOffsetCheck), so a violation is
|
||||||
|
// an ordinary parse failure.
|
||||||
|
TEST_F(GlslangCaptureProbeTest, AtomicCounterOffsetRulesAreRaisedByTheParse) {
|
||||||
|
const auto compiles = [](const String& body) {
|
||||||
|
const String source = "#version 430 core\n" + body + "void main() {}\n";
|
||||||
|
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||||
|
return ShaderCompiler::CompileShader(attrib).has_value();
|
||||||
|
};
|
||||||
|
const String maxSize = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
|
||||||
|
const String lastLegal = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 4);
|
||||||
|
|
||||||
|
// The boundary itself: the last counter that still fits, and the first that does not.
|
||||||
|
EXPECT_TRUE(compiles("layout(binding = 0, offset = " + lastLegal + ") uniform atomic_uint c;\n"));
|
||||||
|
EXPECT_FALSE(compiles("layout(binding = 0, offset = " + maxSize + ") uniform atomic_uint c;\n"));
|
||||||
|
|
||||||
|
// An array occupies one word per element, so what has to fit is the LAST one.
|
||||||
|
EXPECT_TRUE(compiles("layout(binding = 0, offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 16) +
|
||||||
|
") uniform atomic_uint c[4];\n"));
|
||||||
|
EXPECT_FALSE(compiles("layout(binding = 0, offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 8) +
|
||||||
|
") uniform atomic_uint c[4];\n"));
|
||||||
|
|
||||||
|
// An offset that is not a multiple of 4, and one that is.
|
||||||
|
EXPECT_FALSE(compiles("layout(binding = 0, offset = 2) uniform atomic_uint c;\n"));
|
||||||
|
EXPECT_TRUE(compiles("layout(binding = 0, offset = 8) uniform atomic_uint c;\n"));
|
||||||
|
|
||||||
|
// A counter with no explicit offset has nothing to judge, and neither has a shader with no
|
||||||
|
// counter at all.
|
||||||
|
EXPECT_TRUE(compiles("layout(binding = 0) uniform atomic_uint c;\n"));
|
||||||
|
EXPECT_TRUE(compiles(""));
|
||||||
|
|
||||||
|
// The gain over the scan this replaces: an array sized by a constant EXPRESSION, and an
|
||||||
|
// offset spelled as a macro, are now both judged. The scanner declined both - it read
|
||||||
|
// unexpanded text and only understood integer literals.
|
||||||
|
EXPECT_FALSE(compiles("const int kCount = 4;\nlayout(binding = 0, offset = " +
|
||||||
|
std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 8) +
|
||||||
|
") uniform atomic_uint c[kCount];\n"));
|
||||||
|
EXPECT_FALSE(compiles("#define BAD_OFFSET 2\nlayout(binding = 0, offset = BAD_OFFSET) uniform atomic_uint c;\n"));
|
||||||
|
|
||||||
|
// The counterweight: `offset` as an ordinary identifier is not a layout qualifier, and an
|
||||||
|
// offset qualifier on an unrelated declaration must not reach the counter.
|
||||||
|
EXPECT_TRUE(compiles("layout(binding = 0) uniform atomic_uint c;\nconst int offset = 99999;\n"));
|
||||||
|
}
|
||||||
@@ -475,11 +475,11 @@ TEST_F(TranslationCacheTest, L1KeyMovesWithEveryInputThatMovesTheSpirv) {
|
|||||||
v.explicitFragmentOutIndices = &fragIndex;
|
v.explicitFragmentOutIndices = &fragIndex;
|
||||||
variants.emplace_back("explicitFragmentOutIndices", BuildSpirvTranslationKey(v));
|
variants.emplace_back("explicitFragmentOutIndices", BuildSpirvTranslationKey(v));
|
||||||
}
|
}
|
||||||
{ // the merged layout(binding = N) opaque units
|
// NOT the merged layout(binding = N) opaque units, which used to be a variant here: that
|
||||||
SpirvTranslationKeyInputs v = base;
|
// map is an OUTPUT of mapIO (TMglGlslIoResolver writes it and never reads it), so it is a
|
||||||
v.explicitOpaqueUniformBindings = &opaque;
|
// pure function of the stage sources this key already carries in full. It was dropped from
|
||||||
variants.emplace_back("explicitOpaqueUniformBindings", BuildSpirvTranslationKey(v));
|
// SpirvTranslationKeyInputs with the glslang-capture migration; kKeyLayoutVersion moved to
|
||||||
}
|
// 4 so no blob written under the old shape can be honoured.
|
||||||
{ // ShaderCompileBits (0 on both production parse paths; keyed so a future value
|
{ // ShaderCompileBits (0 on both production parse paths; keyed so a future value
|
||||||
// cannot alias a module parsed without it)
|
// cannot alias a module parsed without it)
|
||||||
SpirvTranslationKeyInputs v = base;
|
SpirvTranslationKeyInputs v = base;
|
||||||
|
|||||||
@@ -358,6 +358,107 @@ namespace MobileGL {
|
|||||||
glslang::SetThreadPoolAllocator(nullptr);
|
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) {
|
Result<SharedPtr<glslang::TProgram>> ShaderCompiler::LinkProgram(const ProgramAttrib& attrib) {
|
||||||
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
|
SharedPtr<glslang::TProgram> program = MakeShared<glslang::TProgram>();
|
||||||
for (auto& s : attrib.shaders) {
|
for (auto& s : attrib.shaders) {
|
||||||
@@ -383,7 +484,8 @@ namespace MobileGL {
|
|||||||
MakeUnique<TMglGlslIoResolver>(*program, (EShLanguage)stage, attrib.explicitVertexInLocations,
|
MakeUnique<TMglGlslIoResolver>(*program, (EShLanguage)stage, attrib.explicitVertexInLocations,
|
||||||
attrib.explicitFragmentOutLocations,
|
attrib.explicitFragmentOutLocations,
|
||||||
attrib.explicitFragmentOutIndices,
|
attrib.explicitFragmentOutIndices,
|
||||||
attrib.explicitOpaqueUniformBindings);
|
attrib.explicitOpaqueUniformBindings,
|
||||||
|
attrib.storageBlocksWithoutBinding);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
|
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
|
||||||
|
|||||||
@@ -445,6 +445,24 @@ namespace MobileGL {
|
|||||||
// identical question for the identical decision.
|
// identical question for the identical decision.
|
||||||
static Bool ModuleReadsLocatedInput(const Vector<Uint32>& spirv);
|
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 ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -1621,22 +1621,6 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
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) {
|
bool IsNonLayoutQualifierKeyword(const String& text) {
|
||||||
static const char* kQualifiers[] = {
|
static const char* kQualifiers[] = {
|
||||||
"highp", "mediump", "lowp", "precise", "const", "flat",
|
"highp", "mediump", "lowp", "precise", "const", "flat",
|
||||||
@@ -1669,266 +1653,8 @@ namespace MobileGL {
|
|||||||
return true;
|
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
|
} // 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 {
|
namespace {
|
||||||
// Binding points a storage-block declaration starting at `bufferPos` occupies.
|
// Binding points a storage-block declaration starting at `bufferPos` occupies.
|
||||||
// One for a scalar instance (and for the "layout(...) buffer;" default-qualifier
|
// One for a scalar instance (and for the "layout(...) buffer;" default-qualifier
|
||||||
@@ -1967,129 +1693,6 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // 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) {
|
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings) {
|
||||||
// A backend that advertises nothing has no ceiling to enforce.
|
// A backend that advertises nothing has no ceiling to enforce.
|
||||||
if (maxBindings <= 0) return std::nullopt;
|
if (maxBindings <= 0) return std::nullopt;
|
||||||
@@ -2102,8 +1705,8 @@ namespace MobileGL {
|
|||||||
const Vector<CodeToken> tokens = TokenizeCode(source);
|
const Vector<CodeToken> tokens = TokenizeCode(source);
|
||||||
const SizeT count = tokens.size();
|
const SizeT count = tokens.size();
|
||||||
// The binding the qualifier run currently being scanned declared, -1 for none.
|
// The binding the qualifier run currently being scanned declared, -1 for none.
|
||||||
// Several layout(...) lists may precede one declaration and the later one wins,
|
// Several layout(...) lists may precede one declaration and the later one wins:
|
||||||
// which is the same accumulate-then-consume shape the extractors above use.
|
// accumulate, then consume at the `buffer` keyword.
|
||||||
long long binding = -1;
|
long long binding = -1;
|
||||||
long long literal = 0;
|
long long literal = 0;
|
||||||
for (SizeT pos = 0; pos < count; ++pos) {
|
for (SizeT pos = 0; pos < count; ++pos) {
|
||||||
@@ -2146,137 +1749,6 @@ namespace MobileGL {
|
|||||||
return std::nullopt;
|
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 ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -45,68 +45,44 @@ namespace MobileGL {
|
|||||||
// compile-error text for the first violation, or nullopt for a clean source.
|
// compile-error text for the first violation, or nullopt for a clean source.
|
||||||
std::optional<String> FindReservedIdentifierViolation(const String& source);
|
std::optional<String> FindReservedIdentifierViolation(const String& source);
|
||||||
|
|
||||||
// Explicit layout(location = N) qualifiers on default-block uniform declarations,
|
// NO SIDE-CHANNEL EXTRACTORS LIVE HERE ANY MORE. Three of them did - explicit
|
||||||
// keyed by declared name (no "[0]" suffix). Multi-declarator statements assign
|
// default-block uniform locations, explicit sampler/image bindings, and the storage
|
||||||
// consecutive locations, advancing by the array element count.
|
// blocks that declared no binding - each recovering something MobileGL's
|
||||||
//
|
// Vulkan-client relaxed parse destroys. All three are now taken from glslang at the
|
||||||
// Exists because the single link-compatible parse runs under relaxed Vulkan rules,
|
// point of destruction instead:
|
||||||
// where glslang's vkRelaxedRemapUniformVariable moves plain uniforms into
|
// * uniform locations: a snapshot inside vkRelaxedRemapUniformVariable, read back
|
||||||
// MGL_GLOBAL_UBO and DISCARDS their location qualifiers ("ignoring layout qualifier
|
// through CollectExplicitUniformLocations (ShaderCompiler.h);
|
||||||
// for uniform location"); opaque uniforms keep theirs. This lexical side-channel
|
// * opaque bindings and unqualified storage blocks:
|
||||||
// restores the discarded locations to the GL location assigner
|
// TMglGlslIoResolver::reserverResourceSlot, which mapIO calls while the
|
||||||
// (ProgramObject::DoReflection). It scans preprocessor-visible text, so a
|
// qualifier still says what the shader declared.
|
||||||
// declaration inside an inactive #if branch is still recorded - harmless unless a
|
// The rewrites below stay lexical by construction - they exist to make glslang
|
||||||
// pack declares the same uniform with different explicit locations in alternative
|
// ACCEPT input it would otherwise reject, so they cannot be built on its parse.
|
||||||
// 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);
|
|
||||||
|
|
||||||
// A shader storage block whose layout(binding = N) reaches or passes
|
// 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,
|
// 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
|
// 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
|
// has to fit. Returns the compile-error text for the first violation, or nullopt for
|
||||||
// client under relaxed rules, where the GL ceilings do not apply, and TBuiltInResource
|
// a clean source. `maxBindings` is what glGetIntegerv answers for that pname; a
|
||||||
// has no storage-buffer binding field to check against in the first place. Returns the
|
// non-positive value means "nothing to check against" and every declaration passes.
|
||||||
// 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
|
// THE ONE SCAN THAT COULD NOT MOVE TO GLSLANG, and the reason is structural rather
|
||||||
// means "nothing to check against" and every declaration passes.
|
// 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);
|
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 ShaderTranspiler
|
||||||
} // namespace MG_Util
|
} // namespace MG_Util
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
// 2: L2 gained atomicCounterEsslBindingTop (wave3's atomic-counter block rebinding
|
// 2: L2 gained atomicCounterEsslBindingTop (wave3's atomic-counter block rebinding
|
||||||
// prints it into the emitted ESSL), and L1c was added.
|
// prints it into the emitted ESSL), and L1c was added.
|
||||||
// 3: L2 gained the two interface-block rename maps (wave4's UniquifyIoBlockNames).
|
// 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
|
// The repo's existing cache epoch (MG_Config::CacheVersion, the seed
|
||||||
// ProgramFactory::ComputeHash uses). Strictly redundant for an in-memory
|
// 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.explicitVertexInLocations ? *inputs.explicitVertexInLocations : kEmpty);
|
||||||
builder.NameMap(inputs.explicitFragmentOutLocations ? *inputs.explicitFragmentOutLocations : kEmpty);
|
builder.NameMap(inputs.explicitFragmentOutLocations ? *inputs.explicitFragmentOutLocations : kEmpty);
|
||||||
builder.NameMap(inputs.explicitFragmentOutIndices ? *inputs.explicitFragmentOutIndices : kEmpty);
|
builder.NameMap(inputs.explicitFragmentOutIndices ? *inputs.explicitFragmentOutIndices : kEmpty);
|
||||||
builder.NameMap(inputs.explicitOpaqueUniformBindings ? *inputs.explicitOpaqueUniformBindings : kEmpty);
|
|
||||||
static const Vector<String> kNoXfb;
|
static const Vector<String> kNoXfb;
|
||||||
builder.TextList(inputs.requestedXfbVaryings ? *inputs.requestedXfbVaryings : kNoXfb);
|
builder.TextList(inputs.requestedXfbVaryings ? *inputs.requestedXfbVaryings : kNoXfb);
|
||||||
builder.Value(inputs.xfbBufferMode);
|
builder.Value(inputs.xfbBufferMode);
|
||||||
@@ -146,7 +149,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
return MakeTranslationCacheKey(builder);
|
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.
|
// Leaked for the same exit-order reason as the other two; see the note below.
|
||||||
BoundedTranslationCache<ShaderParseVerdict>& GetShaderParseVerdictCache() {
|
BoundedTranslationCache<ShaderParseVerdict>& GetShaderParseVerdictCache() {
|
||||||
|
|||||||
@@ -365,11 +365,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
|||||||
// covers backend identity and the advertised extension vector;
|
// covers backend identity and the advertised extension vector;
|
||||||
// * per stage, in link order: the GL stage enum and the FULL preprocessed
|
// * per stage, in link order: the GL stage enum and the FULL preprocessed
|
||||||
// source, which is literally the text ParseShaderSource was given;
|
// 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 /
|
// (glBindAttribLocation / glBindFragDataLocation /
|
||||||
// glBindFragDataLocationIndexed, and the merged layout(binding=) opaque
|
// glBindFragDataLocationIndexed) - these steer TMglGlslIoResolver and
|
||||||
// units) - these steer TMglGlslIoResolver and therefore the Locations and
|
// therefore the Locations and Bindings baked into every module. NOT the
|
||||||
// Bindings baked into every module;
|
// 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 ShaderCompileBits the parse ran under (always 0 in production; in
|
||||||
// the key so a future non-zero value cannot alias);
|
// the key so a future non-zero value cannot alias);
|
||||||
// * the SPIR-V validation switch (byte-identical output either way, but it
|
// * 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>* explicitVertexInLocations = nullptr;
|
||||||
const UnorderedMap<String, Uint>* explicitFragmentOutLocations = nullptr;
|
const UnorderedMap<String, Uint>* explicitFragmentOutLocations = nullptr;
|
||||||
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
|
const UnorderedMap<String, Uint>* explicitFragmentOutIndices = nullptr;
|
||||||
const UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
|
|
||||||
Uint32 shaderCompileFlags = 0;
|
Uint32 shaderCompileFlags = 0;
|
||||||
Bool enableSpirvValidation = false;
|
Bool enableSpirvValidation = false;
|
||||||
// ---- inputs that only matter because the PAYLOAD now carries the reflection ----
|
// ---- 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
|
// 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.
|
// glslang wrote into it. Stored rather than assumed so the two cannot drift.
|
||||||
String infoLog;
|
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>;
|
using ShaderParseVerdictPtr = SharedPtr<const ShaderParseVerdict>;
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,13 @@ namespace MobileGL {
|
|||||||
// Dual-source blend color index per fragment output (glBindFragDataLocationIndexed) ->
|
// Dual-source blend color index per fragment output (glBindFragDataLocationIndexed) ->
|
||||||
// emitted as layout(index = N).
|
// emitted as layout(index = N).
|
||||||
UnorderedMap<String, Uint> explicitFragmentOutIndices;
|
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;
|
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
|
||||||
|
std::set<String>* storageBlocksWithoutBinding = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ProgramBinaryAttrib {
|
struct ProgramBinaryAttrib {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
|
|
||||||
#include "TMglGlslIoResolver.h"
|
#include "TMglGlslIoResolver.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
namespace MobileGL {
|
namespace MobileGL {
|
||||||
bool TMglGlslIoResolver::ShouldAssignPlainUniformLocation(const glslang::TType& type) const {
|
bool TMglGlslIoResolver::ShouldAssignPlainUniformLocation(const glslang::TType& type) const {
|
||||||
if (!doAutoLocationMapping()) {
|
if (!doAutoLocationMapping()) {
|
||||||
@@ -149,12 +153,47 @@ namespace MobileGL {
|
|||||||
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
|
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) {
|
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
|
||||||
const glslang::TType& type = ent.symbol->getType();
|
const glslang::TType& type = ent.symbol->getType();
|
||||||
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
|
const glslang::TQualifier& qualifier = type.getQualifier();
|
||||||
type.getQualifier().hasBinding()) {
|
// 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();
|
const glslang::TString& name = ent.symbol->getAccessName();
|
||||||
(*m_explicitOpaqueUniformBindings)[name.c_str()] = type.getQualifier().layoutBinding;
|
|
||||||
|
if (m_explicitOpaqueUniformBindings != nullptr && type.getBasicType() == glslang::EbtSampler &&
|
||||||
|
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);
|
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <glslang/Public/ShaderLang.h>
|
#include <glslang/Public/ShaderLang.h>
|
||||||
@@ -27,14 +28,17 @@ namespace MobileGL {
|
|||||||
using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
|
using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
|
||||||
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
|
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
|
||||||
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
|
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
|
||||||
ExplicitVarSlotMap* opaqueUniformBindings)
|
ExplicitVarSlotMap* opaqueUniformBindings,
|
||||||
|
std::set<String>* storageBlocksWithoutBinding = nullptr)
|
||||||
: TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts),
|
: 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,
|
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
|
||||||
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
|
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,
|
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices,
|
||||||
opaqueUniformBindings) {}
|
opaqueUniformBindings, storageBlocksWithoutBinding) {}
|
||||||
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||||
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
|
||||||
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
|
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
|
||||||
@@ -47,7 +51,17 @@ namespace MobileGL {
|
|||||||
const ExplicitVarSlotMap& m_explicitVertexIns;
|
const ExplicitVarSlotMap& m_explicitVertexIns;
|
||||||
const ExplicitVarSlotMap& m_explicitFragOuts;
|
const ExplicitVarSlotMap& m_explicitFragOuts;
|
||||||
const ExplicitVarSlotMap& m_explicitFragOutIndices;
|
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;
|
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_plainUniformLocationSizeByName;
|
||||||
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
||||||
bool m_plainUniformLocationsAssigned = false;
|
bool m_plainUniformLocationsAssigned = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user