mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (CMakeLists.txt, MG_Test/ShaderTranspiler, MG_Util/ShaderTranspiler): strip unused compatibility members from per-vertex blocks
- glslang gives them no BuiltIn, so Magma declined every compatibility program that wrote gl_Position, including the Iris block outline
This commit is contained in:
@@ -400,6 +400,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||||
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripCompatPerVertexMembersPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
|
||||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ add_executable(
|
|||||||
FlattenXfbInterfaceBlocksTest.cpp
|
FlattenXfbInterfaceBlocksTest.cpp
|
||||||
UniquifyIoBlockNamesTest.cpp
|
UniquifyIoBlockNamesTest.cpp
|
||||||
StripIoBlockLocationsTest.cpp
|
StripIoBlockLocationsTest.cpp
|
||||||
|
StripCompatPerVertexMembersTest.cpp
|
||||||
LowerViewportIndexTest.cpp
|
LowerViewportIndexTest.cpp
|
||||||
DeferGeometryEmitsTest.cpp
|
DeferGeometryEmitsTest.cpp
|
||||||
ClampMultisampleFetchTest.cpp
|
ClampMultisampleFetchTest.cpp
|
||||||
|
|||||||
@@ -0,0 +1,369 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/StripCompatPerVertexMembersTest.cpp
|
||||||
|
// Copyright (c) 2025-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
|
||||||
|
|
||||||
|
// A compatibility-profile stage that only uses core features must reach the backends as the
|
||||||
|
// same Vulkan-valid SPIR-V as its core twin. glslang declares the legacy gl_PerVertex members
|
||||||
|
// (gl_FrontColor, gl_TexCoord, ...) without a BuiltIn, which leaves a module that writes only
|
||||||
|
// gl_Position invalid for Vulkan until the sanitize chain removes them.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Includes.h"
|
||||||
|
#include "Init.h"
|
||||||
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/SpirvPasses/StripCompatPerVertexMembersPass.h>
|
||||||
|
|
||||||
|
#include <spirv-tools/libspirv.hpp>
|
||||||
|
#include <spirv-tools/optimizer.hpp>
|
||||||
|
|
||||||
|
using namespace MobileGL;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||||
|
using MobileGL::MG_Util::ShaderTranspiler::StripCompatPerVertexMembersPass;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
ShaderStage StageOf(GLenum type) {
|
||||||
|
switch (type) {
|
||||||
|
case GL_VERTEX_SHADER:
|
||||||
|
return ShaderStage::Vertex;
|
||||||
|
case GL_TESS_CONTROL_SHADER:
|
||||||
|
return ShaderStage::TessControl;
|
||||||
|
case GL_TESS_EVALUATION_SHADER:
|
||||||
|
return ShaderStage::TessEval;
|
||||||
|
case GL_GEOMETRY_SHADER:
|
||||||
|
return ShaderStage::Geometry;
|
||||||
|
default:
|
||||||
|
return ShaderStage::Fragment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preprocesses, compiles and links a whole program the way the link task does, and returns
|
||||||
|
// one module per stage: straight out of GlslangToSpv, or after the shared sanitize chain.
|
||||||
|
Vector<Vector<Uint32>> CompileProgram(const Vector<Pair<GLenum, String>>& stages, Bool sanitize = true) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
const CompileEnv env{};
|
||||||
|
Vector<String> sources;
|
||||||
|
sources.reserve(stages.size());
|
||||||
|
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||||
|
Vector<GLenum> types;
|
||||||
|
for (const auto& [type, text] : stages) {
|
||||||
|
String& source = sources.emplace_back(text);
|
||||||
|
PreprocessShaderSource(StageOf(type), source, env);
|
||||||
|
ShaderAttrib attrib{.shaderType = type, .sourceStr = source};
|
||||||
|
auto shader = ShaderCompiler::CompileShader(attrib);
|
||||||
|
EXPECT_TRUE(shader) << (shader ? String{} : shader.error().log);
|
||||||
|
if (!shader) return {};
|
||||||
|
shaders.push_back(shader.value());
|
||||||
|
types.push_back(type);
|
||||||
|
}
|
||||||
|
ProgramAttrib programAttrib{.shaders = shaders};
|
||||||
|
auto program = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
EXPECT_TRUE(program) << (program ? String{} : program.error().log);
|
||||||
|
if (!program) return {};
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *program.value()};
|
||||||
|
auto binary = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
EXPECT_TRUE(binary) << (binary ? String{} : binary.error().log);
|
||||||
|
if (!binary) return {};
|
||||||
|
Vector<Vector<Uint32>> modules = Move(binary.value());
|
||||||
|
if (sanitize) {
|
||||||
|
for (auto& module : modules) {
|
||||||
|
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
String Disassemble(const Vector<Uint32>& spirv) {
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String text;
|
||||||
|
EXPECT_TRUE(tools.Disassemble(spirv, &text));
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ValidationError(const Vector<Uint32>& spirv) {
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String error;
|
||||||
|
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
|
||||||
|
error += message != nullptr ? message : "";
|
||||||
|
});
|
||||||
|
return tools.Validate(spirv) ? String{} : (error.empty() ? String{"invalid"} : error);
|
||||||
|
}
|
||||||
|
|
||||||
|
String Transpile(const Vector<Uint32>& spirv) {
|
||||||
|
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||||
|
auto essl = ShaderCompiler::DecompileShader(session);
|
||||||
|
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||||
|
return essl ? essl.value() : String{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the pass alone, as the sanitize chain would, on a hand-assembled module.
|
||||||
|
Vector<Uint32> RunPass(const Vector<Uint32>& input) {
|
||||||
|
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
optimizer.RegisterPass(StripCompatPerVertexMembersPass::CreateStripCompatPerVertexMembersPass());
|
||||||
|
spvtools::OptimizerOptions options;
|
||||||
|
options.set_run_validator(false);
|
||||||
|
Vector<Uint32> output;
|
||||||
|
EXPECT_TRUE(optimizer.Run(input.data(), input.size(), &output, options));
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every compatibility source is compiled as `#version 460 compatibility`
|
||||||
|
// (GetNormalizedVersionDirective), so its core twin is the 460 core source.
|
||||||
|
String Compat(const char* body, const char* version = "410") {
|
||||||
|
return String("#version ") + version + " compatibility\n" + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
String Core(const char* body) {
|
||||||
|
return String("#version 460 core\n") + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disassembly with ids renumbered in order of appearance, so two modules that differ only in
|
||||||
|
// the ids glslang happened to allocate compare equal.
|
||||||
|
String Canonical(const Vector<Uint32>& spirv) {
|
||||||
|
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||||
|
optimizer.RegisterPass(spvtools::CreateCompactIdsPass());
|
||||||
|
Vector<Uint32> compacted;
|
||||||
|
EXPECT_TRUE(optimizer.Run(spirv.data(), spirv.size(), &compacted));
|
||||||
|
return Disassemble(compacted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compiles both programs and expects every compatibility module to validate and to equal the
|
||||||
|
// core twin's.
|
||||||
|
void ExpectSameAsCoreTwin(const Vector<Pair<GLenum, String>>& compatStages,
|
||||||
|
const Vector<Pair<GLenum, String>>& coreStages) {
|
||||||
|
const auto compat = CompileProgram(compatStages);
|
||||||
|
const auto core = CompileProgram(coreStages);
|
||||||
|
ASSERT_EQ(compat.size(), compatStages.size());
|
||||||
|
ASSERT_EQ(core.size(), coreStages.size());
|
||||||
|
for (SizeT i = 0; i < compat.size(); ++i) {
|
||||||
|
EXPECT_EQ(ValidationError(compat[i]), "") << Disassemble(compat[i]);
|
||||||
|
EXPECT_EQ(Canonical(compat[i]), Canonical(core[i])) << "stage " << i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* kVertexBody = R"(in vec2 pos;
|
||||||
|
out vec4 c;
|
||||||
|
void main() {
|
||||||
|
c = vec4(0.25, 0.5, 0.75, 1.0);
|
||||||
|
gl_Position = vec4(pos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kFragmentBody = R"(in vec4 c;
|
||||||
|
out vec4 o;
|
||||||
|
void main() { o = c; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Writes the core members after the legacy ones, so their indices have to move.
|
||||||
|
const char* kClipCullVertexBody = R"(in vec2 pos;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(pos, 0.0, 1.0);
|
||||||
|
gl_PointSize = 2.0;
|
||||||
|
gl_ClipDistance[0] = pos.x;
|
||||||
|
gl_CullDistance[0] = pos.y;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kGeometryBody = R"(layout(triangles) in;
|
||||||
|
layout(triangle_strip, max_vertices = 3) out;
|
||||||
|
in vec4 c[];
|
||||||
|
out vec4 gc;
|
||||||
|
void main() {
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
gc = c[i];
|
||||||
|
gl_Position = gl_in[i].gl_Position;
|
||||||
|
EmitVertex();
|
||||||
|
}
|
||||||
|
EndPrimitive();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kGeometryFragmentBody = R"(in vec4 gc;
|
||||||
|
out vec4 o;
|
||||||
|
void main() { o = gc; }
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kTessControlBody = R"(layout(vertices = 3) out;
|
||||||
|
void main() {
|
||||||
|
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
|
||||||
|
gl_TessLevelOuter[0] = 1.0;
|
||||||
|
gl_TessLevelOuter[1] = 1.0;
|
||||||
|
gl_TessLevelOuter[2] = 1.0;
|
||||||
|
gl_TessLevelInner[0] = 1.0;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kTessEvalBody = R"(layout(triangles) in;
|
||||||
|
void main() {
|
||||||
|
gl_Position = gl_TessCoord.x * gl_in[0].gl_Position + gl_TessCoord.y * gl_in[1].gl_Position +
|
||||||
|
gl_TessCoord.z * gl_in[2].gl_Position;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kPositionOnlyVertexBody = R"(in vec2 pos;
|
||||||
|
void main() { gl_Position = vec4(pos, 0.0, 1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* kWhiteFragmentBody = R"(out vec4 o;
|
||||||
|
void main() { o = vec4(1.0); }
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A compatibility-only built-in in use: there is no core shape to give this stage.
|
||||||
|
const char* kFrontColorVertexBody = R"(in vec2 pos;
|
||||||
|
void main() {
|
||||||
|
gl_FrontColor = vec4(1.0);
|
||||||
|
gl_Position = vec4(pos, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// Reads a legacy member only into dead code, which the sanitize chain removes first.
|
||||||
|
const char* kDeadLegacyReadGeometryBody = R"(layout(points) in;
|
||||||
|
layout(points, max_vertices = 1) out;
|
||||||
|
void main() {
|
||||||
|
vec4 unused = gl_in[0].gl_FrontColor;
|
||||||
|
gl_Position = gl_in[0].gl_Position;
|
||||||
|
EmitVertex();
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
// A mixed built-in block reached through a whole-block load, which the pass cannot follow.
|
||||||
|
const char* kWholeBlockLoadAsm = R"(
|
||||||
|
OpCapability Geometry
|
||||||
|
OpMemoryModel Logical GLSL450
|
||||||
|
OpEntryPoint Geometry %main "main" %gl_in %out_block
|
||||||
|
OpExecutionMode %main InputPoints
|
||||||
|
OpExecutionMode %main Invocations 1
|
||||||
|
OpExecutionMode %main OutputPoints
|
||||||
|
OpExecutionMode %main OutputVertices 1
|
||||||
|
OpMemberDecorate %gl_PerVertex 0 BuiltIn Position
|
||||||
|
OpDecorate %gl_PerVertex Block
|
||||||
|
%void = OpTypeVoid
|
||||||
|
%fn_ty = OpTypeFunction %void
|
||||||
|
%float = OpTypeFloat 32
|
||||||
|
%v4float = OpTypeVector %float 4
|
||||||
|
%gl_PerVertex = OpTypeStruct %v4float %v4float
|
||||||
|
%uint = OpTypeInt 32 0
|
||||||
|
%uint_1 = OpConstant %uint 1
|
||||||
|
%arr = OpTypeArray %gl_PerVertex %uint_1
|
||||||
|
%ptr_in_arr = OpTypePointer Input %arr
|
||||||
|
%gl_in = OpVariable %ptr_in_arr Input
|
||||||
|
%ptr_out_s = OpTypePointer Output %gl_PerVertex
|
||||||
|
%out_block = OpVariable %ptr_out_s Output
|
||||||
|
%ptr_in_s = OpTypePointer Input %gl_PerVertex
|
||||||
|
%int = OpTypeInt 32 1
|
||||||
|
%int_0 = OpConstant %int 0
|
||||||
|
%main = OpFunction %void None %fn_ty
|
||||||
|
%entry = OpLabel
|
||||||
|
%p = OpAccessChain %ptr_in_s %gl_in %int_0
|
||||||
|
%v = OpLoad %gl_PerVertex %p
|
||||||
|
OpStore %out_block %v
|
||||||
|
OpEmitVertex
|
||||||
|
OpReturn
|
||||||
|
OpFunctionEnd
|
||||||
|
)";
|
||||||
|
|
||||||
|
// The OpTypeStruct line and the member decorations of every per-vertex block, in order.
|
||||||
|
String PerVertexShape(const String& disassembly) {
|
||||||
|
String shape;
|
||||||
|
SizeT lineStart = 0;
|
||||||
|
while (lineStart < disassembly.size()) {
|
||||||
|
SizeT lineEnd = disassembly.find('\n', lineStart);
|
||||||
|
if (lineEnd == String::npos) lineEnd = disassembly.size();
|
||||||
|
const String line = disassembly.substr(lineStart, lineEnd - lineStart);
|
||||||
|
if (line.find("%gl_PerVertex") != String::npos &&
|
||||||
|
(line.find("OpTypeStruct") != String::npos || line.find("OpMemberDecorate") != String::npos)) {
|
||||||
|
shape += line + "\n";
|
||||||
|
}
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
}
|
||||||
|
return shape;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class StripCompatPerVertexMembersTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override { MobileGL::Initialize(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(StripCompatPerVertexMembersTest, VertexStageMatchesItsCoreTwin) {
|
||||||
|
const auto raw =
|
||||||
|
CompileProgram({{GL_VERTEX_SHADER, Compat(kVertexBody)}, {GL_FRAGMENT_SHADER, Compat(kFragmentBody)}}, false);
|
||||||
|
ASSERT_EQ(raw.size(), 2u);
|
||||||
|
// The defect, pinned: glslang's own module mixes legacy and built-in members.
|
||||||
|
EXPECT_NE(ValidationError(raw[0]).find("BuiltIn"), String::npos) << Disassemble(raw[0]);
|
||||||
|
|
||||||
|
for (const char* version : {"410", "150"}) {
|
||||||
|
SCOPED_TRACE(version);
|
||||||
|
ExpectSameAsCoreTwin(
|
||||||
|
{{GL_VERTEX_SHADER, Compat(kVertexBody, version)}, {GL_FRAGMENT_SHADER, Compat(kFragmentBody, version)}},
|
||||||
|
{{GL_VERTEX_SHADER, Core(kVertexBody)}, {GL_FRAGMENT_SHADER, Core(kFragmentBody)}});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(StripCompatPerVertexMembersTest, CoreMembersAfterTheLegacyOnesAreRenumbered) {
|
||||||
|
ExpectSameAsCoreTwin(
|
||||||
|
{{GL_VERTEX_SHADER, Compat(kClipCullVertexBody)}, {GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}},
|
||||||
|
{{GL_VERTEX_SHADER, Core(kClipCullVertexBody)}, {GL_FRAGMENT_SHADER, Core(kWhiteFragmentBody)}});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(StripCompatPerVertexMembersTest, GeometryStageInputAndOutputBlocks) {
|
||||||
|
ExpectSameAsCoreTwin({{GL_VERTEX_SHADER, Compat(kVertexBody)},
|
||||||
|
{GL_GEOMETRY_SHADER, Compat(kGeometryBody)},
|
||||||
|
{GL_FRAGMENT_SHADER, Compat(kGeometryFragmentBody)}},
|
||||||
|
{{GL_VERTEX_SHADER, Core(kVertexBody)},
|
||||||
|
{GL_GEOMETRY_SHADER, Core(kGeometryBody)},
|
||||||
|
{GL_FRAGMENT_SHADER, Core(kGeometryFragmentBody)}});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(StripCompatPerVertexMembersTest, TessellationStagesInputAndOutputBlocks) {
|
||||||
|
ExpectSameAsCoreTwin({{GL_VERTEX_SHADER, Compat(kPositionOnlyVertexBody)},
|
||||||
|
{GL_TESS_CONTROL_SHADER, Compat(kTessControlBody)},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, Compat(kTessEvalBody)},
|
||||||
|
{GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}},
|
||||||
|
{{GL_VERTEX_SHADER, Core(kPositionOnlyVertexBody)},
|
||||||
|
{GL_TESS_CONTROL_SHADER, Core(kTessControlBody)},
|
||||||
|
{GL_TESS_EVALUATION_SHADER, Core(kTessEvalBody)},
|
||||||
|
{GL_FRAGMENT_SHADER, Core(kWhiteFragmentBody)}});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(StripCompatPerVertexMembersTest, ALegacyReadThatDiesInTheChainDoesNotPinTheBlock) {
|
||||||
|
const auto compat = CompileProgram({{GL_VERTEX_SHADER, Compat(kPositionOnlyVertexBody)},
|
||||||
|
{GL_GEOMETRY_SHADER, Compat(kDeadLegacyReadGeometryBody)},
|
||||||
|
{GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}});
|
||||||
|
ASSERT_EQ(compat.size(), 3u);
|
||||||
|
EXPECT_EQ(ValidationError(compat[1]), "") << Disassemble(compat[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(StripCompatPerVertexMembersTest, AnAccessedLegacyMemberKeepsTheBlock) {
|
||||||
|
const auto raw = CompileProgram(
|
||||||
|
{{GL_VERTEX_SHADER, Compat(kFrontColorVertexBody)}, {GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}}, false);
|
||||||
|
const auto compat = CompileProgram(
|
||||||
|
{{GL_VERTEX_SHADER, Compat(kFrontColorVertexBody)}, {GL_FRAGMENT_SHADER, Compat(kWhiteFragmentBody)}});
|
||||||
|
ASSERT_EQ(raw.size(), 2u);
|
||||||
|
ASSERT_EQ(compat.size(), 2u);
|
||||||
|
const String shape = PerVertexShape(Disassemble(compat[0]));
|
||||||
|
EXPECT_EQ(shape, PerVertexShape(Disassemble(raw[0])));
|
||||||
|
EXPECT_NE(shape.find("OpTypeStruct %v4float %float %_arr_float_uint_1 %v4float %v4float"), String::npos) << shape;
|
||||||
|
// The ES hop still takes the module as it did before.
|
||||||
|
EXPECT_NE(Transpile(compat[0]).find("gl_Position"), String::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(StripCompatPerVertexMembersTest, AWholeBlockLoadLeavesTheModuleUntouched) {
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
Vector<Uint32> module;
|
||||||
|
ASSERT_TRUE(tools.Assemble(kWholeBlockLoadAsm, &module));
|
||||||
|
EXPECT_EQ(RunPass(module), module);
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
#include "SpirvPasses/EliminateFloatEqualsZeroPass.h"
|
||||||
#include "SpirvPasses/FlattenInterfaceStructPass.h"
|
#include "SpirvPasses/FlattenInterfaceStructPass.h"
|
||||||
|
#include "SpirvPasses/StripCompatPerVertexMembersPass.h"
|
||||||
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
#include "SpirvPasses/RenameSamplerFunctionParameterPass.h"
|
||||||
#include "SpirvPasses/RenameBuiltinShadowingFunctionsPass.h"
|
#include "SpirvPasses/RenameBuiltinShadowingFunctionsPass.h"
|
||||||
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
#include "SpirvPasses/DecomposeWorkgroupVec3Pass.h"
|
||||||
@@ -1358,6 +1359,9 @@ namespace MobileGL {
|
|||||||
// Output (see above), so never-written outputs are trimmed from the
|
// Output (see above), so never-written outputs are trimmed from the
|
||||||
// OpEntryPoint operand list here.
|
// OpEntryPoint operand list here.
|
||||||
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
optimizer.RegisterPass(CreateRemoveUnusedInterfaceVariablesPass());
|
||||||
|
// Gives a compatibility-profile gl_PerVertex its core shape. After ADCE, so a
|
||||||
|
// legacy member that is only read into dead code no longer counts as accessed.
|
||||||
|
optimizer.RegisterPass(StripCompatPerVertexMembersPass::CreateStripCompatPerVertexMembersPass());
|
||||||
// The two module-legality repairs, so the chain's output - the bytes every
|
// The two module-legality repairs, so the chain's output - the bytes every
|
||||||
// consumer downstream sees - is valid Vulkan SPIR-V. Rect lowering used to
|
// consumer downstream sees - is valid Vulkan SPIR-V. Rect lowering used to
|
||||||
// live only in the backends; a validating lane would flag every rectangle
|
// live only in the backends; a validating lane would flag every rectangle
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripCompatPerVertexMembersPass.cpp
|
||||||
|
// Copyright (c) 2025-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 "StripCompatPerVertexMembersPass.h"
|
||||||
|
|
||||||
|
#include "spirv.hpp"
|
||||||
|
#include "source/opt/constants.h"
|
||||||
|
#include "source/opt/def_use_manager.h"
|
||||||
|
#include "source/opt/instruction.h"
|
||||||
|
#include "source/opt/ir_context.h"
|
||||||
|
#include "source/opt/module.h"
|
||||||
|
#include "source/util/make_unique.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
namespace {
|
||||||
|
using spvtools::opt::Instruction;
|
||||||
|
using spvtools::opt::IRContext;
|
||||||
|
using spvtools::opt::Operand;
|
||||||
|
|
||||||
|
// A struct that is a Block, has at least one BuiltIn member and at least one
|
||||||
|
// member without: the shape glslang gives a compatibility-profile gl_PerVertex.
|
||||||
|
struct MixedBlock {
|
||||||
|
Instruction* type = nullptr;
|
||||||
|
std::set<uint32_t> builtInMembers;
|
||||||
|
// Every member index some access chain reaches, across all its variables.
|
||||||
|
std::set<uint32_t> accessedMembers;
|
||||||
|
// (access chain, in-operand) pairs that name a member, rewritten on a strip.
|
||||||
|
std::vector<std::pair<Instruction*, uint32_t>> memberOperands;
|
||||||
|
bool rejected = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsAccessChain(spv::Op op) {
|
||||||
|
return op == spv::Op::OpAccessChain || op == spv::Op::OpInBoundsAccessChain;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsNameOrDecoration(const Instruction& inst) {
|
||||||
|
return inst.IsDecoration() || inst.opcode() == spv::Op::OpName ||
|
||||||
|
inst.opcode() == spv::Op::OpMemberName;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<uint32_t, MixedBlock> CollectMixedBlocks(IRContext* ctx) {
|
||||||
|
std::set<uint32_t> blocks;
|
||||||
|
std::map<uint32_t, std::set<uint32_t>> builtIns;
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() == spv::Op::OpDecorate &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) == spv::Decoration::Block) {
|
||||||
|
blocks.insert(ann.GetSingleWordInOperand(0));
|
||||||
|
} else if (ann.opcode() == spv::Op::OpMemberDecorate &&
|
||||||
|
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) ==
|
||||||
|
spv::Decoration::BuiltIn) {
|
||||||
|
builtIns[ann.GetSingleWordInOperand(0)].insert(ann.GetSingleWordInOperand(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::map<uint32_t, MixedBlock> mixed;
|
||||||
|
for (auto& [structId, members] : builtIns) {
|
||||||
|
if (blocks.count(structId) == 0) continue;
|
||||||
|
Instruction* type = ctx->get_def_use_mgr()->GetDef(structId);
|
||||||
|
if (type == nullptr || type->opcode() != spv::Op::OpTypeStruct) continue;
|
||||||
|
if (members.size() >= type->NumInOperands()) continue;
|
||||||
|
mixed[structId] = MixedBlock{type, members};
|
||||||
|
}
|
||||||
|
return mixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The struct's only uses may be names, decorations, Input/Output pointer types
|
||||||
|
// and arrays of it, so no value of the whole block exists anywhere. A decoration
|
||||||
|
// group could hide a BuiltIn from CollectMixedBlocks, so it rejects too.
|
||||||
|
bool OnlyInterfaceTypesUse(IRContext* ctx, uint32_t typeId, bool allowArray) {
|
||||||
|
return ctx->get_def_use_mgr()->WhileEachUser(typeId, [&](Instruction* user) {
|
||||||
|
if (user->opcode() == spv::Op::OpGroupMemberDecorate) return false;
|
||||||
|
if (IsNameOrDecoration(*user)) return true;
|
||||||
|
if (user->opcode() == spv::Op::OpTypeArray && allowArray) {
|
||||||
|
return OnlyInterfaceTypesUse(ctx, user->result_id(), false);
|
||||||
|
}
|
||||||
|
if (user->opcode() != spv::Op::OpTypePointer) return false;
|
||||||
|
const auto storage = static_cast<spv::StorageClass>(user->GetSingleWordInOperand(0));
|
||||||
|
return storage == spv::StorageClass::Input || storage == spv::StorageClass::Output;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follows every use of a pointer to the block (`arrayed`: to an array of it).
|
||||||
|
// Only access chains are followed; anything else that takes the pointer rejects.
|
||||||
|
void WalkPointerUses(IRContext* ctx, Instruction* pointer, bool arrayed, MixedBlock& block) {
|
||||||
|
ctx->get_def_use_mgr()->ForEachUse(pointer, [&](Instruction* user, uint32_t operandIndex) {
|
||||||
|
if (block.rejected) return;
|
||||||
|
if (IsNameOrDecoration(*user) || user->opcode() == spv::Op::OpEntryPoint) return;
|
||||||
|
// Operand 2 of an access chain is its base (result type and id come first).
|
||||||
|
if (!IsAccessChain(user->opcode()) || operandIndex != 2) {
|
||||||
|
block.rejected = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint32_t memberOperand = arrayed ? 2 : 1;
|
||||||
|
if (user->NumInOperands() <= memberOperand) {
|
||||||
|
// Stops at the block or the array: the result is another such pointer.
|
||||||
|
const bool stillArrayed = arrayed && user->NumInOperands() == 1;
|
||||||
|
WalkPointerUses(ctx, user, stillArrayed, block);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uint32_t indexId = user->GetSingleWordInOperand(memberOperand);
|
||||||
|
const auto* index = ctx->get_constant_mgr()->FindDeclaredConstant(indexId);
|
||||||
|
if (index == nullptr || index->AsIntConstant() == nullptr) {
|
||||||
|
block.rejected = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
block.accessedMembers.insert(index->GetU32());
|
||||||
|
block.memberOperands.emplace_back(user, memberOperand);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The struct the Input/Output variable points at, and whether it is arrayed.
|
||||||
|
uint32_t VariableBlockType(IRContext* ctx, const Instruction& var, bool& arrayed) {
|
||||||
|
Instruction* pointer = ctx->get_def_use_mgr()->GetDef(var.type_id());
|
||||||
|
if (pointer == nullptr || pointer->opcode() != spv::Op::OpTypePointer) return 0;
|
||||||
|
Instruction* pointee = ctx->get_def_use_mgr()->GetDef(pointer->GetSingleWordInOperand(1));
|
||||||
|
arrayed = pointee != nullptr && pointee->opcode() == spv::Op::OpTypeArray;
|
||||||
|
if (arrayed) pointee = ctx->get_def_use_mgr()->GetDef(pointee->GetSingleWordInOperand(0));
|
||||||
|
return pointee != nullptr && pointee->opcode() == spv::Op::OpTypeStruct ? pointee->result_id() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kills an index constant or the array type of a dropped member, and the array
|
||||||
|
// length behind it, once nothing but names and decorations use them.
|
||||||
|
void KillIfUnused(IRContext* ctx, uint32_t id) {
|
||||||
|
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
||||||
|
if (def == nullptr) return;
|
||||||
|
bool used = false;
|
||||||
|
ctx->get_def_use_mgr()->ForEachUser(def, [&](Instruction* user) {
|
||||||
|
if (!IsNameOrDecoration(*user)) used = true;
|
||||||
|
});
|
||||||
|
if (used) return;
|
||||||
|
const bool isArray = def->opcode() == spv::Op::OpTypeArray;
|
||||||
|
const uint32_t lengthId = isArray ? def->GetSingleWordInOperand(1) : 0;
|
||||||
|
if (!isArray && def->opcode() != spv::Op::OpConstant) return;
|
||||||
|
ctx->KillInst(def);
|
||||||
|
if (lengthId != 0) KillIfUnused(ctx, lengthId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns whether the module changed. The block stays as it was when a renumbered
|
||||||
|
// index constant cannot be declared.
|
||||||
|
bool StripBlock(IRContext* ctx, MixedBlock& block) {
|
||||||
|
const uint32_t structId = block.type->result_id();
|
||||||
|
std::map<uint32_t, uint32_t> newIndexOf;
|
||||||
|
for (const uint32_t member : block.builtInMembers) {
|
||||||
|
const uint32_t next = static_cast<uint32_t>(newIndexOf.size());
|
||||||
|
newIndexOf[member] = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants first, while the type manager still sees the original struct.
|
||||||
|
auto* constants = ctx->get_constant_mgr();
|
||||||
|
std::vector<uint32_t> newIndexIds;
|
||||||
|
for (auto& [chain, operand] : block.memberOperands) {
|
||||||
|
const auto* oldIndex = constants->FindDeclaredConstant(chain->GetSingleWordInOperand(operand));
|
||||||
|
const uint32_t newIndex = newIndexOf.at(oldIndex->GetU32());
|
||||||
|
if (newIndex == oldIndex->GetU32()) {
|
||||||
|
newIndexIds.push_back(0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Instruction* constant =
|
||||||
|
constants->GetDefiningInstruction(constants->GetConstant(oldIndex->type(), {newIndex}));
|
||||||
|
if (constant == nullptr) return !newIndexIds.empty();
|
||||||
|
newIndexIds.push_back(constant->result_id());
|
||||||
|
}
|
||||||
|
std::set<uint32_t> oldIndexIds;
|
||||||
|
for (SizeT i = 0; i < block.memberOperands.size(); ++i) {
|
||||||
|
if (newIndexIds[i] == 0) continue;
|
||||||
|
auto& [chain, operand] = block.memberOperands[i];
|
||||||
|
oldIndexIds.insert(chain->GetSingleWordInOperand(operand));
|
||||||
|
chain->SetInOperand(operand, {newIndexIds[i]});
|
||||||
|
ctx->get_def_use_mgr()->AnalyzeInstUse(chain);
|
||||||
|
}
|
||||||
|
for (const uint32_t id : oldIndexIds) {
|
||||||
|
KillIfUnused(ctx, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Instruction*> dropped;
|
||||||
|
const auto renumber = [&](Instruction& inst, uint32_t memberOperand) {
|
||||||
|
if (inst.GetSingleWordInOperand(0) != structId) return;
|
||||||
|
const auto kept = newIndexOf.find(inst.GetSingleWordInOperand(memberOperand));
|
||||||
|
if (kept == newIndexOf.end()) {
|
||||||
|
dropped.push_back(&inst);
|
||||||
|
} else {
|
||||||
|
inst.SetInOperand(memberOperand, {kept->second});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (Instruction& ann : ctx->annotations()) {
|
||||||
|
if (ann.opcode() == spv::Op::OpMemberDecorate ||
|
||||||
|
ann.opcode() == spv::Op::OpMemberDecorateString) {
|
||||||
|
renumber(ann, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Instruction& debug : ctx->module()->debugs2()) {
|
||||||
|
if (debug.opcode() == spv::Op::OpMemberName) renumber(debug, 1);
|
||||||
|
}
|
||||||
|
for (Instruction* inst : dropped) {
|
||||||
|
ctx->KillInst(inst);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint32_t> droppedTypes;
|
||||||
|
Instruction::OperandList members;
|
||||||
|
for (uint32_t i = 0; i < block.type->NumInOperands(); ++i) {
|
||||||
|
const uint32_t memberType = block.type->GetSingleWordInOperand(i);
|
||||||
|
if (newIndexOf.count(i) != 0) {
|
||||||
|
members.push_back({SPV_OPERAND_TYPE_ID, {memberType}});
|
||||||
|
} else {
|
||||||
|
droppedTypes.push_back(memberType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block.type->SetInOperands(std::move(members));
|
||||||
|
ctx->get_def_use_mgr()->AnalyzeInstUse(block.type);
|
||||||
|
for (const uint32_t typeId : droppedTypes) {
|
||||||
|
KillIfUnused(ctx, typeId);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
spvtools::opt::Pass::Status StripCompatPerVertexMembersPass::Process() {
|
||||||
|
IRContext* ctx = context();
|
||||||
|
std::map<uint32_t, MixedBlock> blocks = CollectMixedBlocks(ctx);
|
||||||
|
if (blocks.empty()) return Status::SuccessWithoutChange;
|
||||||
|
|
||||||
|
for (auto& [structId, block] : blocks) {
|
||||||
|
block.rejected = !OnlyInterfaceTypesUse(ctx, structId, true);
|
||||||
|
}
|
||||||
|
for (Instruction& inst : ctx->module()->types_values()) {
|
||||||
|
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||||
|
bool arrayed = false;
|
||||||
|
const auto found = blocks.find(VariableBlockType(ctx, inst, arrayed));
|
||||||
|
if (found == blocks.end() || found->second.rejected) continue;
|
||||||
|
WalkPointerUses(ctx, &inst, arrayed, found->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
for (auto& [structId, block] : blocks) {
|
||||||
|
if (block.rejected) continue;
|
||||||
|
bool legacyMemberUsed = false;
|
||||||
|
for (const uint32_t member : block.accessedMembers) {
|
||||||
|
legacyMemberUsed |= block.builtInMembers.count(member) == 0;
|
||||||
|
}
|
||||||
|
if (legacyMemberUsed) continue;
|
||||||
|
changed |= StripBlock(ctx, block);
|
||||||
|
}
|
||||||
|
return changed ? Status::SuccessWithChange : Status::SuccessWithoutChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
spvtools::Optimizer::PassToken StripCompatPerVertexMembersPass::CreateStripCompatPerVertexMembersPass() {
|
||||||
|
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<StripCompatPerVertexMembersPass>());
|
||||||
|
}
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripCompatPerVertexMembersPass.h
|
||||||
|
// Copyright (c) 2025-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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "source/opt/pass.h"
|
||||||
|
#include "spirv-tools/optimizer.hpp"
|
||||||
|
|
||||||
|
#include <Includes.h>
|
||||||
|
|
||||||
|
namespace MobileGL {
|
||||||
|
namespace MG_Util {
|
||||||
|
namespace ShaderTranspiler {
|
||||||
|
// Gives a compatibility-profile stage the per-vertex block its core twin has.
|
||||||
|
//
|
||||||
|
// Under a compatibility-profile #version glslang declares gl_PerVertex (and the gl_in /
|
||||||
|
// gl_out arrays of it) with the legacy members gl_ClipVertex, gl_FrontColor,
|
||||||
|
// gl_BackColor, gl_FrontSecondaryColor, gl_BackSecondaryColor, gl_TexCoord[] and
|
||||||
|
// gl_FogFragCoord between the core ones, and emits the whole block whenever any
|
||||||
|
// member is used. Those members have no SPIR-V BuiltIn, and Vulkan requires every
|
||||||
|
// member of a built-in block to carry one, so such a module is invalid even when it
|
||||||
|
// writes nothing but gl_Position, and DirectVulkan declines invalid modules.
|
||||||
|
//
|
||||||
|
// A Block struct mixing BuiltIn and plain members loses its plain members when no
|
||||||
|
// access chain reaches them; the rest are renumbered. A block whose legacy member is
|
||||||
|
// accessed, or that is reached in any way other than constant-indexed access chains
|
||||||
|
// (a whole-block load, store or copy, a pointer escaping into a call), is left as is.
|
||||||
|
class StripCompatPerVertexMembersPass final : public spvtools::opt::Pass {
|
||||||
|
public:
|
||||||
|
const char* name() const override { return "mobilegl-strip-compat-per-vertex-members"; }
|
||||||
|
Status Process() override;
|
||||||
|
|
||||||
|
static spvtools::Optimizer::PassToken CreateStripCompatPerVertexMembersPass();
|
||||||
|
};
|
||||||
|
} // namespace ShaderTranspiler
|
||||||
|
} // namespace MG_Util
|
||||||
|
} // namespace MobileGL
|
||||||
Reference in New Issue
Block a user