[Merge] (DirectGLES, GLState, ShaderTranspiler): land GL43 wave5 with its two new passes inside the L2 boundary

This commit is contained in:
2026-08-21 00:43:39 -04:00
49 changed files with 4535 additions and 233 deletions
@@ -1313,6 +1313,24 @@ namespace MobileGL::MG_State::GLState {
// span is left; grow the table instead of leaving the uniform without
// a location (which would make it unsettable via glUniform*).
const SizeT base = artifacts.uniformIndexInTProgram.size();
// The growth stops at the pool GL advertises. GL 4.6 core 7.6.1 bounds every
// uniform location by GL_MAX_UNIFORM_LOCATIONS, and the conformance suite reads a
// returned location >= the advertised maximum as a failure outright
// (KHR-GLES31.explicit_uniform_location.uniform-loc-mix-with-implicit-max). Minting
// 4095, 4096, ... is strictly worse than refusing: those are locations no
// application may legally name and no later query can make legal, so they would
// only turn a link-time exhaustion into a silently unwritable uniform. Unreachable
// for any program that fits glslang's per-stage uniform-component limits - it takes
// a fragmented pool of thousands of explicitly-located slots to get here.
if (base + static_cast<SizeT>(locationSpan) > kMaxUniformLocations) {
artifacts.infoLog = std::format(
"Uniform locations exhausted: '{}' needs {} location(s) and no free span is left below "
"GL_MAX_UNIFORM_LOCATIONS ({}).",
uniform.name, locationSpan, kMaxUniformLocations);
DeferLog(std::format("ProgramObject {}: Link failed - {}", in.externalIndex, artifacts.infoLog));
ProgramObject::ResetLinkArtifacts(artifacts);
return false;
}
artifacts.uniformIndexInTProgram.resize(base + locationSpan,
glslang::TQualifier::layoutLocationEnd);
artifacts.uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1);
@@ -281,6 +281,9 @@ namespace MobileGL::MG_State::GLState {
}
GLenum GetActiveUniformType(Uint index) const {
// The lowered counter is a plain uint inside a synthesized block; what the GL
// client declared - and what glGetActiveUniform must report - is an atomic_uint.
if (IsActiveUniformAtomicCounter(index)) return GL_UNSIGNED_INT_ATOMIC_COUNTER;
return UniformAt(TProgramUniformIndex(index)).glDefineType;
}
@@ -298,10 +301,57 @@ namespace MobileGL::MG_State::GLState {
}
Int GetActiveUniformBlockIndex(Uint index) const {
// An atomic counter is a DEFAULT-BLOCK uniform to GL, whatever block the
// transpiler lowered it onto (GL 4.6 core 7.6, table 7.6): -1.
if (IsActiveUniformAtomicCounter(index)) return -1;
// Members of the synthesized global UBO are default-block uniforms to GL: -1.
return GlBlockIndexFromTProgram(UniformAt(TProgramUniformIndex(index)).index);
}
// The transpiler lowers every atomic_uint onto a synthesized gl_AtomicCounterBlock_N
// block, but GL keeps seeing an atomic counter as a default-block uniform of type
// GL_UNSIGNED_INT_ATOMIC_COUNTER that points at an atomic-counter BUFFER. These two
// answer for that GL-level declaration; without them the query surface reports the
// lowering instead (GL_UNSIGNED_INT, block index 0) and
// KHR-GL43.shader_atomic_counters.basic-program-query fails on both.
//
// The returned value is an index into the GL_ACTIVE_ATOMIC_COUNTER_BUFFERS list, i.e.
// the RANK of the owning counter block among the counter blocks in glslang's block
// order - exactly how ProgramInterface numbers the GL_ATOMIC_COUNTER_BUFFER
// resources glGetActiveAtomicCounterBufferiv answers from. -1 when this uniform is
// not an atomic counter.
// Answered from the OWNED reflection snapshot, never from Artifacts().program. This
// arrived reading the live TProgram, which is null for every program served from the
// translation cache's L1 - and unlike the other query-surface accessors that made the
// same mistake, this one DEREFERENCES it, so the second program built from a given set
// of sources would have taken the process down rather than answered wrongly. The
// snapshot carries the same three facts in the same TPROGRAM index space:
// getUniform(i).index -> UniformAt(i).index, getNumUniformBlocks() ->
// blockReflection.size(), getUniformBlock(i).name -> BlockAt(i).name.
Int GetActiveUniformAtomicCounterBufferIndex(Uint index) const {
const Int tIndex = TProgramUniformIndex(index);
if (tIndex < 0) return -1;
const Int owner = UniformAt(tIndex).index;
if (owner < 0) return -1;
const Int blockCount = static_cast<Int>(Artifacts().blockReflection.size());
if (owner >= blockCount) return -1;
const SizeT prefixLength = StringView(MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX).size();
Int counterBufferIndex = 0;
for (Int i = 0; i < blockCount; ++i) {
const auto& blockName = BlockAt(i).name;
if (blockName.compare(0, prefixLength, MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX) != 0) {
continue;
}
if (i == owner) return counterBufferIndex;
++counterBufferIndex;
}
return -1;
}
Bool IsActiveUniformAtomicCounter(Uint index) const {
return GetActiveUniformAtomicCounterBufferIndex(index) >= 0;
}
// GL_UNIFORM_OFFSET: byte offset within the owning named block; -1 for a default-block
// uniform. The relaxed parse gives global-UBO members real byte offsets, but GL must keep
// seeing them as default-block uniforms, so gate on the GL-visible block index.
@@ -195,6 +195,13 @@ namespace {
return result;
}
if (const std::optional<String> counterOffsetError =
FindAtomicCounterOffsetViolation(result.preprocessedSource)) {
result.outcome = ShaderPreprocessOutcome::AtomicCounterOffsetRejected;
result.infoLog = *counterOffsetError;
return result;
}
// The parse this feeds runs in the link-compatible configuration (Vulkan-client
// env with relaxed rules): the TShader it produces is what glLinkProgram links and
// what the backends' SPIR-V is generated from - there is no second, GL-client
@@ -29,6 +29,9 @@ namespace MobileGL::MG_State::GLState {
// FindShaderStorageBindingViolation rejected it: a storage block declared a binding at or
// past GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS.
ResourceBindingRejected,
// FindAtomicCounterOffsetViolation rejected it: an atomic counter declared a
// layout(offset =) that is misaligned or reaches past GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE.
AtomicCounterOffsetRejected,
// The source-only half was clean but glslang rejected the preprocessed source.
// Memoizing this saves the parse itself on every later object with that source.
ParseFailed,
@@ -843,6 +843,21 @@ namespace MobileGL {
stored = box;
stateChanged = true;
}
// "The application has written this rectangle" is a DIFFERENT predicate from "the
// value moved", and the backends need the first one: glScissor(0, 0, 0, 0) as the
// very first scissor call leaves every stored box byte-identical to its
// never-written default, and that call is precisely the one whose meaning a
// backend must stop guessing at (see ScissorBoxWrittenMask).
//
// The transition has to count as a state change for the version too. DirectGLES'
// SyncRenderState early-outs on an unchanged render-state version BEFORE it
// reaches the span memcmp that would otherwise notice the mask, so a version-less
// flag flip would sit in the parameter block and never be pushed. It is a
// once-per-index transition, so the steady state still costs nothing.
if (m_parameters.ScissorBoxWrittenMask != kAllViewportsMask) {
m_parameters.ScissorBoxWrittenMask = kAllViewportsMask;
stateChanged = true;
}
if (stateChanged) ++m_version;
}
@@ -855,9 +870,15 @@ namespace MobileGL {
MOBILEGL_ASSERT(false, "Scissor box index out of range: %u", index);
return;
}
if (m_parameters.ScissorBoxes[index] == box) return;
// See SetScissorBox: a first write is state even when it does not move the value,
// so the unchanged-value early-out may only fire once this index is already
// marked written.
const Uint32 writtenBit = 1u << index;
const Bool alreadyWritten = (m_parameters.ScissorBoxWrittenMask & writtenBit) != 0;
if (alreadyWritten && m_parameters.ScissorBoxes[index] == box) return;
m_parameters.ScissorBoxes[index] = box;
m_parameters.ScissorBoxWrittenMask |= writtenBit;
++m_version;
}
@@ -328,6 +328,18 @@ namespace MobileGL {
// turns it into a real glEnable/glDisable.
Uint32 ScissorTestEnabledMask = 0;
Array<IntVec4, MAX_VIEWPORTS> ScissorBoxes{}; // x, y, width, height
// One bit per viewport, set the first time the application writes that index's scissor
// rectangle - glScissor broadcasts and sets all 16, glScissorIndexed/glScissorArrayv set
// the indices they name. It exists because the RECTANGLE cannot answer "has the
// application spoken?": ScissorBoxes starts all-zero (its spec initial value is the size
// of a window the frontend does not know yet, see the RenderState constructor), and
// glScissor(0, 0, 0, 0) is a legal GL state meaning "the scissor test rejects every
// fragment". A backend that reads an empty rectangle as the never-written sentinel
// therefore INVERTS that request into "accept every fragment"; DirectGLES did exactly
// that and KHR-GL43.viewport_array.scissor_zero_dimension caught it. Deliberately beside
// ScissorBoxes so it shares their tail span (after LogicOp) and DirectGLES' span memcmp
// picks a transition up like any other state.
Uint32 ScissorBoxWrittenMask = 0;
// glEnable(GL_CLIP_DISTANCE0 + i) for i in [0, 8), one bit each. A bitmask rather than
// eight bools because every consumer wants the set, not an individual flag, and because
// the SYNC_CAPABILITY/SET_CAPABILITY macros key off a "<Name>Enabled" field name that