[Merge] (CTS): land the GL43 wave-3 fixes and the DirectVulkan texture-shape repairs

This commit is contained in:
2026-08-20 14:17:20 -04:00
51 changed files with 3443 additions and 161 deletions
+9 -5
View File
@@ -521,9 +521,11 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("64-bit vertex attributes",
"not supported (ES has no GL_DOUBLE vertex format, and after the fp64 demotion "
"above there is no 64-bit shader input left to feed either); "
"glVertexAttribLFormat / glVertexArrayAttribLFormat report "
"GL_INVALID_OPERATION - feed the attribute with glVertexAttribPointer(GL_FLOAT), "
"which a demoted dvec input reads correctly");
"glVertexAttribLFormat / glVertexArrayAttribLFormat succeed and their state is "
"queryable, but an ENABLED 64-bit array is DROPPED at draw and the attribute "
"reads its generic current value - feed the attribute with "
"glVertexAttribPointer(GL_FLOAT) instead, which a demoted dvec input reads "
"correctly");
if (glesFuncs.glPatchParameteri != nullptr) {
builder.Pass("Tessellation patch parameters",
"glPatchParameteri present (GL_PATCH_VERTICES reaches the driver)");
@@ -2336,8 +2338,10 @@ namespace MobileGL::MG_Util::SelfTest {
builder.Warn("64-bit vertex attributes",
"not supported; there is no 64-bit shader input left to feed after the fp64 demotion "
"above, and no VK_FORMAT_R64*_SFLOAT vertex fetch to feed it with on most devices "
"anyway. glVertexAttribLFormat reports GL_INVALID_OPERATION - feed the attribute with "
"glVertexAttribPointer(GL_FLOAT), which a demoted dvec input reads correctly");
"anyway. glVertexAttribLFormat succeeds and its state is queryable, but an ENABLED "
"64-bit array is DROPPED at pipeline build and the attribute reads its generic "
"current value - feed the attribute with glVertexAttribPointer(GL_FLOAT) instead, "
"which a demoted dvec input reads correctly");
Bool shaderDrawParameters = false;
if (vkGetPhysicalDeviceFeatures2Fn != nullptr && properties.apiVersion >= VK_API_VERSION_1_1) {
@@ -28,6 +28,9 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
HashValue(state, env.maxComputeWorkGroupSize[0]);
HashValue(state, env.maxComputeWorkGroupSize[1]);
HashValue(state, env.maxComputeWorkGroupSize[2]);
HashValue(state, env.maxComputeWorkGroupCount[0]);
HashValue(state, env.maxComputeWorkGroupCount[1]);
HashValue(state, env.maxComputeWorkGroupCount[2]);
HashValue(state, env.maxComputeWorkGroupInvocations);
HashValue(state, env.backend);
// DynamicBackendParameters is a plain aggregate of scalars; hashing its object
@@ -51,19 +54,23 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions;
}
// GL_MAX_COMPUTE_WORK_GROUP_SIZE. This is a REAL driver call on DirectGLES; it must
// happen here, on the context thread, and exactly once per context. The frontend
// minimum is the floor, matching what GL_Getter reports.
// TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima.
constexpr Uint kFrontendMinComputeWorkGroupSizes[3] = {1024, 1024, 64};
// GL_MAX_COMPUTE_WORK_GROUP_SIZE / _COUNT. These are REAL driver calls on DirectGLES; they
// must happen here, on the context thread, and exactly once per context. The frontend
// minimum is the floor, matching what GL_Getter reports - both sides now floor at the
// shared MIN_COMPUTE_WORK_GROUP_* constants rather than at their own copy of them.
for (Uint index = 0; index < 3; ++index) {
Int backendValue = 0;
Int backendSize = 0;
Int backendCount = 0;
if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) {
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index,
&backendValue);
&backendSize);
MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, index,
&backendCount);
}
env->maxComputeWorkGroupSize[index] =
std::max(static_cast<Uint>(std::max(backendValue, 0)), kFrontendMinComputeWorkGroupSizes[index]);
std::max(static_cast<Uint>(std::max(backendSize, 0)), MIN_COMPUTE_WORK_GROUP_SIZE[index]);
env->maxComputeWorkGroupCount[index] =
std::max(static_cast<Uint>(std::max(backendCount, 0)), MIN_COMPUTE_WORK_GROUP_COUNT[index]);
}
constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024;
+19 -1
View File
@@ -12,6 +12,18 @@
#include <MG_Backend/BackendObject.h>
namespace MobileGL::MG_Util::ShaderTranspiler {
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE core minimums (GL 4.6 core table 23.45), in ONE
// place because three separate readers have to agree on them: CaptureCompileEnv (which floors
// the backend's answer at them), GL_Getter (which answers the same query the same way) and
// BuildTBuiltInResource (whose gl_MaxComputeWorkGroup* constants a shader compares against
// the query - KHR-GL43.compute_shader.max does exactly that). They used to be three copies,
// and the z one disagreed: glslang compiled against 1024 while the context advertised 64.
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_COUNT[3] = {65535, 65535, 65535};
inline constexpr Uint MIN_COMPUTE_WORK_GROUP_SIZE[3] = {1024, 1024, 64};
// GL_MAX_COMPUTE_UNIFORM_COMPONENTS, the same invariant with no backend input: the number
// glGetIntegerv answers and the number gl_MaxComputeUniformComponents expands to.
inline constexpr Int MAX_COMPUTE_UNIFORM_COMPONENTS = 1024;
// everything outside (stage, source) this reads - advertised extensions and backend limits -
// so the transformation is a pure function of its three arguments and can run on a worker
// thread.
@@ -34,7 +46,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
struct CompileEnv {
// --- compute limits: the ONLY former real-driver read in the pipeline ---
// GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum.
Uint maxComputeWorkGroupSize[3] = {1024, 1024, 64};
Uint maxComputeWorkGroupSize[3] = {MIN_COMPUTE_WORK_GROUP_SIZE[0], MIN_COMPUTE_WORK_GROUP_SIZE[1],
MIN_COMPUTE_WORK_GROUP_SIZE[2]};
// GL_MAX_COMPUTE_WORK_GROUP_COUNT, likewise. Carried for the same reason the size is:
// gl_MaxComputeWorkGroupCount expands from it at parse time, so the compile pipeline
// needs the number the context advertises without reaching back to the live backend.
Uint maxComputeWorkGroupCount[3] = {MIN_COMPUTE_WORK_GROUP_COUNT[0], MIN_COMPUTE_WORK_GROUP_COUNT[1],
MIN_COMPUTE_WORK_GROUP_COUNT[2]};
// GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise.
Uint64 maxComputeWorkGroupInvocations = 1024;
@@ -83,18 +83,11 @@ namespace MobileGL {
Resources.minProgramTexelOffset = -8;
Resources.maxProgramTexelOffset = 7;
Resources.maxClipDistances = 8;
Resources.maxComputeWorkGroupCountX = 65535;
Resources.maxComputeWorkGroupCountY = 65535;
Resources.maxComputeWorkGroupCountZ = 65535;
Resources.maxComputeWorkGroupSizeX = 1024;
Resources.maxComputeWorkGroupSizeY = 1024;
// TODO: Drive glslang compute resource limits from the active backend instead of this permissive cap.
Resources.maxComputeWorkGroupSizeZ = 1024;
Resources.maxComputeUniformComponents = 1024;
Resources.maxComputeUniformComponents = MAX_COMPUTE_UNIFORM_COMPONENTS;
Resources.maxComputeTextureImageUnits = 16;
Resources.maxComputeImageUniforms = 8;
Resources.maxComputeAtomicCounters = 8;
Resources.maxComputeAtomicCounterBuffers = 1;
Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxVaryingComponents = 60;
Resources.maxVertexOutputComponents = 64;
Resources.maxGeometryInputComponents = 64;
@@ -132,16 +125,22 @@ namespace MobileGL {
Resources.maxTessControlAtomicCounters = 0;
Resources.maxTessEvaluationAtomicCounters = 0;
Resources.maxGeometryAtomicCounters = 0;
Resources.maxFragmentAtomicCounters = 8;
Resources.maxCombinedAtomicCounters = 8;
Resources.maxAtomicCounterBindings = 1;
Resources.maxFragmentAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
Resources.maxCombinedAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE;
// Every atomic-counter limit below is the one glGetIntegerv answers; the shared
// constants in Types.h are what keeps the two sides from drifting apart again.
// gl_MaxAtomicCounterBindings and gl_MaxAtomicCounterBufferSize expand from these
// (Initialize.cpp), and the binding count is also the ceiling glslang checks a
// `layout(binding = N) uniform atomic_uint` against - it was 1, so every counter
// outside binding 0 failed to compile.
Resources.maxAtomicCounterBindings = MAX_ATOMIC_COUNTER_BUFFER_BINDINGS;
Resources.maxVertexAtomicCounterBuffers = 0;
Resources.maxTessControlAtomicCounterBuffers = 0;
Resources.maxTessEvaluationAtomicCounterBuffers = 0;
Resources.maxGeometryAtomicCounterBuffers = 0;
Resources.maxFragmentAtomicCounterBuffers = 1;
Resources.maxCombinedAtomicCounterBuffers = 1;
Resources.maxAtomicCounterBufferSize = 16384;
Resources.maxFragmentAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxCombinedAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE;
Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE;
Resources.maxTransformFeedbackBuffers = 4;
Resources.maxTransformFeedbackInterleavedComponents = 64;
Resources.maxCullDistances = 8;
@@ -173,6 +172,25 @@ namespace MobileGL {
Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms;
Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms;
Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms;
Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits;
// The compute work-group limits are the env's, not the backend parameters': they
// are the only ones that come from a REAL indexed driver query, which
// CaptureCompileEnv already issued once on the GL thread and floored at the core
// minimum exactly as GL_Getter does. Reading the same snapshot here is what makes
// gl_MaxComputeWorkGroupSize and glGetIntegeri_v agree by construction
// (KHR-GL43.compute_shader.max compares them); the z component was 1024 here
// against the 64 every ES driver reports. A null env is the standalone/test entry
// point, which has no context to have queried one - the core minimums stand, which
// is what a default-constructed CompileEnv carries anyway.
const Uint* maxWorkGroupSize = env ? env->maxComputeWorkGroupSize : MIN_COMPUTE_WORK_GROUP_SIZE;
const Uint* maxWorkGroupCount = env ? env->maxComputeWorkGroupCount : MIN_COMPUTE_WORK_GROUP_COUNT;
Resources.maxComputeWorkGroupSizeX = static_cast<int>(maxWorkGroupSize[0]);
Resources.maxComputeWorkGroupSizeY = static_cast<int>(maxWorkGroupSize[1]);
Resources.maxComputeWorkGroupSizeZ = static_cast<int>(maxWorkGroupSize[2]);
Resources.maxComputeWorkGroupCountX = static_cast<int>(maxWorkGroupCount[0]);
Resources.maxComputeWorkGroupCountY = static_cast<int>(maxWorkGroupCount[1]);
Resources.maxComputeWorkGroupCountZ = static_cast<int>(maxWorkGroupCount[2]);
Resources.limits.nonInductiveForLoops = true;
Resources.limits.whileLoops = true;
@@ -10,6 +10,7 @@
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <climits>
#include <cstdlib>
#include <initializer_list>
@@ -818,6 +819,116 @@ namespace {
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
}
// GLSL 4.30 4.1.9 allows an interface-block member array to be left unsized when it is NOT the
// last member; it is then implicitly sized by the largest constant index the shader uses.
// glslang implements the SIZING - adoptImplicitArraySizes, at link - but computes the block's
// member OFFSETS at DECLARATION time (fixBlockUniformOffsets), where the array is still
// unsized and so contributes zero bytes. Every member after it is therefore laid out on top of
// it: `vec4 a[]; vec4 b;` puts BOTH at offset 0, and a shader reading `b` gets `a[0]`
// (KHR-GL43.shader_storage_buffer_object.basic-syntax iteration 6, whose degenerate triangle
// rasterizes nothing at all).
//
// The source level is the only place the two can be reconciled, because the offset pass runs
// before a single statement has been parsed. Deliberately narrow: it fires only on a `buffer`
// block (no other block kind may hold an unsized member at all), only on a member that is not
// the last one, and only when every subscript of that member's name in the source is a decimal
// literal. Anything outside that shape is left exactly as it was - and the shape itself has no
// correct behaviour today, so the rewrite cannot take a working case away.
void SizeNonFinalUnsizedBufferBlockMembers(MobileGL::String& source) {
// Both tokens must be present for the shape to exist, and "[]" is absent from essentially
// every real shader source, so this is the whole cost for them.
if (source.find("[]") == MobileGL::String::npos || source.find("buffer") == MobileGL::String::npos) {
return;
}
const auto isDecimalInteger = [](const String& text) {
return !text.empty() && std::all_of(text.begin(), text.end(), [](char ch) {
return ch >= '0' && ch <= '9';
});
};
const Vector<CodeToken> tokens = TokenizeCode(source);
const SizeT count = tokens.size();
// Pass 1: for every identifier, the largest literal index it is subscripted with (as a
// count, i.e. index + 1), or -1 once it is subscripted with anything that is not a literal.
// The declaration's own empty `[]` is neither.
MobileGL::UnorderedMap<String, long long> subscriptExtent;
for (SizeT i = 1; i < count; ++i) {
if (tokens[i].text != "[" || !IsIdentifierToken(tokens[i - 1])) continue;
if (i + 1 < count && tokens[i + 1].text == "]") continue; // the unsized declarator itself
long long& extent = subscriptExtent[tokens[i - 1].text];
if (i + 2 < count && isDecimalInteger(tokens[i + 1].text) && tokens[i + 2].text == "]") {
if (extent >= 0) {
extent = std::max(extent, std::strtoll(tokens[i + 1].text.c_str(), nullptr, 10) + 1);
}
} else {
extent = -1;
}
}
// Pass 2: one edit per repairable member, applied back to front so earlier offsets stand.
struct SizeEdit {
SizeT pos;
String text;
};
Vector<SizeEdit> edits;
for (SizeT i = 0; i < count; ++i) {
if (tokens[i].text != "buffer") continue;
SizeT cursor = i + 1;
// `buffer` is also a member MEMORY qualifier ("buffer vec4 position0;"), which is why
// the block body has to be found rather than assumed.
if (cursor < count && IsIdentifierToken(tokens[cursor])) ++cursor;
if (cursor >= count || tokens[cursor].text != "{") continue;
const SizeT bodyBegin = cursor + 1;
SizeT bodyEnd = bodyBegin;
int depth = 1;
while (bodyEnd < count) {
if (tokens[bodyEnd].text == "{") {
++depth;
} else if (tokens[bodyEnd].text == "}") {
--depth;
if (depth == 0) break;
}
++bodyEnd;
}
if (depth != 0) continue; // unterminated; glslang will have the last word
Vector<std::pair<SizeT, SizeT>> members; // [begin, end) of each member, ';' excluded
SizeT memberBegin = bodyBegin;
for (SizeT m = bodyBegin; m < bodyEnd; ++m) {
if (tokens[m].text != ";") continue;
members.emplace_back(memberBegin, m);
memberBegin = m + 1;
}
// The LAST member is deliberately untouched: an unsized array there is a run-time
// sized array, which is both legal and correctly laid out already.
for (SizeT index = 0; index + 1 < members.size(); ++index) {
const SizeT begin = members[index].first;
const SizeT end = members[index].second;
if (end < begin + 3) continue;
if (tokens[end - 1].text != "]" || tokens[end - 2].text != "[") continue;
if (!IsIdentifierToken(tokens[end - 3])) continue;
// A multi-declarator member would need one size per declarator; out of scope.
bool multipleDeclarators = false;
for (SizeT t = begin; t < end; ++t) {
if (tokens[t].text == ",") multipleDeclarators = true;
}
if (multipleDeclarators) continue;
const auto known = subscriptExtent.find(tokens[end - 3].text);
if (known == subscriptExtent.end() || known->second <= 0) continue;
edits.push_back({tokens[end - 1].begin, std::to_string(known->second)});
}
i = bodyEnd;
}
for (auto it = edits.rbegin(); it != edits.rend(); ++it) {
source.insert(it->pos, it->text);
}
}
// Rewrite the `packed` / `shared` block-packing qualifiers inside layout(...) declarations to
// `std140`. Desktop GL leaves the memory layout of such blocks to the implementation and the
// app must query member offsets; MobileGL's SPIR-V pipeline always lays uniform blocks out as
@@ -962,6 +1073,11 @@ namespace MobileGL {
FilterUnsupportedGpuShaderInt64(env, source);
CoerceUniformBlockPackingToStd140(source);
// After the packing coercion: that one rewrites `packed`/`shared` in place and so
// cannot move an offset this pass depends on, and reading the block declarations
// once both qualifiers are normalized keeps the two passes' notions of a block
// declaration identical.
SizeNonFinalUnsizedBufferBlockMembers(source);
RenameBuiltinShadowingFunctions(source);
@@ -1113,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
@@ -1129,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;
@@ -1144,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;
@@ -1175,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
@@ -1225,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;
@@ -1240,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;
@@ -1275,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;
}
@@ -1332,6 +1509,100 @@ namespace MobileGL {
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
// form, which declares no block at all); the element count for an instance array,
// whose elements take base, base+1, ... (GLSL 4.30 4.4.5). -1 means "the grammar
// here is outside this scanner's narrow subset", i.e. do not judge this one.
long long StorageBlockBindingPointCount(const Vector<CodeToken>& tokens, SizeT bufferPos,
SizeT count) {
SizeT k = bufferPos + 1;
if (k < count && IsIdentifierToken(tokens[k])) ++k; // block type name
if (k >= count || tokens[k].text != "{") return 1;
MobileGL::Int braceDepth = 0;
while (k < count) {
if (tokens[k].text == "{") {
++braceDepth;
} else if (tokens[k].text == "}") {
--braceDepth;
if (braceDepth == 0) {
++k;
break;
}
}
++k;
}
if (braceDepth != 0) return -1; // unterminated block: not this scanner's business
if (k < count && IsIdentifierToken(tokens[k])) ++k; // instance name
if (k >= count || tokens[k].text != "[") return 1;
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
}
} // namespace
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;
// Fast path: no storage block, nothing to check. Both keywords are required for a
// violation to exist, and the pair is absent from almost every shader-pack source.
if (source.find("buffer") == String::npos || source.find("binding") == String::npos) {
return std::nullopt;
}
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.
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 == "(") {
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 == "binding" && j + 2 < count &&
tokens[j + 1].text == "=" &&
ParseGlslIntegerLiteral(tokens[j + 2].text, literal)) {
binding = std::min(literal, static_cast<long long>(INT_MAX / 2));
j += 2;
}
++j;
}
pos = j - 1;
continue;
}
if (text == "buffer") {
const long long points = binding >= 0 ? StorageBlockBindingPointCount(tokens, pos, count) : -1;
if (points > 0 && binding + points > static_cast<long long>(maxBindings)) {
return "ERROR: invalid value " + std::to_string(binding) +
" for layout specifier 'binding': a shader storage block occupying " +
std::to_string(points) + " binding point(s) from there passes " +
"GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS (" + std::to_string(maxBindings) + ").";
}
binding = -1;
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.
if (!IsNonLayoutQualifierKeyword(text)) binding = -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.
@@ -64,6 +64,17 @@ namespace MobileGL {
// mapIO can capture them, so they are recovered lexically (same narrow
// grammar discipline as ExtractExplicitUniformLocations).
UnorderedMap<String, Uint> ExtractExplicitOpaqueBindings(const String& source);
// 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.
std::optional<String> FindShaderStorageBindingViolation(const String& source, Int maxBindings);
} // namespace ShaderTranspiler
} // namespace MG_Util
} // namespace MobileGL
@@ -326,6 +326,55 @@ namespace MobileGL {
SPVC_CHK_RETURN
}
// "gl_AtomicCounterBlock_5" -> 5, -1 for anything that is not one of those blocks.
// The suffix is the GL atomic-counter binding the application declared, and after
// the relaxed lowering it is the only place that number still exists.
static Int AtomicCounterBlockBinding(const char* blockName) {
if (blockName == nullptr) return -1;
const SizeT prefixLength = std::strlen(ATOMIC_COUNTER_BLOCK_PREFIX);
const String name = blockName;
if (name.length() <= prefixLength + 1) return -1;
if (name.compare(0, prefixLength, ATOMIC_COUNTER_BLOCK_PREFIX) != 0) return -1;
if (name[prefixLength] != '_') return -1;
Int binding = 0;
for (SizeT i = prefixLength + 1; i < name.length(); ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
binding = binding * 10 + (name[i] - '0');
if (binding > 0x0FFFFFFF) return -1;
}
return binding;
}
spvc_result SpvcSession::SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
const spvc_reflected_resource* list = nullptr;
size_t count = 0;
SPVC_CHK_RESULT(spvc_resources_get_resource_list_for_type(
resources, SPVC_RESOURCE_TYPE_STORAGE_BUFFER, &list, &count));
for (size_t i = 0; i < count; ++i) {
auto& resource = list[i];
// The block TYPE name: glslang gives the synthesized block an EMPTY instance
// name, so resource.name carries nothing to match on. Read before Compile(),
// which is where SPIRV-Cross renames the reserved "gl_" prefix away.
const Int glBinding = AtomicCounterBlockBinding(
spvc_compiler_get_name(compiler, resource.base_type_id));
if (glBinding < 0) continue;
const Int esslBinding = topBinding - glBinding;
if (esslBinding < 0) {
MGLOG_E_ONCE("Atomic counter binding %d needs more shader storage binding points than this "
"driver has; its counters will not be updated.",
glBinding);
continue;
}
spvc_compiler_set_decoration(compiler, resource.id, SpvDecorationBinding,
static_cast<unsigned>(esslBinding));
outGlBindings.push_back(glBinding);
}
SPVC_CHK_RETURN
}
spvc_result SpvcSession::Compile(const char** result) {
if (!(usage & SessionUsageBit::Transpile)) return SPVC_ERROR_INVALID_ARGUMENT;
SPVC_CHK_INIT
@@ -105,6 +105,21 @@ namespace MobileGL {
// arrayed block's elements are separate GL resources spelled "B[0]", "B[1]").
// Entries with a negative value mean "never rebound" and are skipped.
spvc_result SetShaderStorageBlockBinding(const UnorderedMap<String, Int>& bindings);
// Points every synthesized atomic-counter block at a RESERVED storage-block
// binding and reports which GL atomic-counter bindings the module declares.
//
// glslang's relaxed parse rewrote each atomic_uint into a member of
// gl_AtomicCounterBlock_<N>, where N is the GL binding the application declared;
// the block itself was then auto-mapped to whatever storage-block binding was
// free, which has no relation to N and can collide with an SSBO the application
// binds itself. Slot N is taken from the TOP of the driver's range downwards
// (`topBinding - N`) so the reserved window never overlaps the low bindings
// applications use, and a block whose slot would be negative is left alone and
// NOT reported - the caller binds nothing there rather than aliasing.
//
// `outGlBindings` is appended to, so one vector can collect a whole program's
// stages; it may repeat a binding declared by several of them.
spvc_result SetAtomicCounterBlockBindings(Int topBinding, Vector<Int>& outGlBindings);
spvc_result Compile(const char** result);
const SpvcMetadata& GetMetadata() const;
const char* GetLastErrorString() const;
+29
View File
@@ -14,6 +14,35 @@ namespace MobileGL {
namespace MG_Util {
namespace ShaderTranspiler {
inline const char* GLOBAL_UBO_NAME = "MGL_GLOBAL_UBO";
// glslang's Vulkan-relaxed parse rewrites every atomic_uint into a member of a
// synthesized storage block named "<this>_<GL atomic-counter binding>"
// (ParseContextBase::growAtomicCounterBlock). That block IS the GL atomic counter
// buffer, and the trailing number is the only place the GL binding survives.
inline constexpr const char* ATOMIC_COUNTER_BLOCK_PREFIX = "gl_AtomicCounterBlock";
// Atomic-counter limits, in ONE place because GL 4.6 requires glGetIntegerv and the
// shading language's gl_MaxAtomicCounter* constants to report the same numbers
// (KHR-GL43.shader_atomic_counters.basic-glsl-built-in compares them directly).
// They used to be two unreconciled tables: BuildTBuiltInResource compiled against one
// binding and glGetIntegerv advertised thirty-six.
//
// The binding count is what the backends can actually serve. glslang lowers every
// atomic_uint onto a storage block, so one counter BUFFER costs one of the ES
// driver's shader-storage binding points, and DirectGLES reserves this many at the
// top of that range (see AtomicCounterEsslBindingTop in the DirectGLES managers).
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_BINDINGS = 8;
// GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, in basic machine units. Independent of the
// counter COUNTS below - it bounds the byte offset a counter may be declared at, and
// the conformance suite declares counters well past the eighth one (offsets 32 and
// 128 in a two-counter buffer). KHR-GL44.multi_bind splits it evenly across every
// advertised binding point and binds them all in one glBindBuffersRange, so it must
// stay a multiple of, and comfortably larger than, four times the binding count.
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFER_SIZE = 16384;
// GL_MAX_{FRAGMENT,COMPUTE,COMBINED}_ATOMIC_COUNTER_BUFFERS and the matching
// _ATOMIC_COUNTERS. Eight is the GL 4.6 core minimum for the compute stage
// (table 23.45) and every other stage this implementation serves counters on.
inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8;
inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8;
struct EmptyType {};