diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index faf5c84b..479fedb9 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -2162,6 +2162,69 @@ void main() { EXPECT_EQ(GetError(), GL_NO_ERROR); } +// Repro for KHR-GLES31.explicit_uniform_location.uniform-loc-arrays-of-arrays: an +// array-of-arrays uniform reaches the GL surface as one entry PER SUB-ARRAY ("u0[0]", +// "u0[1]" - glslang stops expanding at reflection granularity), while SPIRV-Reflect keeps +// it as a single leaf carrying every dimension. Routing the single leaf only ever covered +// the first sub-array, so every element from u0[1][0] on found no UBO offset and fell +// through to the fallback scratch at the tail of the shadow - storage the GPU never reads, +// which made those glUniform writes silently vanish. +TEST_F(ProgramTest, ArrayOfArraysUniformElementOffsets) { + // Arrays of arrays need GLSL 4.30; both stages take the same version. + const char* vsSource = R"(#version 430 core +in vec4 a_position; +void main() { + gl_Position = a_position; +})"; + const char* fsSource = R"(#version 430 core +uniform float u0[2][3]; +uniform vec3 u1[2][2]; +out vec4 o_color; +void main() { + float s = 0.0; + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 3; ++j) s += u0[i][j]; + } + vec3 v = vec3(0.0); + for (int i = 0; i < 2; ++i) { + for (int j = 0; j < 2; ++j) v += u1[i][j]; + } + o_color = vec4(v, s); +})"; + GLuint program = LinkVsFsProgram(vsSource, fsSource); + UseProgram(program); + auto programObject = MG_State::pGLContext->GetProgramObject(program); + ASSERT_NE(programObject, nullptr); + + // std140 gives a float array element and a vec3 array element the same 16-byte slot, + // and a flattened array-of-arrays is one contiguous run of those slots. + constexpr Uint kStd140ElementStride = 16u; + + const auto checkFlattenedRun = [&](const char* base, int outer, int inner) { + Uint firstOffset = MG_State::GLState::ProgramObject::kInvalidUniformOffset; + for (int i = 0; i < outer; ++i) { + for (int j = 0; j < inner; ++j) { + const std::string name = + std::string(base) + "[" + std::to_string(i) + "][" + std::to_string(j) + "]"; + const GLint location = GetUniformLocation(program, name.c_str()); + ASSERT_GE(location, 0) << name; + const Uint offset = programObject->GetUniformOffset(static_cast(location)); + ASSERT_NE(offset, MG_State::GLState::ProgramObject::kInvalidUniformOffset) << name; + const Uint element = static_cast(i * inner + j); + if (element == 0) { + firstOffset = offset; + } else { + EXPECT_EQ(offset, firstOffset + element * kStd140ElementStride) << name; + } + } + } + }; + + checkFlattenedRun("u0", 2, 3); + checkFlattenedRun("u1", 2, 2); + 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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp index 0cbc0cba..29b32225 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp @@ -48,13 +48,16 @@ namespace MobileGL { return SPVC_BASETYPE_UNKNOWN; } - // Record one flattened leaf uniform of the global UBO into the metadata maps. - static void RecordGlobalUboLeaf(const SpvReflectBlockVariable& member, const String& name, - Uint32 offsetInUBO, SpvcMetadata& metadata) { + // Write one metadata entry. `name` is already the glslang-reflection spelling the + // GL uniform locations are keyed on, and `arrayStride`/`sizeInBytes` describe the + // entry rather than the whole declaration (they differ for a sub-array of an + // array-of-arrays - see RecordGlobalUboLeaf). + static void RecordGlobalUboLeafEntry(const SpvReflectBlockVariable& member, const String& name, + Uint32 offsetInUBO, Uint32 arrayStride, SizeT sizeInBytes, + SpvcMetadata& metadata) { metadata.plainUniformOffsetsInUBO[name] = offsetInUBO; - metadata.plainUniformMemberSizesInBytes[name] = member.size; - metadata.plainUniformArrayStridesInUBO[name] = - member.array.dims_count > 0 ? member.array.stride : 0; + metadata.plainUniformMemberSizesInBytes[name] = sizeInBytes; + metadata.plainUniformArrayStridesInUBO[name] = arrayStride; Uint32 vectorSize = member.numeric.vector.component_count; if (vectorSize == 0) vectorSize = 1; @@ -67,6 +70,70 @@ namespace MobileGL { }; } + // Record one flattened leaf uniform of the global UBO into the metadata maps. + // + // SPIRV-Reflect keeps `float u[2][3]` as ONE leaf carrying every dimension in + // array.dims[] and the INNERMOST element stride in array.stride (the outer + // ArrayStride decoration is overwritten as ParseType recurses into the element + // type, which is also why array.size == product(dims) * stride). glslang's + // reflection - which owns the names GL uniform locations are keyed on - stops at + // "reflection granularity" instead (reflection.cpp: !type.isArrayOfArrays()), so + // the same declaration arrives on the GL side as "u[0]" and "u[1]", each a + // `float[3]` holding its own three locations. + // + // Emitting a single "u" leaf here therefore only ever routes the FIRST sub-array: + // the routing loop stops as soon as a location belongs to a different uniform, and + // every element from "u[1][0]" on finds no offset and falls through to the fallback + // scratch storage at the tail of the shadow - bytes the GPU never reads, so those + // glUniform writes are silently lost + // (KHR-GLES31.explicit_uniform_location.uniform-loc-arrays-of-arrays). Expand every + // dimension but the last, exactly as glslang does, and give each sub-array its own + // byte offset. + static void RecordGlobalUboLeaf(const SpvReflectBlockVariable& member, const String& name, + Uint32 offsetInUBO, SpvcMetadata& metadata) { + const Uint32 arrayStride = member.array.dims_count > 0 ? member.array.stride : 0; + const Bool isArrayOfArrays = member.array.dims_count > 1 && arrayStride > 0; + if (!isArrayOfArrays) { + RecordGlobalUboLeafEntry(member, name, offsetInUBO, arrayStride, member.size, metadata); + return; + } + + // Extent 0 is SPIRV-Reflect's OpTypeRuntimeArray marker, which a plain uniform + // cannot be - but it must not be expanded (or divided by) if it ever appears. + Uint32 subArrayCount = 1; + for (Uint32 dim = 0; dim + 1 < member.array.dims_count; ++dim) { + const Uint32 extent = member.array.dims[dim]; + if (extent == 0) { + MGLOG_W_ONCE("RecordGlobalUboLeaf: multi-dimensional uniform '%s' has a non-constant " + "dimension, recording the base entry only", + name.c_str()); + RecordGlobalUboLeafEntry(member, name, offsetInUBO, arrayStride, member.size, metadata); + return; + } + subArrayCount *= extent; + } + const Uint32 innerExtent = member.array.dims[member.array.dims_count - 1] > 0 + ? member.array.dims[member.array.dims_count - 1] + : 1; + const Uint32 subArrayStride = innerExtent * arrayStride; + + // Row-major odometer over dims[0 .. dims_count-2]: the last dimension varies + // fastest, so `subArray` counts sub-arrays in exactly memory order. + Vector indices(member.array.dims_count - 1, 0); + for (Uint32 subArray = 0; subArray < subArrayCount; ++subArray) { + String elementName = name; + for (const Uint32 index : indices) { + elementName += "[" + std::to_string(index) + "]"; + } + RecordGlobalUboLeafEntry(member, elementName, offsetInUBO + subArray * subArrayStride, + arrayStride, subArrayStride, metadata); + for (SizeT dim = indices.size(); dim-- > 0;) { + if (++indices[dim] < member.array.dims[dim]) break; + indices[dim] = 0; + } + } + } + // Flatten a (possibly nested struct / struct array) member of the global UBO // into leaf entries named the way glslang reflection names plain uniforms: // "s[0].b[1].b" for `uniform S s[2]` with `struct T { vec2 b[2]; }` members.