mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 21:58:31 +09:00
[Merge] (ShaderTranspiler): land the single-implementation subroutine lowering and the imageSize select ladder
This commit is contained in:
@@ -4678,3 +4678,143 @@ void main() { clearInstance.b = 0u; }
|
||||
EXPECT_EQ(unqualified.count("Clear"), 1u)
|
||||
<< "the unambiguous block alongside it is still recognised";
|
||||
}
|
||||
|
||||
// KHR-GL43.shader_image_size.advanced-nonMS-* is nothing but its passing twin basic-nonMS-* plus a
|
||||
// GLSL subroutine, and glslang refuses the keyword outright when the target is SPIR-V ("subroutine
|
||||
// : not allowed when generating SPIR-V"), so every stage of those shaders failed to compile. The
|
||||
// lowering turns a subroutine uniform with exactly ONE compatible subroutine - the case where GL
|
||||
// 4.3 core 7.9 makes a direct call indistinguishable from a dispatch, because every legal value of
|
||||
// the uniform selects that one function - into a forwarding call.
|
||||
TEST_F(ProgramUtilTest, PreprocessLowersSingleImplementationSubroutineToAForwardingCall) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = R"(#version 430 core
|
||||
layout(binding = 0, rgba32i) writeonly uniform iimage2D g_result;
|
||||
subroutine void FuncType(int coord);
|
||||
subroutine uniform FuncType g_func;
|
||||
void main() {
|
||||
int coord = gl_VertexID;
|
||||
g_func(coord);
|
||||
}
|
||||
subroutine(FuncType) void Func0(int coord) {
|
||||
imageStore(g_result, ivec2(coord, 0), ivec4(imageSize(g_result), 0, 0));
|
||||
}
|
||||
)";
|
||||
const SizeT mainLine = std::count(source.begin(), source.begin() + source.find("void main"), '\n');
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Vertex, source);
|
||||
|
||||
EXPECT_EQ(source.find("subroutine"), String::npos) << "the keyword glslang refuses must be gone";
|
||||
EXPECT_NE(source.find("void g_func(int mgl_sr_arg0);"), String::npos)
|
||||
<< "the subroutine uniform becomes a prototype under its own name, so call sites stand";
|
||||
EXPECT_NE(source.find("g_func(coord);"), String::npos) << "the call site is untouched";
|
||||
EXPECT_NE(source.find("void Func0(int coord)"), String::npos)
|
||||
<< "the compatible subroutine keeps its body and only sheds the qualifier";
|
||||
EXPECT_NE(source.find("Func0(mgl_sr_arg0);"), String::npos) << "the forwarding body";
|
||||
// The forwarding body has to come after every definition it names: the CTS shaders define
|
||||
// their subroutine BELOW the function that calls through the uniform.
|
||||
EXPECT_LT(source.find("void Func0(int coord)"), source.find("Func0(mgl_sr_arg0);"));
|
||||
// Blanking preserves newlines, and the prototype is single-line, so glslang's diagnostics still
|
||||
// point at the line the application wrote.
|
||||
EXPECT_EQ(std::count(source.begin(), source.begin() + source.find("void main"), '\n'), mainLine)
|
||||
<< "the rewrite must not move a single line";
|
||||
|
||||
ShaderAttrib attrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||
}
|
||||
}
|
||||
|
||||
// The forwarding function is rebuilt from the subroutine TYPE declaration, so it has to carry the
|
||||
// parameter qualifiers and array shapes across (an parameter that arrives by value writes
|
||||
// nothing back) and has to return the forwarded value for a non-void subroutine.
|
||||
TEST_F(ProgramUtilTest, PreprocessSubroutineForwardingKeepsParameterQualifiersAndReturnsValues) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = R"(#version 430 core
|
||||
subroutine float Blend(const int k, out vec4 rgba, float weights[2]);
|
||||
subroutine uniform Blend g_blend;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 rgba;
|
||||
float w[2] = float[2](0.25, 0.75);
|
||||
fragColor = rgba * g_blend(1, rgba, w);
|
||||
}
|
||||
subroutine(Blend) float Mix(const int k, out vec4 rgba, float weights[2]) {
|
||||
rgba = vec4(weights[0], weights[1], float(k), 1.0);
|
||||
return weights[0];
|
||||
}
|
||||
)";
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
|
||||
EXPECT_NE(source.find("float g_blend(const int mgl_sr_arg0, out vec4 mgl_sr_arg1, float mgl_sr_arg2 [ 2 ]);"),
|
||||
String::npos)
|
||||
<< "qualifiers and the array declarator have to survive, under generated names";
|
||||
EXPECT_NE(source.find("return Mix(mgl_sr_arg0, mgl_sr_arg1, mgl_sr_arg2);"), String::npos)
|
||||
<< "a non-void subroutine has to have its value forwarded back";
|
||||
|
||||
ShaderAttrib attrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source};
|
||||
auto res = ShaderCompiler::CompileShader(attrib);
|
||||
if (!res) {
|
||||
FAIL() << "errc: " << res.error().errc << "\nlog: " << res.error().log << "\nsource:\n" << source;
|
||||
}
|
||||
}
|
||||
|
||||
// Two compatible subroutines is genuine dynamic selection, which MobileGL does not implement:
|
||||
// glUniformSubroutinesuiv is still a stub and nothing reflects the subroutine interfaces. Pinning
|
||||
// such a shader to one of the alternatives would render silently wrong, so the whole rewrite is
|
||||
// abandoned and the source is left exactly as it arrived.
|
||||
TEST_F(ProgramUtilTest, PreprocessLeavesMultiImplementationSubroutinesAlone) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String source = R"(#version 430 core
|
||||
subroutine void FuncType(int coord);
|
||||
subroutine uniform FuncType g_func;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
g_func(1);
|
||||
fragColor = vec4(1.0);
|
||||
}
|
||||
subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
|
||||
subroutine(FuncType) void Func1(int coord) { fragColor = vec4(float(coord) * 2.0); }
|
||||
)";
|
||||
const String before = source;
|
||||
|
||||
PreprocessShaderSource(ShaderStage::Fragment, source);
|
||||
|
||||
EXPECT_EQ(source, before) << "an unimplementable dispatch must not be quietly pinned to one arm";
|
||||
}
|
||||
|
||||
// An ARRAY of subroutine uniforms indexes the dispatch at the call site ("g_func[i](x)"), which is
|
||||
// the same dynamic selection - and a subroutine declared inside a #if arm cannot be reasoned about
|
||||
// at all, because the forwarding bodies this appends are unconditional.
|
||||
TEST_F(ProgramUtilTest, PreprocessLeavesArrayAndConditionalSubroutinesAlone) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
|
||||
String arrayed = R"(#version 430 core
|
||||
subroutine void FuncType(int coord);
|
||||
subroutine uniform FuncType g_func[2];
|
||||
out vec4 fragColor;
|
||||
void main() { g_func[0](1); fragColor = vec4(1.0); }
|
||||
subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
|
||||
)";
|
||||
const String arrayedBefore = arrayed;
|
||||
PreprocessShaderSource(ShaderStage::Fragment, arrayed);
|
||||
EXPECT_EQ(arrayed, arrayedBefore) << "an arrayed subroutine uniform is a dispatch, not a call";
|
||||
|
||||
String conditional = R"(#version 430 core
|
||||
out vec4 fragColor;
|
||||
#ifdef USE_SUBROUTINE
|
||||
subroutine void FuncType(int coord);
|
||||
subroutine uniform FuncType g_func;
|
||||
#endif
|
||||
void main() { fragColor = vec4(1.0); }
|
||||
subroutine(FuncType) void Func0(int coord) { fragColor = vec4(float(coord)); }
|
||||
)";
|
||||
const String conditionalBefore = conditional;
|
||||
PreprocessShaderSource(ShaderStage::Fragment, conditional);
|
||||
EXPECT_EQ(conditional, conditionalBefore)
|
||||
<< "an inactive #if arm must not have an unconditional forwarding body appended for it";
|
||||
}
|
||||
|
||||
@@ -342,6 +342,21 @@ void main() {
|
||||
// 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.
|
||||
// imageSize() on a dynamically indexed image array. The query carries the image in the same
|
||||
// leading operand position as an imageLoad and answers with an int vector, so the select
|
||||
// ladder spells it exactly - and unlike a read it touches no memory at all, so evaluating it
|
||||
// for every element cannot even return undefined data.
|
||||
constexpr const char* kUniformIndexedImageSizeQuery = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(rgba32f, binding = 0) uniform image2D g_image[4];
|
||||
layout(rgba32f, binding = 4) uniform image2DArray g_layered[2];
|
||||
layout(std430, binding = 8) buffer Out { ivec2 size; int layers; } g_out;
|
||||
uniform int g_index;
|
||||
void main() {
|
||||
g_out.size = imageSize(g_image[g_index]);
|
||||
g_out.layers = imageSize(g_layered[g_index]).z;
|
||||
}
|
||||
)";
|
||||
constexpr const char* kUniformIndexedImageAtomic = R"(#version 450 core
|
||||
layout(local_size_x = 1) in;
|
||||
layout(r32ui, binding = 0) uniform uimage2D g_image[4];
|
||||
@@ -514,6 +529,36 @@ TEST(LegalizeResourceArrayIndexPass, LeavesADynamicallyIndexedSamplerArrayByteId
|
||||
EXPECT_EQ(output, input);
|
||||
}
|
||||
|
||||
// imageSize() on a dynamically indexed image array used to lose the whole stage: the consumer
|
||||
// whitelist accepted only OpImageRead/OpImageWrite, so the chain was declined and the illegal
|
||||
// subscript reached the ES compiler intact. It is the same select ladder as a read - the query
|
||||
// takes the image in in-operand 0 and produces an int vector - and it reads no memory, so the
|
||||
// elements the shader did not ask for cost nothing but the instruction.
|
||||
TEST(LegalizeResourceArrayIndexPass, LowersAUniformIndexedImageSizeQueryToSelects) {
|
||||
const Vector<Uint32> input = CompileCompute(kUniformIndexedImageSizeQuery);
|
||||
ASSERT_FALSE(input.empty());
|
||||
EXPECT_TRUE(HasDynamicImageArrayIndex(input));
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpImageQuerySize), 2u);
|
||||
EXPECT_EQ(CountOpcode(input, spv::Op::OpSelect), 0u);
|
||||
|
||||
Vector<Uint32> output;
|
||||
ASSERT_TRUE(ShaderCompiler::LegalizeResourceArrayIndexingForEssl(input, output, true));
|
||||
ASSERT_FALSE(output.empty());
|
||||
EXPECT_FALSE(HasDynamicImageArrayIndex(output));
|
||||
// Four elements for g_image and two for g_layered, one query apiece, and one select per
|
||||
// element past the first of each ladder.
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpImageQuerySize), 6u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSelect), 4u);
|
||||
EXPECT_EQ(CountOpcode(output, spv::Op::OpSwitch), 0u) << "a query produces a value, so no control flow";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user