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