[Fix, Test] (ShaderTranspiler): parse layout literals in every GLSL base and key array-of-arrays uniforms per element

This commit is contained in:
2026-08-20 13:24:04 -04:00
parent 6359fba455
commit de8e7a4606
2 changed files with 203 additions and 19 deletions
@@ -3946,3 +3946,122 @@ TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
// A backend that advertises no binding points has no ceiling to enforce.
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).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);
}
@@ -10,6 +10,7 @@
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <climits>
#include <cstdlib>
#include <initializer_list>
@@ -1228,10 +1229,65 @@ namespace MobileGL {
return false;
}
bool IsDecimalIntegerToken(const String& text) {
if (text.empty()) return false;
return std::all_of(text.begin(), text.end(),
[](char ch) { return ch >= '0' && ch <= '9'; });
// One GLSL integer literal, spelled the C way: "0x"/"0X" is hexadecimal, a leading
// '0' is OCTAL, everything else decimal, and a single trailing 'u'/'U' is legal.
// strtoll with base 0 already implements exactly that detection, so the only work
// here is deciding what the tail is allowed to be.
//
// Never guesses, which is the discipline every caller depends on: a float ("1.0"),
// an unknown suffix ("3f"), an out-of-range run and a negative value all return
// false, and the caller skips the declaration rather than recording a wrong number.
bool ParseGlslIntegerLiteral(const String& text, long long& out) {
if (text.empty() || text.front() < '0' || text.front() > '9') return false;
errno = 0;
char* tail = nullptr;
const long long value = std::strtoll(text.c_str(), &tail, 0);
if (tail == text.c_str() || errno == ERANGE || value < 0) return false;
const String suffix = text.substr(static_cast<SizeT>(tail - text.c_str()));
if (!suffix.empty() && suffix != "u" && suffix != "U") return false;
out = value;
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
@@ -1244,6 +1300,7 @@ namespace MobileGL {
MobileGL::UnorderedMap<String, MobileGL::Int>& locations) {
using MobileGL::Int;
long long location = -1;
long long literal = 0;
bool sawUniform = false;
SizeT declaratorBegin = end;
@@ -1259,9 +1316,9 @@ namespace MobileGL {
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "location" && j + 2 < end &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
location = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
static_cast<long long>(INT_MAX / 2));
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
location = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
@@ -1290,21 +1347,25 @@ namespace MobileGL {
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 && IsDecimalIntegerToken(tokens[k].text)) {
dimension = std::strtoll(tokens[k].text.c_str(), nullptr, 10);
if (k < end && ParseGlslIntegerLiteral(tokens[k].text, literal)) {
dimension = literal;
++k;
}
if (k >= end || tokens[k].text != "]") return; // sized by expression; bail out
++k;
span *= std::max(1ll, std::min(dimension, static_cast<long long>(INT_MAX / 2)));
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
@@ -1340,6 +1401,7 @@ namespace MobileGL {
MobileGL::UnorderedMap<String, MobileGL::Uint>& bindings) {
using MobileGL::Int;
long long binding = -1;
long long literal = 0;
bool sawUniform = false;
SizeT declaratorBegin = end;
@@ -1355,9 +1417,9 @@ namespace MobileGL {
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < end &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
static_cast<long long>(INT_MAX / 2));
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
@@ -1390,7 +1452,7 @@ namespace MobileGL {
++k;
while (k < end && tokens[k].text == "[") {
++k;
if (k < end && IsDecimalIntegerToken(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;
}
@@ -1476,8 +1538,10 @@ namespace MobileGL {
if (k < count && IsIdentifierToken(tokens[k])) ++k; // instance name
if (k >= count || tokens[k].text != "[") return 1;
if (k + 2 < count && IsDecimalIntegerToken(tokens[k + 1].text) && tokens[k + 2].text == "]") {
return std::max<long long>(1, std::strtoll(tokens[k + 1].text.c_str(), nullptr, 10));
long long elementCount = 0;
if (k + 2 < count && ParseGlslIntegerLiteral(tokens[k + 1].text, elementCount) &&
tokens[k + 2].text == "]") {
return std::max<long long>(1, elementCount);
}
return -1; // sized by an expression, or unsized
}
@@ -1498,6 +1562,7 @@ namespace MobileGL {
// Several layout(...) lists may precede one declaration and the later one wins,
// which is the same accumulate-then-consume shape the extractors above use.
long long binding = -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 == "(") {
@@ -1510,9 +1575,9 @@ namespace MobileGL {
} else if (layoutToken == ")") {
--parenDepth;
} else if (parenDepth == 1 && layoutToken == "binding" && j + 2 < count &&
tokens[j + 1].text == "=" && IsDecimalIntegerToken(tokens[j + 2].text)) {
binding = std::min(std::strtoll(tokens[j + 2].text.c_str(), nullptr, 10),
static_cast<long long>(INT_MAX / 2));
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;