mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Merge] (CTS): land the GL43 wave-3 fixes and the DirectVulkan texture-shape repairs
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user