[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
@@ -2732,6 +2732,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
ResolveBackendEsslVersion());
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
// Emit against highp default precision in every stage. SPIRV-Cross's fragment
// default is mediump, under which a RelaxedPrecision struct member prints with
// NO qualifier; ForceSupporterOutput later swaps the header to highp, silently
// flipping such members to highp. A uniform-block member that stays explicitly
// "mediump" in the vertex stage then mismatches, and the ES driver refuses to
// link ("definitions of uniform block ... do not match"). With highp defaults
// every relaxed member is printed with an explicit qualifier in both stages.
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES_DEFAULT_FLOAT_PRECISION_HIGHP,
SPVC_TRUE);
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES_DEFAULT_INT_PRECISION_HIGHP,
SPVC_TRUE);
spvcSession.SetOptions(options);
@@ -339,6 +339,9 @@ namespace MobileGL::MG_Impl::GLImpl {
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "Shader is not attached to program."));
return;
}
// A shader flagged with glDeleteShader lives on while attached; this detach may
// have been its last GL-visible attachment.
MG_State::pGLContext->ReleaseShaderNameIfOrphaned(shader);
}
void GetActiveAttrib_State(GLuint program, GLuint index, GLsizei bufSize, GLsizei* length, GLint* size,
@@ -1497,9 +1500,13 @@ namespace MobileGL::MG_Impl::GLImpl {
MGLOG_D("%s: GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER = %d", __func__, *params);
break;
case GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES: {
// Member entries of an arrayed block are recorded against the first instance;
// every instance of the array reports that shared member set (matches
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, which scans with the same owner index).
const Int ownerIndex = static_cast<Int>(programObject->GetUniformBlockMemberOwnerIndex(uniformBlockIndex));
GLint uniformIndexCount = 0;
for (Uint uniformIndex = 0; uniformIndex < programObject->GetUniformCount(); ++uniformIndex) {
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != static_cast<Int>(uniformBlockIndex)) {
if (programObject->GetActiveUniformBlockIndex(uniformIndex) != ownerIndex) {
continue;
}
params[uniformIndexCount++] = static_cast<GLint>(uniformIndex);
+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();
+229
View File
@@ -2053,3 +2053,232 @@ void main() {
EXPECT_EQ(readback, 9.0f); // untouched by the overlong write
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------
// GL CTS KHR-GL33.shaders.uniform_block regression pack. MobileGL's SPIR-V
// pipeline lays every uniform block out as std140; the frontend implements the
// GL-visible consequences of that choice: packed/shared qualifiers compile (as
// std140), reflection uses GL naming ("arr[0]", per-element struct arrays),
// unused block members stay active, block sizes are vec4-padded, and array
// strides are std140 even for arrays nested inside struct members.
// ---------------------------------------------------------------------------
TEST_F(ProgramTest, UniformBlockPackedAndSharedLayoutsCompileAsStd140) {
const char* fsSource = R"(#version 330
layout(packed) uniform PackedBlock {
vec4 pv;
};
layout(shared, row_major) uniform SharedBlock {
float sf;
mat4 sm;
};
out vec4 o_color;
void main() {
o_color = pv + vec4(sf) + vec4(sm[0][0]);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
// The blocks land on the implementation's chosen layout: std140 offsets.
const GLuint pv = UniformIndexByName(program, "pv");
const GLuint sf = UniformIndexByName(program, "sf");
const GLuint sm = UniformIndexByName(program, "sm");
ASSERT_NE(pv, GL_INVALID_INDEX);
ASSERT_NE(sf, GL_INVALID_INDEX);
ASSERT_NE(sm, GL_INVALID_INDEX);
EXPECT_EQ(QueryUniformiv(program, pv, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, sf, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_OFFSET), 16);
// The remaining qualifiers in the rewritten layout() list survive.
EXPECT_EQ(QueryUniformiv(program, sm, GL_UNIFORM_IS_ROW_MAJOR), 1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockReflectsUnusedMembersWithGLNamesAndPaddedSize) {
const char* fsSource = R"(#version 330
layout(std140) uniform Blk {
float used;
vec4 unusedArr[3];
ivec3 tail;
};
out vec4 o_color;
void main() {
o_color = vec4(used);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
const GLuint blockIndex = GetUniformBlockIndex(program, "Blk");
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
// All three members are active (unusedArr and tail are never read), the array is
// reported under its GL name "unusedArr[0]", and both spellings resolve.
const GLuint used = UniformIndexByName(program, "used");
const GLuint unusedSuffixed = UniformIndexByName(program, "unusedArr[0]");
const GLuint unusedBare = UniformIndexByName(program, "unusedArr");
const GLuint tail = UniformIndexByName(program, "tail");
ASSERT_NE(used, GL_INVALID_INDEX);
ASSERT_NE(unusedSuffixed, GL_INVALID_INDEX);
ASSERT_NE(tail, GL_INVALID_INDEX);
EXPECT_EQ(unusedSuffixed, unusedBare);
char nameBuf[64] = "";
GLsizei nameLen = 0;
GLint arraySize = 0;
GLenum type = 0;
GetActiveUniform(program, unusedSuffixed, sizeof(nameBuf), &nameLen, &arraySize, &type, nameBuf);
EXPECT_STREQ(nameBuf, "unusedArr[0]");
EXPECT_EQ(arraySize, 3);
EXPECT_EQ(type, static_cast<GLenum>(GL_FLOAT_VEC4));
// std140 layout of the unused members.
EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_OFFSET), 16);
EXPECT_EQ(QueryUniformiv(program, unusedSuffixed, GL_UNIFORM_ARRAY_STRIDE), 16);
EXPECT_EQ(QueryUniformiv(program, tail, GL_UNIFORM_OFFSET), 64);
// GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS agrees with the INDICES list and counts all members.
GLint activeInBlock = 0;
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &activeInBlock);
ASSERT_EQ(activeInBlock, 3);
GLint indices[3] = {-1, -1, -1};
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, indices);
for (GLint index : indices) {
EXPECT_TRUE(index == static_cast<GLint>(used) || index == static_cast<GLint>(unusedSuffixed) ||
index == static_cast<GLint>(tail));
}
// The block ends with an ivec3 at offset 64 (unpadded end 76); the backend compiles
// the std140 block at its vec4-padded size, and the reported size must cover it or
// buffers sized from this query are too small to draw with.
GLint dataSize = 0;
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
EXPECT_EQ(dataSize, 80);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockStructArrayExpandsPerElementWithStd140Strides) {
const char* fsSource = R"(#version 330
struct S {
ivec2 v[2];
float f;
};
layout(std140) uniform Blk2 {
S s[2];
} inst;
out vec4 o_color;
void main() {
o_color = vec4(inst.s[0].f);
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
// ARB_program_interface_query naming: one entry per struct array element, prefixed
// with the BLOCK name (not the instance name), basic arrays suffixed with "[0]".
const GLuint v0 = UniformIndexByName(program, "Blk2.s[0].v[0]");
const GLuint f0 = UniformIndexByName(program, "Blk2.s[0].f");
const GLuint v1 = UniformIndexByName(program, "Blk2.s[1].v[0]");
const GLuint f1 = UniformIndexByName(program, "Blk2.s[1].f");
ASSERT_NE(v0, GL_INVALID_INDEX);
ASSERT_NE(f0, GL_INVALID_INDEX);
ASSERT_NE(v1, GL_INVALID_INDEX);
ASSERT_NE(f1, GL_INVALID_INDEX);
// std140: ivec2 v[2] rounds each element up to a vec4 (stride 16, NOT the tight 8
// glslang reflects for arrays nested inside a struct member); struct size rounds to
// 48, giving s[1] members a 48-byte bias.
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_OFFSET), 0);
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_ARRAY_STRIDE), 16);
EXPECT_EQ(QueryUniformiv(program, v0, GL_UNIFORM_SIZE), 2);
EXPECT_EQ(QueryUniformiv(program, f0, GL_UNIFORM_OFFSET), 32);
EXPECT_EQ(QueryUniformiv(program, v1, GL_UNIFORM_OFFSET), 48);
EXPECT_EQ(QueryUniformiv(program, f1, GL_UNIFORM_OFFSET), 80);
GLint dataSize = 0;
const GLuint blockIndex = GetUniformBlockIndex(program, "Blk2");
ASSERT_NE(blockIndex, GL_INVALID_INDEX);
GetActiveUniformBlockiv(program, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &dataSize);
EXPECT_EQ(dataSize, 96);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, UniformBlockInstanceArrayReportsPerInstanceBlocks) {
const char* fsSource = R"(#version 330
layout(std140) uniform ArrBlk {
vec4 av;
} insts[2];
out vec4 o_color;
void main() {
o_color = insts[0].av + insts[1].av;
})";
GLuint program = LinkVsFsProgram(kPassthroughCoordsVs, fsSource);
const GLuint inst0 = GetUniformBlockIndex(program, "ArrBlk[0]");
const GLuint inst1 = GetUniformBlockIndex(program, "ArrBlk[1]");
ASSERT_NE(inst0, GL_INVALID_INDEX);
ASSERT_NE(inst1, GL_INVALID_INDEX);
EXPECT_NE(inst0, inst1);
// A bare block name resolves to the first instance.
EXPECT_EQ(GetUniformBlockIndex(program, "ArrBlk"), inst0);
// Every instance of the array shares the single reflected member set.
GLint count0 = 0;
GLint count1 = 0;
GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count0);
GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS, &count1);
EXPECT_EQ(count0, 1);
EXPECT_EQ(count1, 1);
GLint index0 = -1;
GLint index1 = -1;
GetActiveUniformBlockiv(program, inst0, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index0);
GetActiveUniformBlockiv(program, inst1, GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES, &index1);
EXPECT_EQ(index0, index1);
EXPECT_EQ(static_cast<GLuint>(index0), UniformIndexByName(program, "ArrBlk.av"));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
TEST_F(ProgramTest, DeleteShaderWhileAttachedKeepsNameUsableUntilDetach) {
// GL CTS compiles through exactly this sequence (create, attach, DELETE, source,
// compile): glDeleteShader on an attached shader only flags it, and the name must
// keep working until the last detach.
const char* vsSource = R"(#version 330
void main() { gl_Position = vec4(0.0); }
)";
const char* fsSource = R"(#version 330
out vec4 o_color;
void main() { o_color = vec4(1.0); }
)";
GLuint program = CreateProgram();
GLuint vs = CreateShader(GL_VERTEX_SHADER);
AttachShader(program, vs);
DeleteShader(vs);
EXPECT_EQ(IsShader(vs), GL_TRUE); // still alive: attached
ShaderSource(vs, 1, &vsSource, nullptr);
CompileShader(vs);
GLint status = GL_FALSE;
GetShaderiv(vs, GL_COMPILE_STATUS, &status);
EXPECT_EQ(status, GL_TRUE);
status = GL_FALSE;
GetShaderiv(vs, GL_DELETE_STATUS, &status);
EXPECT_EQ(status, GL_TRUE);
GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
AttachShader(program, fs);
DeleteShader(fs);
ShaderSource(fs, 1, &fsSource, nullptr);
CompileShader(fs);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
char infoLog[1024] = "";
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
EXPECT_EQ(linkStatus, GL_TRUE) << infoLog;
// The last GL-visible detach releases the flagged shader's name.
DetachShader(program, vs);
EXPECT_EQ(IsShader(vs), GL_FALSE);
// Deleting the program releases the other flagged shader.
DeleteProgram(program);
EXPECT_EQ(IsShader(fs), GL_FALSE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
@@ -1017,3 +1017,57 @@ void main() {
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(binRes->at(0), optimized));
}
TEST_F(ProgramUtilTest, PreprocessCoercesBlockPackingQualifiersToStd140) {
using namespace MG_Util::ShaderTranspiler;
// glslang rejects `packed`/`shared` outright when generating SPIR-V, and MobileGL's
// UBO layout is always std140 anyway; the preprocessor rewrites the qualifiers so the
// validation compile, reflection, and generated SPIR-V all agree on std140 (GL CTS
// KHR-GL33.shaders.uniform_block.*.packed/shared).
String source = R"(#version 330
layout(packed) uniform PackedBlock { vec4 pv; };
layout(shared, row_major) uniform SharedBlock { mat4 sm; };
layout ( shared ) uniform SpacedBlock { float sx; };
layout(std140) uniform KeptBlock { float kx; };
// A non-layout use of the identifier stays untouched (compute storage qualifier).
void main() {
gl_Position = pv + vec4(sm[0][0]) + vec4(sx) + vec4(kx);
})";
PreprocessShaderSource(ShaderStage::Vertex, source);
EXPECT_EQ(source.find("packed"), String::npos);
EXPECT_EQ(source.find("layout(shared"), String::npos);
EXPECT_NE(source.find("layout(std140) uniform PackedBlock"), String::npos);
EXPECT_NE(source.find("layout(std140, row_major) uniform SharedBlock"), String::npos);
EXPECT_NE(source.find("layout ( std140 ) uniform SpacedBlock"), String::npos);
EXPECT_NE(source.find("layout(std140) uniform KeptBlock"), String::npos);
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER,
.sourceStr = source,
.flags = ShaderCompileBits::CompileForOpenGL};
auto res = ShaderCompiler::CompileShader(attrib);
if (!res) {
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
}
}
TEST_F(ProgramUtilTest, PreprocessLeavesComputeSharedStorageQualifierAlone) {
using namespace MG_Util::ShaderTranspiler;
// `shared` is only a packing qualifier inside layout(...); the compute-shader storage
// qualifier of the same spelling must survive.
String source = R"(#version 430
layout(local_size_x = 8) in;
shared float sharedScratch[8];
layout(shared) uniform Blk { float bx; };
void main() {
sharedScratch[gl_LocalInvocationIndex] = bx;
})";
PreprocessShaderSource(ShaderStage::Compute, source);
EXPECT_NE(source.find("shared float sharedScratch[8];"), String::npos);
EXPECT_NE(source.find("layout(std140) uniform Blk"), String::npos);
}
@@ -261,6 +261,65 @@ namespace {
ReplaceIdentifier(source, "GL_ARB_gpu_shader_int64", "MG_DISABLED_GL_ARB_gpu_shader_int64");
}
// 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
// std140 (glslang under a SPIR-V target rejects `packed`/`shared` outright and SPIRV-Cross has
// no other packing for UBOs), so std140 IS this implementation's chosen layout. Rewriting at
// the source level keeps the validation compile, the reflection the app queries, and the
// generated SPIR-V all agreeing on that choice. Both replacement tokens are 6 characters, so
// the rewrite is done in place.
void CoerceUniformBlockPackingToStd140(MobileGL::String& source) {
constexpr const char* layoutToken = "layout";
constexpr SizeT layoutLen = 6;
SizeT pos = 0;
while ((pos = source.find(layoutToken, pos)) != MobileGL::String::npos) {
const bool hasLeftBoundary = pos == 0 || !IsIdentifierChar(source[pos - 1]);
SizeT probe = pos + layoutLen;
const bool hasRightBoundary = probe >= source.size() || !IsIdentifierChar(source[probe]);
if (!hasLeftBoundary || !hasRightBoundary) {
pos = probe;
continue;
}
while (probe < source.size() && std::isspace(static_cast<unsigned char>(source[probe]))) {
probe++;
}
if (probe >= source.size() || source[probe] != '(') {
pos = probe;
continue;
}
// Scan the qualifier list; layout qualifier values may contain parenthesized
// constant expressions, so track nesting until the matching ')'.
SizeT cursor = probe + 1;
int depth = 1;
while (cursor < source.size() && depth > 0) {
const char ch = source[cursor];
if (ch == '(') {
depth++;
} else if (ch == ')') {
depth--;
} else if (IsIdentifierChar(ch) && (cursor == 0 || !IsIdentifierChar(source[cursor - 1]))) {
SizeT identifierEnd = cursor;
while (identifierEnd < source.size() && IsIdentifierChar(source[identifierEnd])) {
identifierEnd++;
}
const SizeT identifierLen = identifierEnd - cursor;
if (identifierLen == 6 && (source.compare(cursor, 6, "packed") == 0 ||
source.compare(cursor, 6, "shared") == 0)) {
source.replace(cursor, 6, "std140");
}
cursor = identifierEnd;
continue;
}
cursor++;
}
pos = cursor;
}
}
void ModernizeLegacyGLSL(MobileGL::ShaderStage stage, MobileGL::String& source) {
// Precision qualifiers (highp/mediump/lowp and default-precision statements) are legal and
// ignored in the forced "#version 460 core" profile, so glslang handles them natively.
@@ -383,6 +442,7 @@ namespace MobileGL {
}
FilterUnsupportedGpuShaderInt64(source);
CoerceUniformBlockPackingToStd140(source);
// Some shader packs define helpers with built-in GLSL names such as round(), tanh(), or fma().
// These may pass OpenGL-style validation but fail when recompiled for Vulkan/SPIR-V generation.