mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[Fix, Test] (ShaderTranspiler, DirectGLES): make every emitted image-array subscript a compile-time constant
This commit is contained in:
@@ -21,7 +21,7 @@ using MobileGL::MG_Backend::DirectGLES::PrgramImpl::BuildPassthroughTessControlE
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ExtractPerVertexBlockMembers;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ForceFlatIntegerVaryings;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_STAGE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_UNIT_MAP_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_ARRAY_ELEMENT_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::IMAGE_WRITE_ALIAS_PREFIX;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageArrayUnitPlan;
|
||||
using MobileGL::MG_Backend::DirectGLES::PrgramImpl::ImageStageAliasPrefix;
|
||||
@@ -52,7 +52,10 @@ namespace {
|
||||
// The writeonly half is minted from the ALREADY stage-tagged name, so it carries both.
|
||||
String WriteAlias(const String& name) { return String(IMAGE_WRITE_ALIAS_PREFIX) + name; }
|
||||
String SplitWriteAlias(const String& name) { return WriteAlias(StageAlias(name)); }
|
||||
String UnitMap(const String& name) { return String(IMAGE_UNIT_MAP_PREFIX) + name; }
|
||||
// The scalar RemapImageArrayElementUnits declares for one element of a split image array.
|
||||
String Elem(const String& name, Int element) {
|
||||
return String(IMAGE_ARRAY_ELEMENT_PREFIX) + name + "_" + std::to_string(element);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The bug the pass exists for. SPIRV-Cross speculatively marks every storage image
|
||||
@@ -517,17 +520,18 @@ TEST(SplitReadWriteImageUniformsTest, EveryStageTagIsDistinct) {
|
||||
// image uniform - there is no API side to fix, so the emitted text has to carry it.
|
||||
|
||||
namespace {
|
||||
// The advanced-sso-simple shape: a four-element image array on units 0, 2, 4, 6, written
|
||||
// through a loop counter (which is how SPIRV-Cross emits the conformance case's
|
||||
// `for (int i = 0; i < g_image.length(); ++i)` when it does not unroll it).
|
||||
// The advanced-sso-simple shape: a four-element image array on units 0, 2, 4, 6. The
|
||||
// subscripts are literals because LegalizeResourceArrayIndexingForEssl has already folded
|
||||
// the conformance case's `for (int i = 0; i < g_image.length(); ++i)` - ESSL forbids a
|
||||
// non-constant image-array subscript outright, so a loop counter never reaches this pass.
|
||||
const char* const kSsoImageArrayFS = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
imageStore(g_image[i], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
}
|
||||
imageStore(g_image[0], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[1], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[2], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
imageStore(g_image[3], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
|
||||
@@ -539,26 +543,34 @@ void main()
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// The defect, end to end. Elements 0..3 need units 0, 2, 4, 6, so the array is widened to cover
|
||||
// units 0..6 and every subscript is routed through the offset table. Before this, the single
|
||||
// stamped binding sent the four elements to units 0, 1, 2, 3.
|
||||
TEST(RemapImageArrayElementUnitsTest, NonConsecutiveUnitsWidenTheArrayAndRouteEverySubscript) {
|
||||
// The defect, end to end. Elements 0..3 need units 0, 2, 4, 6, so the array becomes four scalars
|
||||
// carrying those four bindings. Before this, the single stamped binding sent the four elements to
|
||||
// units 0, 1, 2, 3.
|
||||
TEST(RemapImageArrayElementUnitsTest, NonConsecutiveUnitsSplitIntoOneScalarPerElement) {
|
||||
Vector<String> declined;
|
||||
const String out =
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, 8, &declined);
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, &declined);
|
||||
|
||||
EXPECT_TRUE(declined.empty()) << (declined.empty() ? String() : declined[0]);
|
||||
// Seven elements from binding 0, i.e. units 0..6 - the span the four assigned units need.
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[7];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "const highp int " + UnitMap("g_image") + "[4] = int[4](0, 2, 4, 6);")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(g_image[" + UnitMap("g_image") + "[i]], ivec2(gl_FragCoord.xy)")) << out;
|
||||
// The original four-element extent is gone; nothing may still address units 0,1,2,3.
|
||||
const Int units[4] = {0, 2, 4, 6};
|
||||
for (Int element = 0; element < 4; ++element) {
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba32f, binding = " + std::to_string(units[element]) +
|
||||
") uniform writeonly highp image2D " + Elem("g_image", element) + ";"))
|
||||
<< out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", element) + ", ivec2(gl_FragCoord.xy)"))
|
||||
<< out;
|
||||
}
|
||||
// The array is gone entirely; nothing may still address units 0,1,2,3 through it.
|
||||
EXPECT_FALSE(Contains(out, "image2D g_image[4];")) << out;
|
||||
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
||||
// Exactly the four image uniforms the application declared - what the earlier widening cost
|
||||
// was the whole SPAN, seven here, which is the budget failure mode this shape removes.
|
||||
EXPECT_EQ(CountOf(out, "image2D "), 4u) << out;
|
||||
}
|
||||
|
||||
// The other program of the same conformance case: units 1, 3, 5, 7, so the binding rebases onto
|
||||
// the LOWEST unit rather than staying on element [0]'s.
|
||||
TEST(RemapImageArrayElementUnitsTest, TheBindingRebasesOntoTheLowestUnitInTheSpan) {
|
||||
// The other program of the same conformance case: units 1, 3, 5, 7 in the application's own
|
||||
// element ORDER, which is what carries the assignment, so it must NOT be sorted or rebased.
|
||||
TEST(RemapImageArrayElementUnitsTest, EachElementCarriesTheUnitTheApplicationGaveIt) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 3) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
@@ -567,14 +579,17 @@ void main()
|
||||
imageStore(g_image[3], ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
const String out = RemapImageArrayElementUnits(source, {Plan("g_image", {3, 1, 7, 5})}, 8);
|
||||
EXPECT_TRUE(Contains(out, "layout(rgba32f, binding = 1) uniform writeonly highp image2D g_image[7];")) << out;
|
||||
// Offsets from the new base, in the application's element order - the order is what carries
|
||||
// the assignment, so it must NOT be sorted.
|
||||
EXPECT_TRUE(Contains(out, "const highp int " + UnitMap("g_image") + "[4] = int[4](2, 0, 6, 4);")) << out;
|
||||
// A literal subscript stays a constant expression: a const array indexed by one is one.
|
||||
EXPECT_TRUE(Contains(out, "imageStore(g_image[" + UnitMap("g_image") + "[0]], ivec2(0)")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(g_image[" + UnitMap("g_image") + "[3]], ivec2(0)")) << out;
|
||||
const String out = RemapImageArrayElementUnits(source, {Plan("g_image", {3, 1, 7, 5})});
|
||||
const Int units[4] = {3, 1, 7, 5};
|
||||
for (Int element = 0; element < 4; ++element) {
|
||||
EXPECT_TRUE(Contains(out, "binding = " + std::to_string(units[element]) +
|
||||
") uniform writeonly highp image2D " + Elem("g_image", element) + ";"))
|
||||
<< out;
|
||||
}
|
||||
// Only elements 0 and 3 are ever accessed; elements 1 and 2 are declared and unused, because
|
||||
// the reflection says the array has four of them.
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 0) + ", ivec2(0)")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + Elem("g_image", 3) + ", ivec2(0)")) << out;
|
||||
}
|
||||
|
||||
// Consecutive-from-element-zero is exactly what ESSL does unaided, so the emitted text of an
|
||||
@@ -588,33 +603,49 @@ void main()
|
||||
imageStore(g_image[1], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {2, 3, 4})}, 8), source);
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {2, 3, 4})}), source);
|
||||
// ...and so is a plan for an array this stage does not declare at all: the reflection is
|
||||
// program-wide, the pass runs per stage.
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("other_image", {0, 4})}, 8), source);
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("other_image", {0, 4})}), source);
|
||||
}
|
||||
|
||||
// Widening costs image-uniform budget, and a stage that cannot afford it must be told so rather
|
||||
// than silently addressing the wrong units - the exact silence this whole pass exists to end.
|
||||
TEST(RemapImageArrayElementUnitsTest, ASpanThatExceedsTheStageBudgetIsDeclinedAndNamed) {
|
||||
// A subscript that is not a literal names no element, so there is no scalar to rewrite it to.
|
||||
// It should never arrive - LegalizeResourceArrayIndexingForEssl runs first and ESSL rejects the
|
||||
// shape outright - but if one does, guessing an element would only change WHICH unit the access
|
||||
// reaches wrongly. Decline, loudly, and change nothing.
|
||||
TEST(RemapImageArrayElementUnitsTest, ANonLiteralSubscriptIsDeclinedAndNamed) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[4];
|
||||
void main()
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
imageStore(g_image[i], ivec2(gl_FragCoord.xy), vec4(1.0));
|
||||
}
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
const String out =
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, 4, &declined);
|
||||
EXPECT_EQ(out, String(kSsoImageArrayFS)) << out;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 2, 4, 6})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
||||
|
||||
// A budget of "cannot say" (the ES side reports no limit for this stage) must not be read as
|
||||
// a budget of zero - that would decline every array on a driver that simply does not answer.
|
||||
Vector<String> unknownBudget;
|
||||
const String repaired =
|
||||
RemapImageArrayElementUnits(kSsoImageArrayFS, {Plan("g_image", {0, 2, 4, 6})}, -1, &unknownBudget);
|
||||
EXPECT_TRUE(unknownBudget.empty());
|
||||
EXPECT_TRUE(Contains(repaired, "image2D g_image[7];")) << repaired;
|
||||
// A literal that is out of the reflected range is the same class of mismatch.
|
||||
const String outOfRange = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[5], ivec2(0), vec4(1.0));
|
||||
}
|
||||
)";
|
||||
Vector<String> outOfRangeDeclined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(outOfRange, {Plan("g_image", {0, 5})}, &outOfRangeDeclined),
|
||||
outOfRange);
|
||||
ASSERT_EQ(outOfRangeDeclined.size(), 1u);
|
||||
}
|
||||
|
||||
// A use the pass cannot see a subscript on has no element index to rewrite, so widening the
|
||||
// array underneath it would change which unit it reaches. Decline, loudly, and change nothing.
|
||||
// A use the pass cannot see a subscript on has no element index to rewrite, so splitting the
|
||||
// array out from under it would leave it naming a declaration that no longer exists. Decline,
|
||||
// loudly, and change nothing.
|
||||
TEST(RemapImageArrayElementUnitsTest, AUseWithoutASubscriptIsDeclined) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 0) uniform writeonly highp image2D g_image[2];
|
||||
@@ -626,7 +657,7 @@ void main()
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 5})}, 8, &declined), source);
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 5})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
EXPECT_TRUE(Contains(declined[0], "g_image")) << declined[0];
|
||||
}
|
||||
@@ -642,14 +673,15 @@ void main()
|
||||
}
|
||||
)";
|
||||
Vector<String> declined;
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4, 8})}, 16, &declined), source);
|
||||
EXPECT_EQ(RemapImageArrayElementUnits(source, {Plan("g_image", {0, 4, 8})}, &declined), source);
|
||||
ASSERT_EQ(declined.size(), 1u);
|
||||
}
|
||||
|
||||
// The two passes that run after it have to see the widened declaration and keep its binding: an
|
||||
// The two passes that run after it have to see the split declarations and keep their bindings: an
|
||||
// ES image unit cannot be assigned through the API, so the qualifier is the only mechanism there
|
||||
// is, and a read+write array is split into two declarations that must BOTH be the widened one.
|
||||
TEST(RemapImageArrayElementUnitsTest, TheWidenedArraySurvivesTheLaterImagePasses) {
|
||||
// is, and an element that is both read and written is split again into a pair that must BOTH
|
||||
// carry that element's own unit.
|
||||
TEST(RemapImageArrayElementUnitsTest, TheSplitElementsSurviveTheLaterImagePasses) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
||||
void main()
|
||||
@@ -657,19 +689,44 @@ void main()
|
||||
imageStore(g_image[1], ivec2(0), imageLoad(g_image[0], ivec2(0)));
|
||||
}
|
||||
)";
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 6})}, 8);
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 6})});
|
||||
out = SplitReadWriteImageUniforms(out, kStage);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Both halves, both widened to the three units 4..6, and both still bound.
|
||||
EXPECT_EQ(CountOf(out, "binding = 4"), 2u) << out;
|
||||
EXPECT_TRUE(Contains(out, "image2D " + StageAlias("g_image") + "[3];")) << out;
|
||||
EXPECT_TRUE(Contains(out, "image2D " + SplitWriteAlias("g_image") + "[3];")) << out;
|
||||
// The offset table is untouched by the rename - it is not an image uniform - and both halves
|
||||
// still route their subscripts through it.
|
||||
EXPECT_TRUE(Contains(out, "const highp int " + UnitMap("g_image") + "[2] = int[2](0, 2);")) << out;
|
||||
EXPECT_TRUE(Contains(out, SplitWriteAlias("g_image") + "[" + UnitMap("g_image") + "[1]]")) << out;
|
||||
EXPECT_TRUE(Contains(out, StageAlias("g_image") + "[" + UnitMap("g_image") + "[0]]")) << out;
|
||||
// Element 0 is only ever loaded and element 1 only ever stored, so neither is split into a
|
||||
// pair - but each keeps the unit the application gave it, which the array could not express.
|
||||
EXPECT_TRUE(Contains(out, "binding = 4")) << out;
|
||||
EXPECT_TRUE(Contains(out, "binding = 6")) << out;
|
||||
EXPECT_TRUE(Contains(out, "readonly highp image2D " + StageAlias(Elem("g_image", 0)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + StageAlias(Elem("g_image", 1)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "imageStore(" + StageAlias(Elem("g_image", 1)) + ", ivec2(0), imageLoad(" +
|
||||
StageAlias(Elem("g_image", 0)) + ", ivec2(0)))"))
|
||||
<< out;
|
||||
// Nothing is left addressing the array.
|
||||
EXPECT_FALSE(Contains(out, "g_image[")) << out;
|
||||
}
|
||||
|
||||
// The same element both read and written IS split into a coherent pair, and both halves have to
|
||||
// inherit that element's binding - the shape the widening used to have to carry on an array.
|
||||
TEST(RemapImageArrayElementUnitsTest, AnElementThatIsBothReadAndWrittenIsSplitWithItsOwnBinding) {
|
||||
const String source = R"(#version 320 es
|
||||
layout(rgba32f, binding = 4) uniform highp image2D g_image[2];
|
||||
void main()
|
||||
{
|
||||
imageStore(g_image[1], ivec2(0), imageLoad(g_image[1], ivec2(0)));
|
||||
imageStore(g_image[0], ivec2(0), vec4(0.0));
|
||||
}
|
||||
)";
|
||||
String out = RemapImageArrayElementUnits(source, {Plan("g_image", {4, 9})});
|
||||
out = SplitReadWriteImageUniforms(out, kStage);
|
||||
out = RemoveLayoutBinding(out);
|
||||
|
||||
// Element 1 sits on unit 9, and both halves of its split pair say so.
|
||||
EXPECT_EQ(CountOf(out, "binding = 9"), 2u) << out;
|
||||
EXPECT_TRUE(Contains(out, "readonly highp image2D " + StageAlias(Elem("g_image", 1)) + ";")) << out;
|
||||
EXPECT_TRUE(Contains(out, "writeonly highp image2D " + WriteAlias(StageAlias(Elem("g_image", 1))) + ";"))
|
||||
<< out;
|
||||
EXPECT_EQ(CountOf(out, "binding = 4"), 1u) << out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
@@ -12,7 +12,7 @@ add_executable(
|
||||
UniquifyIoBlockNamesTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
LegalizeStorageBlockArrayIndexTest.cpp
|
||||
LegalizeResourceArrayIndexTest.cpp
|
||||
FlattenAtomicCounterBlockTest.cpp
|
||||
WidenImageFormatsTest.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/LegalizeResourceArrayIndexTest.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/SpvcSession.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;
|
||||
}
|
||||
|
||||
// The image half of the same reference walker, and equally independent of the production
|
||||
// detection: true when some access chain rooted at an array-of-IMAGES variable carries a
|
||||
// non-constant FIRST index. A UniformConstant array whose element type is an OpTypeImage
|
||||
// with Sampled == 2 is what GLSL spells `image2D g_image[N]`; a sampler array is an
|
||||
// OpTypeSampledImage and is deliberately not matched here, because ESSL allows it a
|
||||
// dynamically-uniform index.
|
||||
bool HasDynamicImageArrayIndex(const Vector<Uint32>& spirv) {
|
||||
std::set<Uint32> storageImages; // OpTypeImage ids with Sampled == 2
|
||||
std::set<Uint32> constants; // OpConstant / OpConstantNull result ids
|
||||
std::set<Uint32> imageArrayTypes; // OpTypeArray ids whose element is such an image
|
||||
std::set<Uint32> imageArrayPointers; // OpTypePointer ids pointing at one of those arrays
|
||||
std::set<Uint32> imageArrayVars; // OpVariable ids of one of those pointer types
|
||||
|
||||
ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) {
|
||||
switch (opcode) {
|
||||
case spv::Op::OpTypeImage:
|
||||
// OpTypeImage <result> <sampled type> <dim> <depth> <arrayed> <ms> <sampled>
|
||||
if (wordCount >= 8u && words[7] == 2u) storageImages.insert(words[1]);
|
||||
break;
|
||||
case spv::Op::OpConstant:
|
||||
case spv::Op::OpConstantNull:
|
||||
if (wordCount >= 3u) constants.insert(words[2]);
|
||||
break;
|
||||
case spv::Op::OpTypeArray:
|
||||
if (wordCount >= 4u && storageImages.count(words[2]) != 0u) {
|
||||
imageArrayTypes.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpTypePointer:
|
||||
if (wordCount >= 4u && imageArrayTypes.count(words[3]) != 0u) {
|
||||
imageArrayPointers.insert(words[1]);
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpVariable:
|
||||
if (wordCount >= 4u && imageArrayPointers.count(words[1]) != 0u) {
|
||||
imageArrayVars.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;
|
||||
if (wordCount < 5u) return;
|
||||
if (imageArrayVars.count(words[3]) == 0u) return;
|
||||
if (constants.count(words[4]) != 0u) return;
|
||||
dynamic = true;
|
||||
});
|
||||
return dynamic;
|
||||
}
|
||||
|
||||
// The ESSL SPIRV-Cross prints for a module, or the error it refused with. This is where the
|
||||
// rule actually bites: the SPIR-V is legal Vulkan either way, and what a strict ES driver
|
||||
// reads is this text.
|
||||
struct EsslAttempt {
|
||||
Bool succeeded = false;
|
||||
String text;
|
||||
String error;
|
||||
};
|
||||
|
||||
EsslAttempt EmitEssl(const Vector<Uint32>& spirv) {
|
||||
using namespace MobileGL::MG_Util::ShaderTranspiler;
|
||||
EsslAttempt attempt;
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
spvc_compiler_options options;
|
||||
if (session.CreateOptions(&options) != SPVC_SUCCESS) return attempt;
|
||||
spvc_compiler_options_set_uint(options, SPVC_COMPILER_OPTION_GLSL_VERSION, 320);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_ES, SPVC_TRUE);
|
||||
spvc_compiler_options_set_bool(options, SPVC_COMPILER_OPTION_GLSL_VULKAN_SEMANTICS, SPVC_FALSE);
|
||||
if (session.SetOptions(options) != SPVC_SUCCESS) return attempt;
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
if (!essl) {
|
||||
attempt.error = essl.error().log;
|
||||
return attempt;
|
||||
}
|
||||
attempt.succeeded = true;
|
||||
attempt.text = *essl;
|
||||
return attempt;
|
||||
}
|
||||
|
||||
// `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];
|
||||
}
|
||||
)";
|
||||
|
||||
// The IMAGE half, and the case that has always been broken independently of any per-element
|
||||
// unit remapping: a plain CONSECUTIVE image array subscripted by a loop variable. This is
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-simple's own fragment shader shape, and a raw
|
||||
// GLES probe on Mesa 26.1.4 at ES 3.2 refuses the ESSL it produces with "image arrays indexed
|
||||
// with non-constant expressions are forbidden in GLSL ES". Foldable: after unrolling every
|
||||
// subscript is a literal.
|
||||
constexpr const char* kLoopIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly image2D g_image[4];
|
||||
void main() {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
imageStore(g_image[i], ivec2(0), vec4(1.0));
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// A uniform-sourced image index: nothing can fold it, so the switch/select lowering is what
|
||||
// has to carry it. Both directions in one shader, as the block-array fixture does.
|
||||
constexpr const char* kUniformIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform image2D g_image[4];
|
||||
layout(std430, binding = 8) buffer Out { vec4 value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
imageStore(g_image[g_index], ivec2(0), vec4(7.0));
|
||||
g_out.value = imageLoad(g_image[g_index], ivec2(1));
|
||||
}
|
||||
)";
|
||||
|
||||
// An image array indexed only with literals is already legal ES.
|
||||
constexpr const char* kConstantIndexedImageArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform writeonly image2D g_image[4];
|
||||
void main() {
|
||||
imageStore(g_image[1], ivec2(0), vec4(1.0));
|
||||
imageStore(g_image[3], ivec2(0), vec4(2.0));
|
||||
}
|
||||
)";
|
||||
|
||||
// The positive control for the scope decision: ESSL 3.20 4.1.7 allows a SAMPLER array a
|
||||
// dynamically-uniform index, and the same raw GLES probe confirms it - both a loop-variable
|
||||
// subscript and a const-table lookup compile and link. Nothing here may be rewritten.
|
||||
constexpr const char* kUniformIndexedSamplerArray = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
uniform sampler2D g_tex[4];
|
||||
layout(std430, binding = 8) buffer Out { vec4 value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = texture(g_tex[g_index], vec2(0.5));
|
||||
}
|
||||
)";
|
||||
|
||||
// An imageAtomic* reaches the array through OpImageTexelPointer, and running one per element
|
||||
// would perform every other element's atomic as well. The pass has to decline rather than
|
||||
// lower this.
|
||||
constexpr const char* kUniformIndexedImageAtomic = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(r32ui, binding = 0) uniform uimage2D g_image[4];
|
||||
layout(std430, binding = 8) buffer Out { uint value; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.value = imageAtomicAdd(g_image[g_index], ivec2(0), 1u);
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, FoldsALoopIndexedBlockArray) {
|
||||
const Vector<Uint32> input = CompileCompute(kLoopIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(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(LegalizeResourceArrayIndexPass, 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::LegalizeResourceArrayIndexingForEssl(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(LegalizeResourceArrayIndexPass, LeavesADynamicMemberOfOneBlockByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kArrayMemberInsideOneBlock);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesAConstantIndexedBlockArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kConstantIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicBlockArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, IsIdempotent) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedBlockArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, once, true));
|
||||
ASSERT_FALSE(once.empty());
|
||||
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
|
||||
// The case no test covered before, and the one that has nothing to do with per-element unit
|
||||
// remapping: an ordinary consecutive image array written from a loop. Every emitted subscript has
|
||||
// to end up a literal, or the ES driver drops the stage and every draw with it.
|
||||
TEST(LegalizeResourceArrayIndexPass, FoldsALoopIndexedImageArray) {
|
||||
const Vector<Uint32> input = CompileCompute(kLoopIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
EXPECT_TRUE(Validates(output));
|
||||
|
||||
// ...and in the text the driver actually reads. Before: `g_image[i]`; after: four literals.
|
||||
const EsslAttempt before = EmitEssl(input);
|
||||
ASSERT_TRUE(before.succeeded) << before.error;
|
||||
EXPECT_NE(before.text.find("g_image[i]"), String::npos) << before.text;
|
||||
|
||||
const EsslAttempt after = EmitEssl(output);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
EXPECT_EQ(after.text.find("g_image[i]"), String::npos) << after.text;
|
||||
for (int element = 0; element < 4; ++element) {
|
||||
EXPECT_NE(after.text.find("g_image[" + std::to_string(element) + "]"), String::npos) << after.text;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LowersAUniformIndexedImageWriteToASwitchAndAReadToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSwitch), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
// One switch for the imageStore, and one select per element past the first for the imageLoad.
|
||||
// The selection is on the loaded TEXEL, never on the image object - an opaque type cannot be
|
||||
// selected at all - so there is one OpImageRead per element behind those selects.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 1u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 3u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpImageRead), 4u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpImageWrite), 4u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
|
||||
const EsslAttempt after = EmitEssl(output);
|
||||
ASSERT_TRUE(after.succeeded) << after.error;
|
||||
for (int element = 0; element < 4; ++element) {
|
||||
EXPECT_NE(after.text.find("g_image[" + std::to_string(element) + "]"), String::npos) << after.text;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesAConstantIndexedImageArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kConstantIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
// The scope decision, asserted rather than assumed: a sampler array indexed by a uniform is legal
|
||||
// ESSL, so the module must come back untouched - not merely legal, byte for byte the same.
|
||||
TEST(LegalizeResourceArrayIndexPass, LeavesADynamicallyIndexedSamplerArrayByteIdentical) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedSamplerArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
// An imageAtomic* is the shape the lowering must refuse: its per-element rebuild would run every
|
||||
// other element's read-modify-write. Declining leaves the illegal subscript in place - which is
|
||||
// what the latched warning in LegalizeResourceArrayIndexingForEssl is for - but a half-transform
|
||||
// would corrupt four images instead of losing one stage.
|
||||
TEST(LegalizeResourceArrayIndexPass, DeclinesAUniformIndexedImageAtomic) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageAtomic);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(output));
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 0u);
|
||||
EXPECT_TRUE(Validates(output));
|
||||
}
|
||||
|
||||
TEST(LegalizeResourceArrayIndexPass, IsIdempotentOnImageArrays) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageArray);
|
||||
ASSERT_FALSE(input.empty());
|
||||
|
||||
Vector<Uint32> once;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, once, true));
|
||||
ASSERT_FALSE(once.empty());
|
||||
|
||||
Vector<Uint32> twice;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(once, twice, true));
|
||||
EXPECT_EQ(twice, once);
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user