[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
@@ -233,6 +233,101 @@ void main()
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
}
// The ORDERING half of the split, which `coherent` alone does not buy. Coherent makes the store
// through one variable VISIBLE to a load through the other; it says nothing about the order of
// the two within a single invocation, and the ES compiler - seeing a write to one variable and a
// read of another it has no reason to believe alias - is free to serve the read from before the
// write. That is what advanced-memory-order measured on Adreno with the coherent pair already in
// place. memoryBarrierImage() is the primitive that orders them.
TEST(SplitReadWriteImageUniformsTest, EverySplitStoreIsFollowedByAnImageMemoryBarrier) {
const String source = R"(#version 320 es
layout(binding = 2, rgba8) uniform highp image2D goku;
layout(location = 0) out highp vec4 mg_FragColor;
void main()
{
imageStore(goku, ivec2(0), vec4(1.0));
highp vec4 first = imageLoad(goku, ivec2(0));
imageStore(goku, ivec2(0), vec4(2.0));
mg_FragColor = first + imageLoad(goku, ivec2(0));
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(1.0)); memoryBarrierImage();"))
<< out;
EXPECT_TRUE(Contains(out, "imageStore(" + WriteAlias("goku") + ", ivec2(0), vec4(2.0)); memoryBarrierImage();"))
<< out;
// One per store, not one per shader and not one per load.
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 2u) << out;
}
// The barrier belongs to the SPLIT alone. A store-only image was repaired in place, nothing
// aliases it, and paying for a barrier there would slow down every shader that merely writes an
// image - which is most of them.
TEST(SplitReadWriteImageUniformsTest, ARepairedButUnsplitStoreGetsNoBarrier) {
const String source = R"(#version 320 es
layout(binding = 3, rgba8) uniform highp image2D storeOnly;
void main()
{
imageStore(storeOnly, ivec2(0), vec4(1.0));
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "uniform writeonly highp image2D storeOnly;")) << out;
EXPECT_FALSE(Contains(out, "memoryBarrierImage")) << out;
}
// The store site is found by matching the call's own parentheses, not by looking for the next
// ')', so a nested call in the value argument does not truncate the statement and the barrier
// still lands after the whole thing.
TEST(SplitReadWriteImageUniformsTest, TheBarrierLandsAfterAStoreWithNestedParentheses) {
const String source = R"(#version 320 es
layout(binding = 6, rgba8) uniform highp image2D gohan[3];
void main()
{
imageStore(gohan[1], ivec2(0), max(imageLoad(gohan[2], ivec2(0)), vec4(0.5)));
}
)";
const String out = SplitReadWriteImageUniforms(source);
EXPECT_TRUE(Contains(out, "max(imageLoad(gohan[2], ivec2(0)), vec4(0.5))); memoryBarrierImage();")) << out;
EXPECT_EQ(CountOf(out, "memoryBarrierImage();"), 1u) << out;
}
// The split is the one thing that makes a stage declare MORE image uniforms than the application
// did, and MobileGL keeps advertising GL_MAX_*_IMAGE_UNIFORMS unadjusted (lowering it would fail
// basic-api and NotSupported-out every case that only uses readonly/writeonly images). So the
// count has to be reportable, or a link failure caused by the doubling looks like a driver
// mystery - which is what KHR-GL4x.shader_image_load_store.multiple-uniforms will hit the moment
// the format work stops masking it.
TEST(SplitReadWriteImageUniformsTest, TheSplitCountIsReportedToTheCaller) {
const String twoSplits = R"(#version 320 es
layout(binding = 0, rgba8) uniform highp image2D goku;
layout(binding = 1, rgba16f) uniform highp image2D gohan;
layout(binding = 2, rgba8) uniform highp image2D storeOnly;
void main()
{
imageStore(goku, ivec2(0), imageLoad(goku, ivec2(0)));
imageStore(gohan, ivec2(0), imageLoad(gohan, ivec2(0)));
imageStore(storeOnly, ivec2(0), vec4(0.0));
}
)";
Uint splitCount = 99u;
SplitReadWriteImageUniforms(twoSplits, &splitCount);
EXPECT_EQ(splitCount, 2u) << "only the read+write pair counts; the store-only repair adds no uniform";
// Every early return has to write the count too, or a caller reads whatever was there before.
const String noImages = R"(#version 320 es
layout(location = 0) out highp vec4 mg_FragColor;
void main()
{
mg_FragColor = vec4(1.0);
}
)";
splitCount = 99u;
SplitReadWriteImageUniforms(noImages, &splitCount);
EXPECT_EQ(splitCount, 0u);
}
// imageSize reads no texels and writes none, so it decides nothing; readonly is what keeps
// such a declaration legal.
TEST(SplitReadWriteImageUniformsTest, ImageSizeAloneDoesNotCountAsALoadOrAStore) {
@@ -716,6 +716,86 @@ void main() {
EXPECT_EQ(TakeError(), GL_INVALID_ENUM);
}
// The CLASSIC query surface has to agree with the interface query above. MobileGL lowers
// every atomic_uint onto a synthesized gl_AtomicCounterBlock_N, and glGetActiveUniform /
// glGetActiveUniformsiv used to report that lowering: GL_UNSIGNED_INT instead of
// GL_UNSIGNED_INT_ATOMIC_COUNTER, the synthesized block's index instead of the -1 a
// default-block uniform owes, and GL_INVALID_ENUM for
// GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX - the last of which is what made
// KHR-GL43.shader_atomic_counters.basic-program-query a forced FAIL.
TEST_F(ProgramInterfaceTest, AtomicCounterClassicUniformQueries) {
const char* fs = R"(#version 430
out vec4 color;
layout (binding = 0, offset = 0) uniform atomic_uint ac_counter0;
layout (binding = 1, offset = 0) uniform atomic_uint ac_counter1;
uniform float plain;
void main() {
color = vec4(float(atomicCounterIncrement(ac_counter0) + atomicCounterIncrement(ac_counter1)) + plain);
}
)";
const GLuint p = MakeProgram(kSimpleVs, fs);
LinkProgram(p);
ExpectLinked(p);
ClearErrors();
const auto indexOf = [p](const char* name) {
const GLchar* names[1] = {name};
GLuint index = GL_INVALID_INDEX;
GetUniformIndices(p, 1, names, &index);
return index;
};
const auto uniformiv = [p](GLuint index, GLenum pname) {
GLint value = -12345;
const GLuint indices[1] = {index};
GetActiveUniformsiv(p, 1, indices, pname, &value);
return value;
};
const GLuint counter0 = indexOf("ac_counter0");
const GLuint counter1 = indexOf("ac_counter1");
const GLuint plain = indexOf("plain");
ASSERT_NE(counter0, GL_INVALID_INDEX);
ASSERT_NE(counter1, GL_INVALID_INDEX);
ASSERT_NE(plain, GL_INVALID_INDEX);
// (a) glGetActiveUniform and glGetActiveUniformsiv(GL_UNIFORM_TYPE) both report the
// GL-level type.
GLint size = 0;
GLenum type = 0;
GLchar nameBuffer[64] = {'\0'};
GetActiveUniform(p, counter0, sizeof(nameBuffer), nullptr, &size, &type, nameBuffer);
EXPECT_EQ(type, static_cast<GLenum>(GL_UNSIGNED_INT_ATOMIC_COUNTER));
EXPECT_EQ(std::string(nameBuffer), "ac_counter0");
EXPECT_EQ(uniformiv(counter0, GL_UNIFORM_TYPE), GL_UNSIGNED_INT_ATOMIC_COUNTER);
EXPECT_EQ(uniformiv(counter1, GL_UNIFORM_TYPE), GL_UNSIGNED_INT_ATOMIC_COUNTER);
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_TYPE), GL_FLOAT);
// (b) an atomic counter is a DEFAULT-BLOCK uniform, whatever it was lowered onto.
EXPECT_EQ(uniformiv(counter0, GL_UNIFORM_BLOCK_INDEX), -1);
EXPECT_EQ(uniformiv(counter1, GL_UNIFORM_BLOCK_INDEX), -1);
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_BLOCK_INDEX), -1);
// (c) the pname is accepted, answers with the buffer's index, and reports -1 for a
// uniform that is not a counter. Two bindings mean two distinct buffers.
const GLint buffer0 = uniformiv(counter0, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX);
const GLint buffer1 = uniformiv(counter1, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX);
EXPECT_GE(buffer0, 0);
EXPECT_GE(buffer1, 0);
EXPECT_NE(buffer0, buffer1);
EXPECT_LT(buffer0, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
EXPECT_LT(buffer1, Interfaceiv(p, GL_ATOMIC_COUNTER_BUFFER, GL_ACTIVE_RESOURCES));
EXPECT_EQ(uniformiv(plain, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX), -1);
// No leftover error: the CTS harness fails the subcase on one.
EXPECT_EQ(TakeError(), GL_NO_ERROR);
// The classic surface and the interface query name the same buffer.
const std::vector<GLint> interfaceBuffer =
PropsOf(p, GL_UNIFORM, "ac_counter0", {GL_ATOMIC_COUNTER_BUFFER_INDEX});
ASSERT_EQ(interfaceBuffer.size(), 1u);
EXPECT_EQ(interfaceBuffer[0], buffer0);
EXPECT_EQ(TakeError(), GL_NO_ERROR);
}
// Two counters that share a binding AND an offset must fail to link. glslang's own check
// lives in fixOffset(), which the Vulkan-relaxed parse never reaches - it folds the
// atomic_uint into a storage block and returns from declareVariable() first - so the pair
+75 -3
View File
@@ -1934,10 +1934,19 @@ TEST_F(ProgramTest, GetActiveUniformsivErrors) {
EXPECT_EQ(GetError(), GL_INVALID_VALUE);
EXPECT_EQ(params[0], -999);
// E3: GL 4.2 token -> GL_INVALID_ENUM here.
// E3: the GL 4.2 / ARB_shader_atomic_counters token is ACCEPTED, not rejected.
//
// This case used to assert GL_INVALID_ENUM, which was right only while the token was
// unimplemented. It is implemented now, and `validIndex` names an ordinary uniform rather
// than an atomic counter, so the spec answer is -1 with no error (GL 4.6 core table 7.6).
// ProgramInterfaceTest's atomic-counter case asserts the same -1 for a non-counter
// uniform; leaving this one inverted made the two contradict each other.
GetActiveUniformsiv(program, 1, &validIndex, GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX, params);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
EXPECT_EQ(params[0], -999);
EXPECT_EQ(GetError(), GL_NO_ERROR);
EXPECT_EQ(params[0], -1);
// Restored: the cases below assert that a REJECTED call leaves params untouched, and this
// one legitimately wrote to it.
params[0] = -999;
// E4a: a live shader name -> GL_INVALID_OPERATION.
GLuint shader = CreateShader(GL_VERTEX_SHADER);
@@ -2162,6 +2171,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<Uint>(location));
ASSERT_NE(offset, MG_State::GLState::ProgramObject::kInvalidUniformOffset) << name;
const Uint element = static_cast<Uint>(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
+163 -3
View File
@@ -3348,11 +3348,38 @@ namespace {
return count;
}
// Same word walk, for the NON-arrayed half of the family (Arrayed == 0).
SizeT Count1DNonArrayedStorageImageTypes(const Vector<Uint32>& spirv) {
constexpr unsigned kOpTypeImage = 25, kDim1D = 0;
SizeT count = 0;
for (SizeT i = 5; i < spirv.size();) {
const unsigned wordCount = spirv[i] >> 16;
const unsigned opcode = spirv[i] & 0xFFFFu;
if (wordCount == 0 || i + wordCount > spirv.size()) break;
if (opcode == kOpTypeImage && wordCount >= 8 && spirv[i + 3] == kDim1D && spirv[i + 5] == 0u &&
spirv[i + 7] == 2u) {
++count;
}
i += wordCount;
}
return count;
}
const char* k1DArrayImageCompute = R"(#version 440 core
layout (local_size_x = 1) in;
layout (location = 0, r32ui) readonly uniform uimage1DArray i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r; }
)";
// KHR-GL4x.shader_image_load_store.basic-allTargets-atomic's own shape, minus the six other
// targets: a non-arrayed 1D storage image reached ONLY through an atomic. r32ui because ES
// defines image atomics on r32i/r32ui/r32f alone.
const char* k1DImageAtomicCompute = R"(#version 440 core
layout (local_size_x = 1) in;
layout (r32ui) coherent uniform uimage1D i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageAtomicAdd(i0, 2, 7u); }
)";
} // namespace
@@ -3464,9 +3491,11 @@ void main() { ssb.sum = imageLoad(i0, ivec2(2, 3)).r + imageLoad(i1, ivec3(1, 1,
EXPECT_NE(essl.find("ivec3(2, 0, 3)"), String::npos) << essl;
}
// Scope, half one: a NON-arrayed 1D storage image is emitted correctly by the very same
// SPIRV-Cross code, so the pass must not touch it - replacing working emission with our own buys
// nothing and risks everything.
// Scope, half one: a NON-arrayed 1D storage image that is only READ or WRITTEN is emitted
// correctly by the very same SPIRV-Cross code, so the pass must not touch it - replacing working
// emission with our own buys nothing and risks everything. (The atomic shape below is the one
// exception, and it is gated on an OpImageTexelPointer actually being present, which is why this
// fixture still passes through byte for byte.)
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesNonArrayed1DImagesToSpirvCross) {
using namespace MG_Util::ShaderTranspiler;
@@ -3489,6 +3518,94 @@ void main() { ssb.sum = imageLoad(i0, 2).r; }
<< "SPIRV-Cross's own 1D-as-2D emulation must still be what handles this:\n" << essl;
}
// The negative control for the ATOMIC half, and the reason the non-arrayed case is in scope at
// all: SPIRV-Cross widens a 1D image coordinate in OpImageRead and OpImageWrite but not in
// OpImageTexelPointer, so the atomic comes out addressing an `uimage2D` with a scalar. Every ES
// driver answers "no matching overloaded function found" and the whole stage - with every other
// image in it - is lost. Pinning the upstream behaviour here means a future SPIRV-Cross bump that
// fixes it fails this test instead of leaving the lowering as silent dead weight.
TEST_F(ProgramUtilTest, SpirvCrossEmitsAScalarCoordinateForA1DImageAtomic) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(k1DImageAtomicCompute, GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
ASSERT_EQ(Count1DNonArrayedStorageImageTypes(spirv), 1u)
<< "glslang no longer emits a Dim1D/non-arrayed/Sampled=2 image for uimage1D";
const String essl = DecompileToEssl(spirv);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("uimage2D"), String::npos)
<< "SPIRV-Cross declares the 1D image as 2D on ES; that half it does do:\n" << essl;
EXPECT_NE(essl.find("imageAtomicAdd(i0, 2"), String::npos)
<< "SPIRV-Cross is expected to pass the SCALAR coordinate straight through to the atomic. "
"If this no longer happens, the non-arrayed half of Lower1DArrayImagesForEssl may no "
"longer be needed:\n"
<< essl;
EXPECT_EQ(essl.find("ivec2("), String::npos)
<< "nothing else in this fixture builds an ivec2, so its absence is the defect:\n" << essl;
}
// The fix: the type becomes a plain 2D image - which is what MobileGL stores a GL_TEXTURE_1D in,
// height 1 - and the coordinate becomes (u, 0), so the atomic type-checks against the declaration
// SPIRV-Cross was already emitting.
TEST_F(ProgramUtilTest, Lower1DArrayImagesWidensThe1DAtomicCoordinate) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> raw = BuildSpirvForStage(k1DImageAtomicCompute, GL_COMPUTE_SHADER);
ASSERT_FALSE(raw.empty());
Vector<Uint32> spirv;
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(raw, spirv));
ASSERT_EQ(Count1DNonArrayedStorageImageTypes(spirv), 1u)
<< "the shared chain must leave the 1D image for this pass to handle";
const Uint64 failuresBefore = ShaderCompiler::SpirvValidationFailureCount();
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
ASSERT_FALSE(lowered.empty());
EXPECT_EQ(Count1DNonArrayedStorageImageTypes(lowered), 0u)
<< "no non-arrayed 1D storage image type may survive when an atomic reaches one:\n"
<< DisassembleSpirv(lowered);
EXPECT_EQ(ShaderCompiler::SpirvValidationFailureCount(), failuresBefore)
<< "the lowered module must stay validator-clean";
const String essl = DecompileToEssl(lowered);
ASSERT_FALSE(essl.empty());
EXPECT_NE(essl.find("uimage2D"), String::npos)
<< "the declaration must still be the 2D one the ES texture is:\n" << essl;
EXPECT_NE(essl.find("imageAtomicAdd(i0, ivec2(2, 0)"), String::npos)
<< "the atomic must address the image with the same (u, 0) SPIRV-Cross writes for a read "
"or a write:\n"
<< essl;
}
// The declined shape for the atomic half, for the same reason as the arrayed one: after the
// rewrite the image is 2D, so imageSize() yields two components where the shader consumes one and
// there is no correct scalar to substitute.
TEST_F(ProgramUtilTest, Lower1DArrayImagesDeclinesA1DAtomicModuleThatQueriesTheImageSize) {
using namespace MG_Util::ShaderTranspiler;
const Vector<Uint32> spirv = BuildSpirvForStage(R"(#version 440 core
layout (local_size_x = 1) in;
layout (r32ui) coherent uniform uimage1D i0;
layout (std430, binding = 0) buffer SSB { uint sum; } ssb;
void main() { ssb.sum = imageAtomicAdd(i0, 2, 7u) + uint(imageSize(i0)); }
)",
GL_COMPUTE_SHADER);
ASSERT_FALSE(spirv.empty());
const auto traits = Lower1DArrayImagesPass::InspectBinary(spirv);
ASSERT_TRUE(traits.declaresImage && traits.queriesImageSize)
<< "the fixture must contain the shape the pass declines";
Vector<Uint32> lowered;
ASSERT_TRUE(ShaderCompiler::Lower1DArrayImagesForEssl(spirv, lowered, true));
EXPECT_EQ(lowered, spirv) << "a declined module must be handed back untouched, not partly rewritten";
EXPECT_EQ(Count1DNonArrayedStorageImageTypes(lowered), 1u)
<< "declining means the 1D type is still there for the driver to reject";
}
// Scope, half two: a 1D-array SAMPLER reaches SPIRV-Cross's sampler path, which does check
// `arrayed` and does move the layer into the third component. The pass is storage-image only.
TEST_F(ProgramUtilTest, Lower1DArrayImagesLeavesSampledImagesAlone) {
@@ -3947,6 +4064,49 @@ TEST_F(ProgramUtilTest, StorageBlockBindingCeilingIsCheckedAtItsExactBoundary) {
EXPECT_FALSE(FindShaderStorageBindingViolation("layout(binding = 36) buffer B { int x; };\n", 0).has_value());
}
// KHR-GL43.shader_atomic_counters.negative-offset-1: an atomic counter whose layout(offset = N)
// puts its last byte past GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE is a COMPILE-time error, and the CTS
// never links the shader at all. MobileGL only had the rule at link, because the Vulkan-relaxed
// parse never reaches glslang's fixOffset().
TEST_F(ProgramUtilTest, AtomicCounterOffsetCeilingIsCheckedAtCompile) {
using namespace MG_Util::ShaderTranspiler;
const auto violation = [](const String& body) {
return FindAtomicCounterOffsetViolation("#version 430 core\n" + body + "void main() {}\n");
};
const String maxSize = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE);
const String lastLegal = std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 4);
// The boundary itself: the last counter that still fits, and the first that does not.
EXPECT_FALSE(violation("layout(binding = 0, offset = " + lastLegal + ") uniform atomic_uint c;\n").has_value());
EXPECT_TRUE(violation("layout(binding = 0, offset = " + maxSize + ") uniform atomic_uint c;\n").has_value());
// An array occupies one word per element, so what has to fit is the LAST one.
EXPECT_FALSE(violation("layout(offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 16) +
") uniform atomic_uint c[4];\n")
.has_value());
EXPECT_TRUE(violation("layout(offset = " + std::to_string(MAX_ATOMIC_COUNTER_BUFFER_SIZE - 8) +
") uniform atomic_uint c[4];\n")
.has_value());
// An offset that is not a multiple of 4 (GL 4.6 core 7.7), and one that is.
EXPECT_TRUE(violation("layout(offset = 2) uniform atomic_uint c;\n").has_value());
EXPECT_FALSE(violation("layout(offset = 8) uniform atomic_uint c;\n").has_value());
// Things the scanner must NOT judge: a counter with no explicit offset, an `offset` that is
// an ordinary identifier rather than a layout qualifier, an array sized by an expression,
// and an offset qualifier that belongs to a different declaration.
EXPECT_FALSE(violation("uniform atomic_uint c;\nconst int offset = 99999;\n").has_value());
EXPECT_FALSE(violation("const int kCount = 4;\nlayout(offset = " + maxSize +
") uniform atomic_uint c[kCount];\n")
.has_value());
EXPECT_FALSE(violation("layout(offset = " + maxSize + ") uniform Block { int x; };\n"
"uniform atomic_uint c;\n")
.has_value());
// A source with no counter at all never pays for the scan and never reports one.
EXPECT_FALSE(FindAtomicCounterOffsetViolation("#version 430 core\nvoid main() {}\n").has_value());
}
// KHR-GL43.explicit_uniform_location.uniform-loc-nondecimal: GLSL integer literals are C-style, so
// layout(location = 0xA) is 10 and layout(location = 010) is OCTAL 8. The extractor used to accept
// a base-10 digit run and nothing else: the hex spelling failed the test entirely and the
@@ -12,6 +12,8 @@ add_executable(
UniquifyIoBlockNamesTest.cpp
LowerViewportIndexTest.cpp
ClampMultisampleFetchTest.cpp
LegalizeStorageBlockArrayIndexTest.cpp
FlattenAtomicCounterBlockTest.cpp
)
target_include_directories(SpirvPassTest PRIVATE
@@ -0,0 +1,214 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/FlattenAtomicCounterBlockTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <cstring>
#include <map>
#include <string>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
// Test-side reference walker, deliberately independent of the production code.
Uint32 FindAtomicCounterBlockStructId(const Vector<Uint32>& spirv) {
const String prefix = MG_Util::ShaderTranspiler::ATOMIC_COUNTER_BLOCK_PREFIX;
Uint32 structId = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpName || wordCount < 3u || structId != 0u) return;
const char* text = reinterpret_cast<const char*>(&words[2]);
const SizeT available = static_cast<SizeT>(wordCount - 2u) * sizeof(Uint32);
if (available < prefix.size()) return;
if (std::strncmp(text, prefix.c_str(), prefix.size()) != 0) return;
structId = words[1];
});
return structId;
}
// The Offset of member `member` on struct `structId`, or -1.
Int64 MemberOffsetOf(const Vector<Uint32>& spirv, Uint32 structId, Uint32 member) {
Int64 offset = -1;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpMemberDecorate || wordCount < 5u) return;
if (words[1] != structId || words[2] != member) return;
if (static_cast<spv::Decoration>(words[3]) != spv::Decoration::Offset) return;
offset = words[4];
});
return offset;
}
Uint32 MemberCountOf(const Vector<Uint32>& spirv, Uint32 structId) {
Uint32 count = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpTypeStruct || wordCount < 2u || words[1] != structId) return;
count = wordCount - 2u;
});
return count;
}
Uint32 MemberTypeOf(const Vector<Uint32>& spirv, Uint32 structId, Uint32 member) {
Uint32 typeId = 0;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpTypeStruct || wordCount < 3u + member || words[1] != structId) return;
typeId = words[2 + member];
});
return typeId;
}
// The declared length of an OpTypeArray, resolved through the uint constants in the module.
Int64 ArrayLengthOf(const Vector<Uint32>& spirv, Uint32 arrayTypeId) {
std::map<Uint32, Uint32> constants;
Int64 length = -1;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode == spv::Op::OpConstant && wordCount >= 4u) constants[words[2]] = words[3];
if (opcode == spv::Op::OpTypeArray && wordCount >= 4u && words[1] == arrayTypeId) {
const auto it = constants.find(words[3]);
if (it != constants.end()) length = it->second;
}
});
return length;
}
// KHR-GL43.compute_shader.resources-atomic-counter's non-zero-offset shape: two counters
// declared eight bytes into the buffer, which glslang lowers to one block member at Offset 8.
constexpr const char* kOffsetCounters = R"(#version 450 core
layout(local_size_x = 1) in;
layout(binding = 1, offset = 8) uniform atomic_uint g_counter[2];
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
void main() {
g_out.value[0] = atomicCounterIncrement(g_counter[0]);
g_out.value[1] = atomicCounterIncrement(g_counter[1]);
}
)";
// The latch: offset 0 is what nearly every shader declares, and it transpiles today.
constexpr const char* kNaturalCounters = R"(#version 450 core
layout(local_size_x = 1) in;
layout(binding = 1, offset = 0) uniform atomic_uint g_counter[2];
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
void main() {
g_out.value[0] = atomicCounterIncrement(g_counter[0]);
g_out.value[1] = atomicCounterIncrement(g_counter[1]);
}
)";
constexpr const char* kNoCounters = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Output { uint value[]; } g_out;
void main() {
g_out.value[0] = 1u;
}
)";
} // namespace
TEST(FlattenAtomicCounterBlockPass, MovesTheBlockToOffsetZeroAndGrowsTheArray) {
const Vector<Uint32> input = CompileCompute(kOffsetCounters);
ASSERT_FALSE(input.empty());
const Uint32 structId = FindAtomicCounterBlockStructId(input);
ASSERT_NE(structId, 0u) << "glslang did not lower the counters onto a gl_AtomicCounterBlock_*";
ASSERT_EQ(MemberOffsetOf(input, structId, 0u), 8) << "the input's member 0 is not at the declared offset";
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
ASSERT_FALSE(output.empty());
const Uint32 outStructId = FindAtomicCounterBlockStructId(output);
ASSERT_EQ(outStructId, structId) << "the block's id must not move; SetAtomicCounterBlockBindings "
"still finds it by name";
EXPECT_EQ(MemberCountOf(output, outStructId), 1u);
EXPECT_EQ(MemberOffsetOf(output, outStructId, 0u), 0)
<< "member 0 must sit at offset 0 or no std140/std430 layout can express the block";
// Two counters eight bytes in: the flattened array has to cover bytes [0, 16), i.e. 4 uints,
// so counter k lands on element 2 + k and therefore on byte 8 + 4k - where it was declared.
EXPECT_EQ(ArrayLengthOf(output, MemberTypeOf(output, outStructId, 0u)), 4);
EXPECT_TRUE(Validates(output));
}
TEST(FlattenAtomicCounterBlockPass, LeavesANaturallyPackedBlockByteIdentical) {
const Vector<Uint32> input = CompileCompute(kNaturalCounters);
ASSERT_FALSE(input.empty());
ASSERT_NE(FindAtomicCounterBlockStructId(input), 0u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FlattenAtomicCounterBlockPass, LeavesAShaderWithoutCountersByteIdentical) {
const Vector<Uint32> input = CompileCompute(kNoCounters);
ASSERT_FALSE(input.empty());
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, output, true));
EXPECT_EQ(output, input);
}
TEST(FlattenAtomicCounterBlockPass, IsIdempotent) {
const Vector<Uint32> input = CompileCompute(kOffsetCounters);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(input, once, true));
ASSERT_FALSE(once.empty());
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::FlattenAtomicCounterBlockOffsetsForEssl(once, twice, true));
EXPECT_EQ(twice, once);
}
@@ -0,0 +1,251 @@
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LegalizeStorageBlockArrayIndexTest.cpp
// Copyright (c) 2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include <gtest/gtest.h>
#define SPV_ENABLE_UTILITY_CODE
#include "glslang/SPIRV/spirv.hpp11"
#undef SPV_ENABLE_UTILITY_CODE
#include "Includes.h"
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <spirv-tools/libspirv.hpp>
#include <set>
#include <vector>
using namespace MobileGL;
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
namespace {
constexpr SizeT kSpirvHeaderWordCount = 5u;
template <typename Visitor>
void ForEachInstruction(const Vector<Uint32>& spirv, Visitor&& visit) {
for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) {
const Uint32 wordCount = spirv[offset] >> 16u;
if (wordCount == 0u || offset + wordCount > spirv.size()) break;
visit(static_cast<spv::Op>(spirv[offset] & 0xffffu), &spirv[offset], wordCount);
offset += wordCount;
}
}
Vector<Uint32> CompileCompute(const String& source) {
using namespace MobileGL::MG_Util::ShaderTranspiler;
ShaderAttrib shaderAttrib{.shaderType = GL_COMPUTE_SHADER, .sourceStr = source};
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
if (!shaderResult) return {};
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
if (!programResult) return {};
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_COMPUTE_SHADER}, .program = *programResult.value()};
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
if (!binaryResult || binaryResult->empty()) return {};
return binaryResult->front();
}
bool Validates(const Vector<Uint32>& spirv) {
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
tools.SetMessageConsumer(
[](spv_message_level_t, const char*, const spv_position_t& position, const char* message) {
ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message;
});
return tools.Validate(spirv);
}
Uint32 CountOpcode(const Vector<Uint32>& spirv, spv::Op wanted) {
Uint32 count = 0u;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32*, Uint32) {
if (opcode == wanted) ++count;
});
return count;
}
// Test-side reference walker, deliberately independent of the production detection so a
// bug in the pass cannot hide behind the same helper: true when some access chain rooted
// at an array-of-storage-blocks variable carries a non-constant FIRST index, which is
// exactly what the Qualcomm ES compiler refuses.
bool HasDynamicBlockArrayIndex(const Vector<Uint32>& spirv) {
std::set<Uint32> blockStructs; // OpTypeStruct ids decorated Block / BufferBlock
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
std::set<Uint32> blockArrayTypes; // OpTypeArray ids whose element is such a struct
std::set<Uint32> blockArrayPointers;// OpTypePointer ids pointing at one of those arrays
std::set<Uint32> blockArrayVars; // OpVariable ids of one of those pointer types
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
switch (opcode) {
case spv::Op::OpDecorate:
if (wordCount >= 3u) {
const auto decoration = static_cast<spv::Decoration>(words[2]);
if (decoration == spv::Decoration::Block ||
decoration == spv::Decoration::BufferBlock) {
blockStructs.insert(words[1]);
}
}
break;
case spv::Op::OpConstant:
if (wordCount >= 3u) constants.insert(words[2]);
break;
case spv::Op::OpConstantNull:
if (wordCount >= 3u) constants.insert(words[2]);
break;
case spv::Op::OpTypeArray:
// OpTypeArray <result> <element type> <length>
if (wordCount >= 4u && blockStructs.count(words[2]) != 0u) {
blockArrayTypes.insert(words[1]);
}
break;
case spv::Op::OpTypePointer:
// OpTypePointer <result> <storage class> <pointee>
if (wordCount >= 4u && blockArrayTypes.count(words[3]) != 0u) {
blockArrayPointers.insert(words[1]);
}
break;
case spv::Op::OpVariable:
// OpVariable <result type> <result> <storage class>
if (wordCount >= 4u && blockArrayPointers.count(words[1]) != 0u) {
blockArrayVars.insert(words[2]);
}
break;
default:
break;
}
});
bool dynamic = false;
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
if (opcode != spv::Op::OpAccessChain && opcode != spv::Op::OpInBoundsAccessChain) return;
// OpAccessChain <result type> <result> <base> <index 0> ...
if (wordCount < 5u) return;
if (blockArrayVars.count(words[3]) == 0u) return;
if (constants.count(words[4]) != 0u) return;
dynamic = true;
});
return dynamic;
}
// `for (i = 0; i < 4; ++i)` over an array of storage blocks - the shape
// KHR-GL43.shader_storage_buffer_object.basic-stdLayout-case1 uses. Foldable: the
// induction variable is a literal after unrolling.
constexpr const char* kLoopIndexedBlockArray = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
layout(std430, binding = 8) buffer Out { uint data[4]; } g_out;
void main() {
for (int i = 0; i < 4; ++i) {
g_out.data[i] = g_blocks[i].data[0];
}
}
)";
// A uniform-sourced index - the shape
// KHR-GL43.shader_storage_buffer_object.advanced-indirectAddressing-case2 uses. Nothing
// can fold it, so the switch/select lowering is what has to carry it.
constexpr const char* kUniformIndexedBlockArray = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
layout(std430, binding = 8) buffer Out { uint value; } g_out;
uniform int g_index;
void main() {
g_blocks[g_index].data[0] = 7u;
g_out.value = g_blocks[g_index].data[1];
}
)";
// The positive control from the device run: dynamic addressing through an array MEMBER of
// ONE block is legal ES and must not be rewritten.
constexpr const char* kArrayMemberInsideOneBlock = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_block;
layout(std430, binding = 8) buffer Out { uint value; } g_out;
uniform int g_index;
void main() {
g_out.value = g_block.data[g_index];
}
)";
// A block array indexed only with literals is already legal ES.
constexpr const char* kConstantIndexedBlockArray = R"(#version 450 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Blk { uint data[4]; } g_blocks[4];
layout(std430, binding = 8) buffer Out { uint value; } g_out;
void main() {
g_out.value = g_blocks[2].data[0] + g_blocks[3].data[1];
}
)";
} // namespace
TEST(LegalizeStorageBlockArrayIndexPass, FoldsALoopIndexedBlockArray) {
const Vector<Uint32> input = CompileCompute(kLoopIndexedBlockArray);
ASSERT_FALSE(input.empty());
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
ASSERT_FALSE(output.empty());
// Either half of the legalization is an acceptable outcome here - what the ES driver
// cares about is only that no dynamic subscript survives.
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
EXPECT_TRUE(Validates(output));
}
TEST(LegalizeStorageBlockArrayIndexPass, LowersAUniformIndexedWriteToASwitchAndAReadToSelects) {
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
ASSERT_FALSE(input.empty());
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
ASSERT_FALSE(output.empty());
EXPECT_FALSE(HasDynamicBlockArrayIndex(output));
// One switch for the store, and one select per element past the first for the load.
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
EXPECT_TRUE(Validates(output));
}
TEST(LegalizeStorageBlockArrayIndexPass, LeavesADynamicMemberOfOneBlockByteIdentical) {
const Vector<Uint32> input = CompileCompute(kArrayMemberInsideOneBlock);
ASSERT_FALSE(input.empty());
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
EXPECT_EQ(output, input);
}
TEST(LegalizeStorageBlockArrayIndexPass, LeavesAConstantIndexedBlockArrayByteIdentical) {
const Vector<Uint32> input = CompileCompute(kConstantIndexedBlockArray);
ASSERT_FALSE(input.empty());
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
Vector<Uint32> output;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, output, true));
EXPECT_EQ(output, input);
}
TEST(LegalizeStorageBlockArrayIndexPass, IsIdempotent) {
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
ASSERT_FALSE(input.empty());
Vector<Uint32> once;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(input, once, true));
ASSERT_FALSE(once.empty());
Vector<Uint32> twice;
ASSERT_TRUE(ShaderCompiler::LegalizeStorageBlockArrayIndexingForEssl(once, twice, true));
EXPECT_EQ(twice, once);
}
@@ -559,3 +559,76 @@ TEST_F(RenderStateTest, IndexedRectangleQueriesRejectAnOutOfRangeIndex) {
MG_Impl::GLImpl::GetDoublei_v(GL_DEPTH_RANGE, kMaxViewports - 1, doubles);
ExpectSingleGlError(GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------------------
// "Has the application written this scissor rectangle?" - the flag, not the extent
// ---------------------------------------------------------------------------------------------
// glScissor(0, 0, 0, 0) is legal GL and means "the scissor test rejects every fragment", but it
// is byte-identical to the never-written default, whose meaning is the opposite ("the whole
// window", which the frontend cannot spell before a surface exists). DirectGLES resolved the two
// by looking at the EXTENT and so inverted every deliberately empty box into the full surface -
// KHR-GL43.viewport_array.scissor_zero_dimension is exactly that draw, and it came back holding
// the drawn colour where the untouched fill was required.
//
// These drive RenderState directly instead of the GL entry points on purpose: the flag's whole
// content is what it says BEFORE the first scissor call of a context, and this binary shares one
// context across every case in the file, so a pristine object is the only place that state
// still exists by the time these run.
namespace {
constexpr Uint32 kAllViewportsWritten =
RenderStateParameters::MAX_VIEWPORTS >= 32 ? ~0u : (1u << RenderStateParameters::MAX_VIEWPORTS) - 1u;
} // namespace
TEST_F(RenderStateTest, AnEmptyScissorBoxIsDistinguishableFromNeverHavingBeenWritten) {
MG_State::GLState::RenderState state;
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, 0u)
<< "a fresh context has never been given a scissor box, and the all-zero rectangle it "
"starts with must not be mistaken for one";
// Not one stored byte moves here - every box already held (0,0,0,0) - and yet this is the
// call that turns "the frontend does not know the window size" into "reject every fragment".
state.SetScissorBox(IntVec4(0, 0, 0, 0));
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, kAllViewportsWritten);
for (GLuint index = 0; index < kMaxViewports; ++index) {
EXPECT_EQ(state.GetScissorBoxIndexed(index), IntVec4(0, 0, 0, 0)) << "index " << index;
}
}
TEST_F(RenderStateTest, AnIndexedScissorWriteClaimsOnlyItsOwnIndex) {
MG_State::GLState::RenderState state;
state.SetScissorBoxIndexed(5, IntVec4(0, 0, 0, 0));
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, 1u << 5);
state.SetScissorBoxIndexed(0, IntVec4(0, 0, 0, 0));
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, (1u << 5) | 1u);
// ARB_viewport_array makes the non-indexed setter a write to every index, so it claims all 16.
state.SetScissorBox(IntVec4(1, 2, 3, 4));
EXPECT_EQ(state.GetAllParameters().ScissorBoxWrittenMask, kAllViewportsWritten);
}
TEST_F(RenderStateTest, TheFirstScissorWriteBumpsTheVersionEvenWhenTheValueDoesNotMove) {
// Load-bearing, and not merely tidy: DirectGLES' SyncRenderState early-outs on an unchanged
// render-state version BEFORE it reaches the span memcmp that would otherwise notice the
// flag. A version-less transition would sit in the parameter block, never be pushed, and the
// empty box would go on rendering as the whole surface.
MG_State::GLState::RenderState state;
const Uint initial = state.GetVersion();
state.SetScissorBox(IntVec4(0, 0, 0, 0));
EXPECT_GT(state.GetVersion(), initial) << "claiming the rectangle is itself a state change";
// Once claimed, a genuinely redundant write stays free - the flag costs one transition, not
// a version bump per call.
const Uint settled = state.GetVersion();
state.SetScissorBox(IntVec4(0, 0, 0, 0));
EXPECT_EQ(state.GetVersion(), settled);
MG_State::GLState::RenderState indexed;
const Uint indexedInitial = indexed.GetVersion();
indexed.SetScissorBoxIndexed(3, IntVec4(0, 0, 0, 0));
EXPECT_GT(indexed.GetVersion(), indexedInitial);
const Uint indexedSettled = indexed.GetVersion();
indexed.SetScissorBoxIndexed(3, IntVec4(0, 0, 0, 0));
EXPECT_EQ(indexed.GetVersion(), indexedSettled);
}
+31 -8
View File
@@ -4982,10 +4982,14 @@ TEST_F(TextureTest, CopyImageSubDataCountsCubeMapFacesOnTheZAxis) {
ExpectSingleGlError(GL_INVALID_VALUE);
}
// The other axis convention: GL puts a 1D ARRAY's layers on y for this entry point (srcY is the
// first layer, srcHeight the layer count), which is also where this frontend keeps them - so the
// level extent answers directly and z stays a single slice.
TEST_F(TextureTest, CopyImageSubDataBoundsA1DArraysLayersOnTheYAxis) {
// The other axis convention, and it is NOT the one this frontend stores. GL 4.6 core 18.3.2
// treats every array texture as a stack of slices on Z and gives a 1D array an image HEIGHT OF
// ONE (which is exactly what the CTS asserts: it forces height = 1 for GL_TEXTURE_1D_ARRAY and
// lists the target as multilayer). MobileGL keeps a 1D array's layers on y internally - that is
// what glTexImage2D(GL_TEXTURE_1D_ARRAY, w, layers) writes - so this entry point has to convert,
// and measuring srcY against the LAYER count is what let an out-of-range srcY come back
// GL_NO_ERROR (KHR-GL43.copy_image.exceeding_boundaries, the src_test_case y variants).
TEST_F(TextureTest, CopyImageSubDataBoundsA1DArraysLayersOnTheZAxis) {
const ScopedTextureBackendFunctionsOverride backendGuard;
MG_Backend::gBackendFunctionsTable.GL.CopyImageSubData = RecordCopyImageSubData;
g_copyImageSubDataCall = {};
@@ -5006,14 +5010,33 @@ TEST_F(TextureTest, CopyImageSubDataBoundsA1DArraysLayersOnTheYAxis) {
GTEST_SKIP() << "this context could not give the 1D arrays storage";
}
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 3, 0, dstTexture, GL_TEXTURE_1D_ARRAY,
0, 0, 3, 0, 4, 5, 1);
// 16 wide, 8 layers. Five layers from layer 3 is legal, and it is spelled on z with a
// height of 1 - the layer count rides on srcDepth.
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 0, 3, dstTexture, GL_TEXTURE_1D_ARRAY,
0, 0, 0, 3, 4, 1, 5);
EXPECT_TRUE(g_copyImageSubDataCall.Called);
EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR);
// One layer past the last one is out of bounds on z.
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 4, 0, dstTexture, GL_TEXTURE_1D_ARRAY,
0, 0, 0, 0, 4, 5, 1);
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 0, 4, dstTexture, GL_TEXTURE_1D_ARRAY,
0, 0, 0, 0, 4, 1, 5);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
// The image is one texel HIGH whatever its layer count is, so any srcY past 0 is out of
// bounds - this is the KHR-GL43.copy_image case that used to be measured against the 8
// layers and pass.
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 6, 0, dstTexture, GL_TEXTURE_1D_ARRAY,
0, 0, 6, 0, 4, 1, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
// ... and a height of 1 at y = 0 is the only legal y extent.
g_copyImageSubDataCall = {};
MG_Impl::GLImpl::CopyImageSubData(srcTexture, GL_TEXTURE_1D_ARRAY, 0, 0, 0, 0, dstTexture, GL_TEXTURE_1D_ARRAY,
0, 0, 0, 0, 4, 2, 1);
EXPECT_FALSE(g_copyImageSubDataCall.Called);
ExpectSingleGlError(GL_INVALID_VALUE);
}