[Fix] (MG_Util/ShaderTranspiler, MG_State, MG_Impl/GLImpl, MG_Backend/DirectGLES): GL CTS uniform_block - coerce packed/shared block layouts to std140 at source preprocess (glslang rejects them when targeting SPIR-V; std140 is the only UBO layout the pipeline emits), GL-style block reflection (array "[0]" names, per-element struct-array expansion, unused members and declared-but-unread blocks stay active), vec4-padded GL_UNIFORM_BLOCK_DATA_SIZE, std140 array strides for struct-nested arrays (glslang reflects tight strides there), arrayed-block instances share the first instance member set, glDeleteShader-flagged names stay usable while attached, and backend ESSL emits against highp default precision so relaxed block members match across stages (KHR-GL33.shaders.uniform_block on llvmpipe: 659 Fail -> 828/828 Pass)

This commit is contained in:
2026-07-16 12:09:11 -04:00
parent 1cefb9780b
commit 254cf1dc21
11 changed files with 522 additions and 18 deletions
+4
View File
@@ -279,6 +279,10 @@ namespace MobileGL::MG_State {
return m_programState.MarkShaderObjectForDeletion(index);
}
void GLContext::ReleaseShaderNameIfOrphaned(const Uint index) {
return m_programState.ReleaseShaderNameIfOrphaned(index);
}
Bool GLContext::ValidateProgramName(const Uint index) const {
return m_programState.ValidateProgramObject(index);
}
+3
View File
@@ -116,6 +116,9 @@ namespace MobileGL {
Uint CreateShader(ShaderStage stage);
void MarkProgramForDeletion(Uint index);
void MarkShaderForDeletion(Uint index);
// Frees a deletion-flagged shader's name once it lost its last GL-visible
// attachment (call after glDetachShader).
void ReleaseShaderNameIfOrphaned(Uint index);
Bool ValidateProgramName(Uint index) const;
Bool ValidateShaderName(Uint index) const;
const SharedPtr<ProgramObject>& GetProgramObject(Uint index);
@@ -360,7 +360,18 @@ namespace MobileGL::MG_State::GLState {
}
MGLOG_D("ProgramObject %u: DoReflection - building reflection", m_externalIndex);
if (!m_program->buildReflection()) {
// GL-style reflection naming (GL CTS uniform_block relies on all four):
// - BasicArraySuffix: an array uniform is reported as "arr[0]" per the GL spec.
// - StrictArraySuffix: named-block struct arrays expand per element ("s[0].a",
// "s[1].a", ...) following ARB_program_interface_query rules. Default-block
// (loose) uniforms already expand per element without this option.
// - AllBlockVariables: every member of an active named block is active even when
// no shader statement reads it (ES 3.0/GL 3.3 named-block semantics).
// - SharedStd140UBO: a DECLARED uniform block is active even when no member is
// ever read (reflected from the linker objects). PreprocessShaderSource coerces
// every block to std140, so this covers all of them.
if (!m_program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix |
EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) {
m_linkStatus = false;
m_infoLog = "Build reflection failed.";
MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex);
@@ -483,7 +494,14 @@ namespace MobileGL::MG_State::GLState {
continue;
}
const auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
// Reflection names an array "texs[0]" while the layout(binding = N) map from the IO
// resolver is keyed by the declared name ("texs"); look up both spellings.
auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name);
if (explicitBinding == m_explicitOpaqueUniformBindings.end() && uniform.name.length() > 3 &&
uniform.name.compare(uniform.name.length() - 3, 3, "[0]") == 0) {
explicitBinding =
m_explicitOpaqueUniformBindings.find(uniform.name.substr(0, uniform.name.length() - 3));
}
const int initialUnit =
explicitBinding != m_explicitOpaqueUniformBindings.end() ? static_cast<int>(explicitBinding->second) : 0;
const Int locationSpan = GetUniformLocationSpan(uniform);
@@ -685,7 +703,13 @@ namespace MobileGL::MG_State::GLState {
m_globalUboScratch.resize(size);
}
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
const auto locationIt = m_uniformLocations.find(name);
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
// suffix before declaring the uniform unbacked.
auto locationIt = m_uniformLocations.find(name);
if (locationIt == m_uniformLocations.end()) {
locationIt = m_uniformLocations.find(name + "[0]");
}
if (locationIt == m_uniformLocations.end()) {
MGLOG_D("ProgramObject %u: GenerateBinary - uniform '%s' offset=%u but not found in "
"m_uniformLocations",
@@ -18,6 +18,13 @@ namespace MobileGL::MG_State::GLState {
public:
ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {}
bool ShaderIsAttached(const SharedPtr<ShaderObject>& shader);
// GL-visible attachment: in the attach list and not pending detach (glDetachShader
// defers the actual removal to the next link).
Bool ShaderIsAttachedGLVisible(const SharedPtr<ShaderObject>& shader) const {
const auto matches = [&shader](const SharedPtr<ShaderObject>& s) { return s.get() == shader.get(); };
if (std::none_of(m_shaders.begin(), m_shaders.end(), matches)) return false;
return std::none_of(m_detachedShaders.begin(), m_detachedShaders.end(), matches);
}
bool AttachShader(const SharedPtr<ShaderObject>& shader);
SizeT DetachShader(const SharedPtr<ShaderObject>& shader);
SizeT RemoveShader(const SharedPtr<ShaderObject>& shader);
@@ -45,10 +52,16 @@ namespace MobileGL::MG_State::GLState {
const auto it = m_uniformLocations.find(name);
if (it != m_uniformLocations.end()) return (Int)it->second;
// "arr[k]" resolves to the location of element k: glslang reflection stores
// arrays under their base name (no "[0]" suffix), and DoReflection reserves
// one location per array element, so element k lives at base + k.
if (name.length() < 4 || name.back() != ']') return -1;
// Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base
// location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves
// to base + k because DoReflection reserves one location per array element.
if (name.empty()) return -1;
if (name.back() != ']') {
const auto suffixedIt = m_uniformLocations.find(name + "[0]");
if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second;
return -1;
}
if (name.length() < 4) return -1;
const SizeT bracket = name.rfind('[');
// Require at least one digit between the brackets.
if (bracket == String::npos || bracket + 1 >= name.length() - 1) return -1;
@@ -58,8 +71,13 @@ namespace MobileGL::MG_State::GLState {
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0FFFFFFFu) return -1;
}
const auto baseIt = m_uniformLocations.find(name.substr(0, bracket));
if (baseIt == m_uniformLocations.end()) return -1;
auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]");
if (baseIt == m_uniformLocations.end()) {
// Legacy key without the "[0]" suffix (defensive; reflection normally
// stores the suffixed form for arrays).
baseIt = m_uniformLocations.find(name.substr(0, bracket));
if (baseIt == m_uniformLocations.end()) return -1;
}
const Int base = (Int)baseIt->second;
if (!IsValidUniformLocation(base)) return -1;
const Int index = m_uniformIndexInTProgram[base];
@@ -86,6 +104,19 @@ namespace MobileGL::MG_State::GLState {
return uniformIndex;
}
// Reflection stores an array uniform under "arr[0]"; accept the bare "arr"
// spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for
// robustness against non-suffixed reflection entries.
if (!name.empty() && name.back() != ']') {
const String suffixedName = name + "[0]";
const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str());
if (suffixedIndex >= 0 && suffixedIndex < m_activeUniformCount &&
m_program->getUniform(suffixedIndex).name == suffixedName) {
return suffixedIndex;
}
return -1;
}
if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1;
const String baseName = name.substr(0, name.length() - 3);
const Int baseIndex = m_program->getUniformIndex(baseName.c_str());
@@ -136,11 +167,24 @@ namespace MobileGL::MG_State::GLState {
}
// GL_UNIFORM_ARRAY_STRIDE: byte stride of an array member in a named block; 0 for a non-array
// block member; -1 for a default-block uniform. glslang yields arrayStride==0 for the
// default-block case, so gate on block membership to return the spec-mandated -1.
// block member; -1 for a default-block uniform (glslang yields arrayStride==0 there, so gate
// on block membership for the spec-mandated -1). The stride itself is derived from the type
// instead of glslang's reflected arrayStride: for an array nested inside a struct member,
// glslang computes that field against the enclosing STRUCT's (unset) packing and reports a
// tight std430-like stride (ivec2 a[7] -> 8), even though its own member offsets and the
// generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO
// layout is always std140, where every array element stride rounds up to a vec4.
GLint GetActiveUniformArrayStride(Uint index) const {
const auto& uniform = m_program->getUniform(static_cast<Int>(index));
return (uniform.index < 0) ? -1 : uniform.arrayStride;
if (uniform.index < 0) return -1;
const glslang::TType* type = uniform.getType();
if (type == nullptr || !type->isArray()) return 0;
if (type->isMatrix()) {
const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0;
const int vectors = rowMajor ? type->getMatrixRows() : type->getMatrixCols();
return GetActiveUniformMatrixStride(index) * vectors;
}
return 16; // scalars and vectors: std140 rounds the element stride up to a vec4
}
// GL_UNIFORM_IS_ROW_MAJOR: 1 only for a row-major matrix in a named block, else 0. The
@@ -327,6 +371,11 @@ namespace MobileGL::MG_State::GLState {
Uint GetUniformBlockIndex(const char* name) const {
auto it = m_uniformBlockIndexByName.find(name);
if (it != m_uniformBlockIndexByName.end()) return it->second;
// Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]";
// a bare "Block" query resolves to the first instance per GL semantics.
const String suffixedName = String(name) + "[0]";
it = m_uniformBlockIndexByName.find(suffixedName);
if (it != m_uniformBlockIndexByName.end()) return it->second;
return 0xFFFFFFFFu; // GL_INVALID_INDEX
}
Bool IsActiveUniformBlock(Uint index) const {
@@ -335,7 +384,11 @@ namespace MobileGL::MG_State::GLState {
}
Uint GetUBOSizeAt(Uint index) const {
if (!IsActiveUniformBlock(index)) return 0;
return m_program->getUniformBlock((Int)index).size;
// glslang reports the unpadded end offset of the last member, but a std140 block
// (like a std140 struct) occupies a vec4-rounded size, and that is what the
// backend compiles: ES drivers reject draws whose bound UBO range is smaller
// than the block (a block ending in ivec3 reported 12 while the driver needs 16).
return (m_program->getUniformBlock((Int)index).size + 15u) & ~15u;
}
const String& GetUniformBlockName(Uint index) const {
@@ -343,8 +396,30 @@ namespace MobileGL::MG_State::GLState {
return ubo.name;
}
// Uniform entries that belong to an arrayed uniform block are reflected once, against
// the first instance ("Block[0]"); per GL semantics every other instance shares that
// member set. Maps any instance's block index to the index owning the member entries.
Uint GetUniformBlockMemberOwnerIndex(Uint index) const {
const String& name = GetUniformBlockName(index);
if (name.empty() || name.back() != ']') return index;
const SizeT bracket = name.rfind('[');
if (bracket == String::npos) return index;
const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]");
if (it != m_uniformBlockIndexByName.end()) return it->second;
return index;
}
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS: derived from the same active-uniform scan that
// fills GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, so the two queries always agree
// (glslang's numMembers counts declared members, which diverges from the reflected
// entry list for struct arrays and arrayed block instances).
Int GetUniformBlockActiveUniformCount(Uint index) const {
return m_program->getUniformBlock((Int)index).numMembers;
const Int ownerIndex = static_cast<Int>(GetUniformBlockMemberOwnerIndex(index));
Int count = 0;
for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) {
if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count;
}
return count;
}
Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const {
@@ -29,9 +29,18 @@ namespace MobileGL::MG_State::GLState {
if (!CheckIndexAvail(program, m_programObjects)) return; // FIXME: add error reporting here
auto& programObject = m_programObjects[program];
if (programObject != nullptr) {
// Snapshot the attachments: deleting the program is a detach point for shaders
// that were flagged with glDeleteShader while still attached.
const Vector<SharedPtr<ShaderObject>> attachedShaders = programObject->GetAttachedShaders();
programObject->MarkAsDeleted();
programObject.reset();
m_programIndexGenerator.Delete(program);
for (const auto& shader : attachedShaders) {
const Uint shaderName = shader->GetExternalIndex();
if (CheckIndexAvail(shaderName, m_shaderObjects) && m_shaderObjects[shaderName] == shader) {
ReleaseShaderNameIfOrphaned(shaderName);
}
}
}
}
@@ -66,12 +75,35 @@ namespace MobileGL::MG_State::GLState {
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
auto& shaderObject = m_shaderObjects[shader];
if (shaderObject != nullptr) {
m_shaderObjects[shader]->MarkAsDeleted();
m_shaderObjects[shader].reset();
m_shaderIndexGenerator.Delete(shader);
// glDeleteShader on an attached shader only FLAGS it; the name stays valid (and
// glShaderSource/glCompileShader keep working on it) until the shader is detached
// from every program. The GL CTS compiles shaders through exactly this
// create-attach-delete-source-compile sequence (uniform_block.common.name_matching).
shaderObject->MarkAsDeleted();
ReleaseShaderNameIfOrphaned(shader);
}
}
Bool ProgramState::ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const {
for (const auto& programObject : m_programObjects) {
if (programObject != nullptr && programObject->ShaderIsAttachedGLVisible(shaderObject)) {
return true;
}
}
// A program deleted while current vacates its table slot but stays alive as the
// current program; its attachments still count.
return m_currentProgram != nullptr && m_currentProgram->ShaderIsAttachedGLVisible(shaderObject);
}
void ProgramState::ReleaseShaderNameIfOrphaned(Uint shader) {
if (!CheckIndexAvail(shader, m_shaderObjects)) return;
auto& shaderObject = m_shaderObjects[shader];
if (shaderObject == nullptr || !shaderObject->GetDeleteStatus()) return;
if (ShaderHasGLVisibleAttachment(shaderObject)) return;
shaderObject.reset();
m_shaderIndexGenerator.Delete(shader);
}
Bool ProgramState::ValidateShaderObject(Uint shader) const {
return CheckIndexAvail(shader, m_shaderObjects) && m_shaderObjects[shader] != nullptr;
}
@@ -26,11 +26,16 @@ namespace MobileGL::MG_State::GLState {
Uint CreateShader(ShaderStage stage);
const SharedPtr<ShaderObject>& GetShaderObject(Uint shader);
void MarkShaderObjectForDeletion(Uint shader);
// Frees a deletion-flagged shader's name once no program holds a GL-visible
// attachment to it (the deferred half of glDeleteShader-while-attached).
void ReleaseShaderNameIfOrphaned(Uint shader);
Bool ValidateShaderObject(Uint shader) const;
const SharedPtr<ProgramObject>& GetCurrentProgram() const { return m_currentProgram; }
private:
Bool ShaderHasGLVisibleAttachment(const SharedPtr<ShaderObject>& shaderObject) const;
template <typename T>
static Bool CheckIndexAvail(const SizeT idx, const Vector<T>& vec) {
return idx < vec.size();