mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 13:48:30 +09:00
[Merge] (DirectGLES, GLState, ShaderTranspiler): land GL43 wave5 with its two new passes inside the L2 boundary
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user