[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
+367 -33
View File
@@ -396,6 +396,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// entry would otherwise false-skip the rebind).
void ScrubBufferBindingShadowsForId(Uint id);
// Defined next to the same shadow, and the counterpart to the scrub above for a
// buffer whose STORE was re-specified rather than deleted: the binding survives -
// nothing unbound the id - but the extent the driver resolved for it at bind time
// does not. Marks those points unknown so the next sync issues a real
// glBindBufferBase/Range instead of skipping it.
void InvalidateIndexedBufferBindingShadowsForId(Uint id);
// Resources whose owning BufferObject died; ids deleted at the next
// sync point with a current ES context.
Vector<SharedPtr<BackendBufferResource>> g_deferredBufferReleases;
@@ -568,6 +575,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const SizeT size = bufferObject.GetSize();
// Read BEFORE the fields below are overwritten: whether this respecify changes
// the store's EXTENT is what decides if the indexed-binding shadow still
// describes the driver.
const Bool extentChanged = !resource.storageInitialized || resource.storageSize != size;
const GLenum usage = MG_Util::ConvertBufferUsageToGLEnum(bufferObject.GetUsage());
BindBufferId(TempBufferTarget, resource.id);
// An orphaning respecify (glBufferData with NULL, content never
@@ -583,6 +594,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
resource.pendingRespecify = false;
resource.pendingRanges.clear();
resource.syncedChangeSerial = bufferObject.GetChangeSerial();
// A GROWN store keeps its indexed bindings, and BindBufferBaseCached skips a
// rebind whenever the shadow already records this id at that index - so on a
// driver that resolves a whole-buffer indexed binding's extent at BIND time
// (Adreno does; Mali does not) the shader keeps seeing the old, smaller range:
// stores past it are dropped and loads return zero. Forget what the shadow
// claims for this id so the next SyncBufferBindingPoints issues the bind for
// real. Only when the extent actually moved: an orphaning respecify at the same
// size is Minecraft's per-frame hot path and its bindings are still exact.
if (extentChanged) {
InvalidateIndexedBufferBindingShadowsForId(resource.id);
}
}
Bool StorageMatches(const GLESBufferResource& resource, const BufferObject& bufferObject) {
@@ -1180,6 +1202,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLintptr offset = 0;
GLsizeiptr size = 0;
Bool isBase = true;
// False when the driver's binding at this point is no longer described by the
// fields above and the next bind must be issued whatever it asks for. Set by
// InvalidateIndexedBufferBindingShadowsForId after a store was re-specified at
// a new size: the id is still bound, so the entry must NOT be scrubbed to
// base(0) (a later bind of 0 would then be false-skipped) - only distrusted.
Bool known = true;
};
constexpr SizeT kMaxIndexedBufferBindings = 64;
IndexedBufferBinding g_indexedUBOBindings[kMaxIndexedBufferBindings];
@@ -1211,20 +1239,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_boundPixelUnpackBufferId = 0;
}
}
void InvalidateIndexedBufferBindingShadowsForId(Uint id) {
if (id == 0) return;
for (auto& binding : g_indexedUBOBindings) {
if (binding.id == id) binding.known = false;
}
for (auto& binding : g_indexedSSBOBindings) {
if (binding.id == id) binding.known = false;
}
}
} // namespace
void BindBufferBaseCached(GLenum glTarget, Uint index, Uint id) {
auto* s = IndexedBindingShadow(glTarget, index);
if (s && s->isBase && s->id == id) return;
if (s && s->known && s->isBase && s->id == id) return;
g_GLESFuncs.glBindBufferBase(glTarget, index, id);
if (s) *s = {id, 0, 0, true};
if (s) *s = {id, 0, 0, true, true};
}
void BindBufferRangeCached(GLenum glTarget, Uint index, Uint id, GLintptr offset, GLsizeiptr size) {
auto* s = IndexedBindingShadow(glTarget, index);
if (s && !s->isBase && s->id == id && s->offset == offset && s->size == size) return;
if (s && s->known && !s->isBase && s->id == id && s->offset == offset && s->size == size) return;
g_GLESFuncs.glBindBufferRange(glTarget, index, id, offset, size);
if (s) *s = {id, offset, size, false};
if (s) *s = {id, offset, size, false, true};
}
void InvalidateIndexedBufferBindingCache() {
@@ -1548,6 +1586,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT componentSize = GetDataTypeSize(type);
return componentSize == 0 ? 0 : componentSize * static_cast<SizeT>(size);
}
// Deinterleaves elementCount elements of componentCount doubles into a tightly packed
// float32 stream - the fetch half of the fp64 demotion the shader side already does
// unconditionally (DemoteFloat64Pass). GL byte strides and offsets are arbitrary, so
// no component carries an 8-byte alignment guarantee and each is copied out before it
// is narrowed.
void NarrowDoubleStreamToFloat32(const Uint8* sourceBase, SizeT sourceStride, SizeT componentCount,
SizeT elementCount, Vector<Float>& outData) {
outData.resize(elementCount * componentCount);
for (SizeT element = 0; element < elementCount; ++element) {
const Uint8* sourceElement = sourceBase + element * sourceStride;
Float* destinationElement = outData.data() + element * componentCount;
for (SizeT component = 0; component < componentCount; ++component) {
Double value = 0.0;
Memcpy(&value, sourceElement + component * sizeof(Double), sizeof(Double));
destinationElement[component] = static_cast<Float>(value);
}
}
}
} // namespace
BackendVertexArrayObject::BackendVertexArrayObject() {
@@ -1555,6 +1612,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
m_clientAttributeBufferIds.fill(0);
m_convertedAttributeBufferIds.fill(0);
g_GLESFuncs.glGenVertexArrays(1, &m_backendVAOId);
if (m_backendVAOId == 0) {
MGLOG_E_ONCE("Failed to generate vertex array object.");
@@ -1580,6 +1638,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
bufferId = 0;
}
}
for (auto& bufferId : m_convertedAttributeBufferIds) {
if (bufferId != 0) {
BufferImpl::NoteBufferIdDeleted(bufferId);
g_GLESFuncs.glDeleteBuffers(1, &bufferId);
bufferId = 0;
}
}
}
namespace {
@@ -1748,10 +1813,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// that never calls a *BaseInstance entry point never pays for this compare.
const Uint32 fetchBaseInstance = g_pendingFetchBaseInstance;
const Bool baseInstanceDirty = m_syncedFetchBaseInstance != fetchBaseInstance;
const Bool emitAttributes = attributesDirty || baseInstanceDirty;
// A narrowed GL_DOUBLE stream is derived from the source buffer's CONTENT, and no
// VAO version moves when an app writes into a buffer, so the version gate cannot
// prove such a stream is still current. Re-walk while one is live; the walk itself
// re-checks the buffer's change serial and only re-converts on a real move.
const Bool emitAttributes = attributesDirty || baseInstanceDirty || m_hasConvertedFloat64Attribute;
if (!emitAttributes && !indexBufferDirty) {
return;
}
m_hasConvertedFloat64Attribute = false;
Bind();
@@ -1777,35 +1847,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_syncedAttributeVersions[attribIndex].FormatVersion;
Bool needsSyncBuffer = bufferIdsRemitted || allAttributeVersions[attribIndex].BufferVersion !=
m_syncedAttributeVersions[attribIndex].BufferVersion;
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue;
// This is where a 64-bit array actually stops. glVertexAttribLFormat is a legal call
// in a GL 4.3 context and the frontend RECORDS its format (the state queries have to
// answer), so IsLong does arrive here - what this backend cannot do is FEED it:
// SupportsFloat64VertexAttributes is false because ES has no GL_DOUBLE vertex format
// and ESSL has no fp64 type, and passing GL_DOUBLE to glVertexAttribPointer would
// only raise GL_INVALID_ENUM on the real driver. Disabling rather than merely
// skipping matters: becoming long bumps FormatVersion, not SwitchVersion, so the
// enable/disable block above will not run again and an already-enabled array would
// stay enabled with no pointer and no ARRAY_BUFFER binding - which ES 3.1+ makes an
// INVALID_OPERATION at draw.
// This is where a 64-bit array is narrowed. glVertexAttribLFormat is a legal call in
// a GL 4.3 context and the frontend RECORDS its format (the state queries have to
// answer), so IsLong does arrive here - what this backend cannot do is feed it at
// full precision: ES has no GL_DOUBLE vertex FORMAT and ESSL has no fp64 type, and
// passing GL_DOUBLE to glVertexAttribPointer would only raise GL_INVALID_ENUM on the
// real driver. But the FORMAT is the only 64-bit thing here: the source bytes are
// ordinary IEEE-754 doubles, and MobileGL already narrows every fp64 value in every
// shader (DemoteFloat64Pass) and every glUniform*d the same way, so the array is
// deinterleaved into a float32 stream and fetched as GL_FLOAT rather than dropped.
// glVertexAttribFormat(GL_DOUBLE) asks for exactly that conversion anyway; the L form
// asks for more precision than any backend here can give, and gets the same stream.
//
// IsLong is not the only way a 64-bit array gets here: glVertexAttribFormat
// with GL_DOUBLE asks for doubles in memory CONVERTED to float, so it is not
// long, is not declined by the frontend, and still has no ES vertex format.
// Leaving that one enabled did not merely raise INVALID_ENUM - the Adreno
// driver dereferenced null inside the next draw and took the process with it
// (SIGSEGV in libGLESv2_adreno, KHR-GL43.vertex_attrib_binding.basic-input-case4),
// because the array stayed enabled with no pointer the failed call could set.
// The type test therefore covers the storage, not the spelling.
// The conversion is deliberately NOT behind the version gate below: it is derived
// from buffer CONTENT, which no VAO version covers. Its own memo (keyed on the
// source buffer's change serial) is what keeps the repeat cost down.
//
// When no stream can be built the array is DISABLED rather than left alone. That
// matters: becoming long bumps FormatVersion, not SwitchVersion, so the
// enable/disable block above will not run again and an already-enabled array would
// stay enabled with no pointer and no ARRAY_BUFFER binding - which did not merely
// raise INVALID_ENUM but had the Adreno driver dereference null inside the next draw
// and take the process with it (SIGSEGV in libGLESv2_adreno,
// KHR-GL43.vertex_attrib_binding.basic-input-case4).
if (attrib.IsLong || attrib.Type == DataType::Float64) {
MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array, which this "
"backend cannot feed - disabling the array",
attribIndex);
if (attrib.Enabled && attrib.Type == DataType::Float64 &&
SyncFloat64AttributeAsFloat32(attribIndex, attrib, fetchBaseInstance)) {
m_hasConvertedFloat64Attribute = true;
// Explicit, not redundant: an earlier walk that could not build the stream
// disabled this array, and becoming feedable again bumps no SwitchVersion,
// so the enable/disable block above would never turn it back on.
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
g_GLESFuncs.glVertexAttribDivisor(attribIndex, attrib.Divisor);
continue;
}
if (attrib.Enabled) {
MGLOG_W_ONCE("DirectGLES: vertex attribute %u is a 64-bit (GL_DOUBLE) array whose source "
"stream could not be narrowed to float32 - disabling the array",
attribIndex);
}
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
if (!needsSyncFormat && !needsSyncBuffer && !needsSyncBaseInstance) continue;
// A resolved stride of zero is the binding model's "never advance" (see
// VertexAttribute::Stride) and glVertexAttribPointer cannot say it - a zero
// stride argument there means "tightly packed" instead, i.e. exactly the
@@ -1928,11 +2014,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
continue;
}
// Same reason as SyncToBackend, including why the test is on the storage rather
// than on IsLong: there is no ES vertex format for a 64-bit array, and this path
// only ever reaches glVertexAttribPointer/IPointer.
// Same narrowing as SyncToBackend (the long note lives there), with the draw's own
// fetch range standing in for the buffer extent a client array does not have. The
// upload starts at element 0 so `first` keeps indexing the stream, exactly as the
// unconverted upload below does. The test is on the storage rather than on IsLong
// because glVertexAttribFormat(GL_DOUBLE) is 64-bit data without being long.
if (attrib.IsLong || attrib.Type == DataType::Float64) {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
const auto* sourceBase = reinterpret_cast<const Uint8*>(attrib.Offset);
if (attrib.Type != DataType::Float64 || sourceBase == nullptr || attrib.Size < 1 ||
attrib.Size > 4) {
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
auto& bufferId = m_clientAttributeBufferIds[attribIndex];
if (bufferId == 0) {
g_GLESFuncs.glGenBuffers(1, &bufferId);
if (bufferId == 0) {
MGLOG_E_ONCE("Failed to create client-side vertex attribute upload buffer.");
g_GLESFuncs.glDisableVertexAttribArray(attribIndex);
continue;
}
}
const SizeT componentCount = static_cast<SizeT>(attrib.Size);
const SizeT sourceElementSize = componentCount * sizeof(Double);
// A client array only ever reaches here through a pointer call, whose stride 0
// the frontend already resolved to the element size, so this is a guard rather
// than a case (VertexAttribute::Stride).
const SizeT sourceStride =
attrib.Stride > 0 ? static_cast<SizeT>(attrib.Stride) : sourceElementSize;
const SizeT elementCount = static_cast<SizeT>(first) + static_cast<SizeT>(count);
Vector<Float> converted;
NarrowDoubleStreamToFloat32(sourceBase, sourceStride, componentCount, elementCount, converted);
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
// GL ignores `normalized` for floating-point array types, so it is not
// forwarded here either.
g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE,
static_cast<GLsizei>(componentCount * sizeof(Float)),
nullptr);
g_GLESFuncs.glEnableVertexAttribArray(attribIndex);
continue;
}
@@ -1972,6 +2097,120 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Bool BackendVertexArrayObject::SyncFloat64AttributeAsFloat32(
Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib, Uint32 fetchBaseInstance) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
if (attribIndex >= m_convertedAttributeBufferIds.size() || attrib.Size < 1 || attrib.Size > 4) {
return false;
}
const auto& bufferObject = attrib.Buffer;
if (!bufferObject) {
// A client-memory 64-bit array is narrowed on the draw path instead, which is the
// only place its fetch range is known (SyncClientSideAttributesForDrawArrays).
return false;
}
// The frontend shadow is what the conversion reads, so a shader write that has not
// been pulled back yet has to land first. A no-op unless one is outstanding.
bufferObject->SyncGpuWrites();
const Uint8* const sourceBase = bufferObject->MappedData();
const SizeT sourceSize = bufferObject->GetSize();
if (sourceBase == nullptr || attrib.Offset >= sourceSize) {
return false;
}
const SizeT componentCount = static_cast<SizeT>(attrib.Size);
const SizeT sourceElementSize = componentCount * sizeof(Double);
const SizeT available = sourceSize - attrib.Offset;
if (available < sourceElementSize) {
return false;
}
// A resolved stride of zero is the binding model's "never advance" (see
// VertexAttribute::Stride): exactly one element exists and every vertex reads it, so
// exactly one is converted. Otherwise the array's extent is the source buffer's own -
// SyncToBackend has no draw range, and a whole-array conversion is affordable because
// it is memoised on the buffer's change serial and 64-bit arrays are vanishingly rare.
const Bool neverAdvances = attrib.Stride <= 0;
const SizeT sourceStride = neverAdvances ? sourceElementSize : static_cast<SizeT>(attrib.Stride);
const SizeT elementCount = neverAdvances ? 1 : ((available - sourceElementSize) / sourceStride) + 1;
// baseInstance shifts the ELEMENT index of a divisor'd array, and one element of the
// converted stream is componentCount floats. A zero stride never advances, so no
// shift can move it. A shift past the array's own extent has no source data at all.
const SizeT firstElement = (fetchBaseInstance != 0 && attrib.Divisor != 0 && !neverAdvances)
? static_cast<SizeT>(fetchBaseInstance)
: 0;
if (firstElement >= elementCount) {
return false;
}
auto& stream = m_convertedAttributeStreams[attribIndex];
Uint& convertedBufferId = m_convertedAttributeBufferIds[attribIndex];
const Uint64 sourceLifetimeId = bufferObject->GetLifetimeId();
const Uint64 sourceChangeSerial = bufferObject->GetChangeSerial();
// A persistent map is written through the pointer, with no API call to bump the change
// serial (see BufferObject::SyncPersistentMappedRange), so its serial cannot prove the
// converted copy is still current and the memo is never trusted for one.
const Bool memoHit =
stream.valid && convertedBufferId != 0 && !bufferObject->IsBackendPersistentMapped() &&
stream.sourceLifetimeId == sourceLifetimeId && stream.sourceChangeSerial == sourceChangeSerial &&
stream.sourceOffset == attrib.Offset && stream.sourceStride == sourceStride &&
stream.componentCount == componentCount && stream.elementCount == elementCount;
if (!memoHit) {
if (convertedBufferId == 0) {
g_GLESFuncs.glGenBuffers(1, &convertedBufferId);
if (convertedBufferId == 0) {
MGLOG_E_ONCE("Failed to create the float32 scratch buffer for the 64-bit vertex array at "
"attribute %u.",
attribIndex);
return false;
}
}
Vector<Float> converted;
NarrowDoubleStreamToFloat32(sourceBase + attrib.Offset, sourceStride, componentCount, elementCount,
converted);
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER,
static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
converted.data(), GL_STREAM_DRAW);
stream.valid = true;
stream.sourceLifetimeId = sourceLifetimeId;
stream.sourceChangeSerial = sourceChangeSerial;
stream.sourceOffset = attrib.Offset;
stream.sourceStride = sourceStride;
stream.componentCount = componentCount;
stream.elementCount = elementCount;
MGLOG_D("DirectGLES: narrowed the 64-bit vertex array at attribute %u to %zu float32 element(s).",
attribIndex, elementCount);
}
const SizeT convertedElementSize = componentCount * sizeof(Float);
if (neverAdvances) {
// Only the binding-point API can say "stride 0": glVertexAttribPointer's zero
// means "tightly packed" instead, i.e. the opposite, and would walk the driver
// straight off the end of the single converted element.
if (!HasVertexBindingApi()) {
return false;
}
g_GLESFuncs.glVertexAttribFormat(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE, 0);
g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex);
g_GLESFuncs.glBindVertexBuffer(attribIndex, convertedBufferId, 0, 0);
return true;
}
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, convertedBufferId);
// `normalized` is deliberately GL_FALSE rather than attrib.Normalized: GL ignores it
// for floating-point array types, and honouring it would scale the fetched values
// (KHR-GL43.vertex_attrib_binding.basic-input-case5 passes GL_TRUE and expects 10/20).
g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE,
static_cast<GLsizei>(convertedElementSize),
(const void*)(firstElement * convertedElementSize));
return true;
}
StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
g_backendVertexArrayObjects;
} // namespace VertexArrayImpl
@@ -4764,6 +5003,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
return reflectionName;
}
// GL_MAX_<stage>_IMAGE_UNIFORMS as the ES driver reports it, which is also exactly what
// MobileGL advertises for it (GL_Getter answers from the same DynamicBackendParameters).
// -1 for a stage the ES side has no such limit for, which is the "cannot say" answer
// the diagnostic that reads it prints rather than a made-up number. [[maybe_unused]]
// because its only caller is an MGLOG_E argument, and MGLOG_E compiles to nothing in a
// build whose MOBILEGL_LOG_ACTIVE_LEVEL is above ERROR.
[[maybe_unused]] Int AdvertisedStageImageUniformLimit(ShaderStage stage) {
switch (stage) {
case ShaderStage::Vertex: return g_GLESCapabilities.MaxVertexImageUniforms;
case ShaderStage::Geometry: return g_GLESCapabilities.MaxGeometryImageUniforms;
case ShaderStage::Fragment: return g_GLESCapabilities.MaxFragmentImageUniforms;
case ShaderStage::Compute: return g_GLESCapabilities.MaxComputeImageUniforms;
default: return -1;
}
}
// Whether a glslang layout format is one GLSL ES has in core; the rest reach ES only
// through GL_NV_image_formats. Asked of DECLARED formats, which this backend passes
// through untouched - the emitted ESSL still has to be legal for the driver.
@@ -5206,6 +5461,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
effectiveSpirv = &outputIndexSpirv;
}
// Same rule, different resource, every stage: GL 4.3 lets an array of storage
// blocks be indexed with any dynamically-uniform expression, GLSL ES keeps the
// ES 3.1 constant-expression rule, and the Qualcomm compiler enforces it
// ("indexing into an SSBO array using a non-constant expression is not
// permitted") - losing the stage, the program, and every dispatch that used
// it, while the frontend keeps reporting the link glslang performed. Fold or
// lower the index here, on the ESSL path only: the same module is legal for
// DirectVulkan, which binds the array as one descriptor array.
//
// NO KEY MATERIAL, and that is a conclusion rather than an omission: this takes the
// module and nothing else - no capability bit arms it, no per-program plan steers
// it - and it self-gates on the module's own content
// (BinaryHasDynamicStorageBlockArrayIndexing). The module is already the largest
// thing in the L2 key, so it is fully covered. Contrast LowerViewportIndexForEssl,
// whose signature is equally module-only but which SupportsViewportArray ARMS -
// that bit is in the key precisely because of it.
Vector<unsigned int> blockArrayIndexSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(
*effectiveSpirv, blockArrayIndexSpirv, enableSpirvValidation) &&
!blockArrayIndexSpirv.empty()) {
effectiveSpirv = &blockArrayIndexSpirv;
}
// glslang kept the application's layout(offset = N) on the atomic counters it
// lowered onto gl_AtomicCounterBlock_<N>, and no std140/std430 layout can put
// member 0 anywhere but offset 0 - so SPIRV-Cross throws ("cannot be expressed as
// neither std430 nor std140") and the stage never reaches the driver. Collapse the
// block into one uint array at offset 0 and re-index each counter to the element
// that used to be at its byte offset; the buffer then stays bound whole, which it
// has to (GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT is 32 on this device, so an
// 8-byte bind offset is not expressible). BEFORE SetAtomicCounterBlockBindings
// below, which only moves the block's BINDING and needs the block intact.
//
// NO KEY MATERIAL either, for the same reason - and note where the application's
// layout(offset = N) values live: glslang already baked them into the module as
// member Offset decorations, so they are in the key as MODULE BYTES. There is no
// separate offset input to carry.
Vector<unsigned int> atomicCounterSpirv;
if (MG_Util::ShaderTranspiler::ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(
*effectiveSpirv, atomicCounterSpirv, enableSpirvValidation) &&
!atomicCounterSpirv.empty()) {
effectiveSpirv = &atomicCounterSpirv;
}
MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv,
MG_Util::ShaderTranspiler::SessionUsageBit::Transpile);
@@ -5371,6 +5670,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
std::set<String> flattenedXfbBlockNames;
// Stages whose ESSL had a read+write image declaration doubled into a coherent
// read/write pair, and by how many. Empty for every program but a handful; consulted
// ONLY when the link then fails, because the doubling spends the driver's per-stage
// GL_MAX_*_IMAGE_UNIFORMS budget that MobileGL keeps advertising unadjusted (halving
// the advertised value would fail basic-api and NotSupported-out cases that never pay
// the doubling, so the limit must stay honest and the connection has to be made here
// instead). See the budget note on SplitReadWriteImageUniforms.
struct SplitImageUniformStage {
ShaderStage stage;
Uint splitCount;
};
Vector<SplitImageUniformStage> splitImageUniformStages;
// Desktop GLSL keeps SEPARATE name namespaces for input and output interface
// blocks, so ONE stage may legally declare `in FOO {...}` and `out FOO {...}` at
@@ -5642,7 +5953,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// declaration and preserves its binding - an image unit cannot be set from
// the API in ES, so the qualifier is the only binding mechanism there is,
// and both halves of the pair have to still be carrying theirs when it runs.
source = SplitReadWriteImageUniforms(source);
Uint splitImageUniformCount = 0;
source = SplitReadWriteImageUniforms(source, &splitImageUniformCount);
if (splitImageUniformCount != 0) {
splitImageUniformStages.push_back({shader->GetShaderStage(), splitImageUniformCount});
}
source = RemoveLayoutBinding(source);
source = ProcessOutColorLocations(source);
source = ForceFlatIntegerVaryings(source, glShaderType);
@@ -5787,6 +6102,25 @@ namespace MobileGL::MG_Backend::DirectGLES {
// in an INFO-level artifact.
MGLOG_E("Program linking failed. State program ID: %u, backend program ID: %u, driver log: %s",
stateProgramObject->GetExternalIndex(), m_backendProgramId, log.data());
// The one link failure MobileGL can name a cause for that the driver's log never
// will: ESSL has no legal single declaration for a read+write image outside
// r32f/r32i/r32ui, so those are split into a coherent pair and the stage ends up
// declaring more image uniforms than the application did - against a
// GL_MAX_*_IMAGE_UNIFORMS that is still the driver's raw number, because lowering
// it would fail basic-api and NotSupported-out every case that only ever uses
// readonly/writeonly images. A shader declaring more than half a stage's budget in
// read+write images therefore links here and nowhere else, and without this line
// the next reader has only a generic driver message to go on.
for (const SplitImageUniformStage& split : splitImageUniformStages) {
MGLOG_E("...and %u read+write image uniform(s) in stage %s were split into coherent "
"read/write pairs, so that stage declares %u image uniform(s) more than the "
"program did; GL_MAX_*_IMAGE_UNIFORMS for it is %d. If the driver log names "
"image uniforms, that is the cause.",
split.splitCount,
MG_Util::ConvertGLEnumToString(
MG_Util::ConvertShaderStageToGLEnum(split.stage)).c_str(),
split.splitCount, AdvertisedStageImageUniformLimit(split.stage));
}
} else {
MGLOG_D("Program linked successfully. ID: %u", m_backendProgramId);
}