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): keep geometry outputs across EmitVertex
- RetainGeometryOutputsPass shadows each output whose pre-emit value can be observed later and copies it into the real output before every emit - runs in SanitizeAndOptimizeBinary, so Magma and Espryt both get it - iterationT terrain: flat textureResolution written once, emitted 3 times
This commit is contained in:
@@ -411,6 +411,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RetainGeometryOutputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||
|
||||
@@ -23,6 +23,7 @@ add_executable(
|
||||
StripCompatPerVertexMembersTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
DeferGeometryEmitsTest.cpp
|
||||
RetainGeometryOutputsTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
LegalizeResourceArrayIndexTest.cpp
|
||||
FlattenAtomicCounterBlockTest.cpp
|
||||
@@ -46,6 +47,7 @@ target_include_directories(SpirvPassTest PRIVATE
|
||||
target_link_libraries(
|
||||
SpirvPassTest PRIVATE
|
||||
MobileGLTestMain
|
||||
glslang-default-resource-limits
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/RetainGeometryOutputsTest.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
|
||||
//
|
||||
// RetainGeometryOutputsPass makes a geometry output keep its last written value across
|
||||
// EmitVertex(), as desktop drivers do. What has to hold: an output written before an earlier emit
|
||||
// is copied from its Private shadow into the real output right before every emit and is otherwise
|
||||
// never touched directly; outputs written before every emit, and stages that write everything
|
||||
// before every emit, stay as they were; unwritten builtin members stay inactive; the module stays
|
||||
// valid Vulkan SPIR-V whose ESSL translation compiles; and the sanitize chain both backends consume
|
||||
// runs it.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpirvPasses/RetainGeometryOutputsPass.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <glslang/Public/ResourceLimits.h>
|
||||
#include <glslang/Public/ShaderLang.h>
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
#include <spirv-tools/optimizer.hpp>
|
||||
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "spirv.hpp"
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::RetainGeometryOutputsPass;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
|
||||
namespace {
|
||||
Vector<Uint32> CompileStage(GLenum stage, const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .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 = {stage}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
Vector<Uint32> CompileGeometry(const String& source) {
|
||||
return CompileStage(GL_GEOMETRY_SHADER, source);
|
||||
}
|
||||
|
||||
Vector<Uint32> Retain(const Vector<Uint32>& spirv) {
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(RetainGeometryOutputsPass::CreateRetainGeometryOutputsPass());
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false);
|
||||
Vector<Uint32> out;
|
||||
EXPECT_TRUE(optimizer.Run(spirv.data(), spirv.size(), &out, options));
|
||||
return out;
|
||||
}
|
||||
|
||||
Bool Validates(const Vector<Uint32>& spirv, String& messages) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
tools.SetMessageConsumer([&messages](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) { messages += String(message) + "\n"; });
|
||||
return tools.Validate(spirv);
|
||||
}
|
||||
|
||||
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{};
|
||||
}
|
||||
|
||||
// Whether the ESSL SPIRV-Cross printed is accepted by an ES front end.
|
||||
Bool EsslCompiles(const String& essl, String& log) {
|
||||
glslang::TShader shader(EShLangGeometry);
|
||||
const char* strings[] = {essl.c_str()};
|
||||
shader.setStrings(strings, 1);
|
||||
const Bool ok = shader.parse(GetDefaultResources(), 320, EEsProfile, false, false, EShMsgDefault);
|
||||
log = shader.getInfoLog();
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::unique_ptr<IRContext> Parse(const Vector<Uint32>& spirv) {
|
||||
return spvtools::BuildModule(SPV_ENV_VULKAN_1_1, nullptr, spirv.data(), spirv.size());
|
||||
}
|
||||
|
||||
// The OpVariable whose OpName is `name`, or 0.
|
||||
Uint32 VariableNamed(IRContext* context, const String& name) {
|
||||
for (const Instruction& debugInst : context->debugs2()) {
|
||||
if (debugInst.opcode() != spv::Op::OpName || debugInst.GetInOperand(1).AsString() != name) continue;
|
||||
const Instruction* def = context->get_def_use_mgr()->GetDef(debugInst.GetSingleWordInOperand(0));
|
||||
if (def != nullptr && def->opcode() == spv::Op::OpVariable) return def->result_id();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
spv::StorageClass StorageClassOf(IRContext* context, Uint32 variableId) {
|
||||
return static_cast<spv::StorageClass>(
|
||||
context->get_def_use_mgr()->GetDef(variableId)->GetSingleWordInOperand(0));
|
||||
}
|
||||
|
||||
// Whether `pointerId` addresses the whole of `variableId` (member < 0) or exactly its member.
|
||||
Bool Addresses(IRContext* context, Uint32 pointerId, Uint32 variableId, Int member) {
|
||||
if (member < 0) return pointerId == variableId;
|
||||
const Instruction* chain = context->get_def_use_mgr()->GetDef(pointerId);
|
||||
if (chain == nullptr || chain->opcode() != spv::Op::OpAccessChain || chain->NumInOperands() != 2 ||
|
||||
chain->GetSingleWordInOperand(0) != variableId)
|
||||
return false;
|
||||
const Instruction* index = context->get_def_use_mgr()->GetDef(chain->GetSingleWordInOperand(1));
|
||||
return index != nullptr && index->opcode() == spv::Op::OpConstant &&
|
||||
index->GetSingleWordInOperand(0) == Uint32(member);
|
||||
}
|
||||
|
||||
struct EmitSites {
|
||||
SizeT emits = 0;
|
||||
// Emits whose own straight-line run (since the previous emit or the block start) stores the output.
|
||||
SizeT emitsPrecededByStore = 0;
|
||||
SizeT storesToOutput = 0;
|
||||
SizeT loadsFromOutput = 0;
|
||||
};
|
||||
|
||||
EmitSites Inspect(IRContext* context, Uint32 variableId, Int member = -1) {
|
||||
EmitSites sites;
|
||||
for (auto& function : *context->module()) {
|
||||
for (auto& block : function) {
|
||||
Bool storedSinceEmit = false;
|
||||
for (Instruction& inst : block) {
|
||||
if (inst.opcode() == spv::Op::OpEmitVertex || inst.opcode() == spv::Op::OpEmitStreamVertex) {
|
||||
++sites.emits;
|
||||
if (storedSinceEmit) ++sites.emitsPrecededByStore;
|
||||
storedSinceEmit = false;
|
||||
} else if (inst.opcode() == spv::Op::OpStore &&
|
||||
Addresses(context, inst.GetSingleWordInOperand(0), variableId, member)) {
|
||||
++sites.storesToOutput;
|
||||
storedSinceEmit = true;
|
||||
} else if (inst.opcode() == spv::Op::OpLoad &&
|
||||
Addresses(context, inst.GetSingleWordInOperand(0), variableId, member)) {
|
||||
++sites.loadsFromOutput;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
Uint32 PerVertexBlock(IRContext* context) {
|
||||
for (const Instruction& global : context->types_values()) {
|
||||
if (global.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) != spv::StorageClass::Output)
|
||||
continue;
|
||||
const Instruction* pointer = context->get_def_use_mgr()->GetDef(global.type_id());
|
||||
const Instruction* pointee = context->get_def_use_mgr()->GetDef(pointer->GetSingleWordInOperand(1));
|
||||
if (pointee->opcode() == spv::Op::OpTypeStruct) return global.result_id();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ExpectValidAndCompilable(const Vector<Uint32>& spirv) {
|
||||
String messages;
|
||||
EXPECT_TRUE(Validates(spirv, messages)) << messages;
|
||||
const String essl = Transpile(spirv);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
String log;
|
||||
EXPECT_TRUE(EsslCompiles(essl, log)) << log << "\n" << essl;
|
||||
}
|
||||
|
||||
// gsprobe's case 3 and iterationT's gbuffers_terrain.gsh: a flat output written (and read
|
||||
// back) once, before the loop that emits; the provoking vertex is the third.
|
||||
const char* const kFlatBeforeLoop = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
in vec2 v_pos[];
|
||||
flat out float res;
|
||||
out vec2 p;
|
||||
void main() {
|
||||
res = abs(v_pos[0].x) + 1.0;
|
||||
res = exp2(round(log2(res)));
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; p = v_pos[i]; EmitVertex(); }
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// Everything written before every emit, one output through both arms of a branch.
|
||||
const char* const kWritesEverythingPerEmit = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
in vec2 v_pos[];
|
||||
flat out float res;
|
||||
out vec2 p;
|
||||
void main() {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (v_pos[i].x > 0.0) res = 1.0; else res = 2.0;
|
||||
gl_Position = gl_in[i].gl_Position;
|
||||
p = v_pos[i];
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
|
||||
// gl_PointSize written once in a gl_PerVertex that also declares gl_ClipDistance, plus the
|
||||
// layered-rendering idiom of a gl_Layer set before the emit loop.
|
||||
const char* const kBuiltinsBeforeLoop = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
void main() {
|
||||
gl_PointSize = 4.0;
|
||||
gl_Layer = 2;
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
}
|
||||
)";
|
||||
|
||||
// The emit lives in a helper; main writes the flat output once and calls it per vertex.
|
||||
const char* const kEmitInHelper = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
flat out float res;
|
||||
void emitCorner(int i) { gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
void main() {
|
||||
res = 42.0;
|
||||
emitCorner(0);
|
||||
emitCorner(1);
|
||||
emitCorner(2);
|
||||
}
|
||||
)";
|
||||
|
||||
// Fully written before every emit, but from its own value before the previous emit.
|
||||
const char* const kReadBackAfterEmit = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
flat out float res;
|
||||
void main() {
|
||||
res = 1.0;
|
||||
for (int i = 0; i < 3; i++) { res = res * 2.0; gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
}
|
||||
)";
|
||||
|
||||
// A partially written output: .w once, .xyz per vertex.
|
||||
const char* const kPartialWrites = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
out vec4 c;
|
||||
void main() {
|
||||
c.w = 0.25;
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; c.xyz = vec3(float(i)); EmitVertex(); }
|
||||
}
|
||||
)";
|
||||
|
||||
// A user output block: only the member written before the loop is carried.
|
||||
const char* const kUserBlock = R"(#version 410 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
out Data { flat float res; vec2 p; } vOut;
|
||||
void main() {
|
||||
vOut.res = 42.0;
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; vOut.p = vec2(float(i)); EmitVertex(); }
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kStreams = R"(#version 410 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 4) out;
|
||||
layout(stream = 0) out float a;
|
||||
layout(stream = 1) out float b;
|
||||
void main() {
|
||||
a = 1.0;
|
||||
b = 2.0;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
EmitStreamVertex(0);
|
||||
EmitStreamVertex(1);
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// The flat-before-loop shape plus a storage write after the emits, so DeferGeometryEmitsPass
|
||||
// has something to defer.
|
||||
const char* const kFlatBeforeLoopWithMemoryWrite = R"(#version 430 core
|
||||
layout(triangles) in;
|
||||
layout(triangle_strip, max_vertices = 3) out;
|
||||
layout(std430, binding = 0) buffer Counter { uint n; };
|
||||
flat out float res;
|
||||
void main() {
|
||||
res = 42.0;
|
||||
for (int i = 0; i < 3; i++) { gl_Position = gl_in[i].gl_Position; EmitVertex(); }
|
||||
atomicAdd(n, 1u);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kVertex = R"(#version 410 core
|
||||
out float res;
|
||||
void main() { res = 1.0; gl_Position = vec4(0.0); }
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(RetainGeometryOutputs, AFlatOutputWrittenBeforeTheLoopIsCopiedBeforeEveryEmit) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kFlatBeforeLoop);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const Uint32 res = VariableNamed(context.get(), "res");
|
||||
const Uint32 shadow = VariableNamed(context.get(), "mg_retained_res");
|
||||
ASSERT_NE(res, 0u);
|
||||
ASSERT_NE(shadow, 0u);
|
||||
EXPECT_EQ(StorageClassOf(context.get(), res), spv::StorageClass::Output);
|
||||
EXPECT_EQ(StorageClassOf(context.get(), shadow), spv::StorageClass::Private);
|
||||
|
||||
// The real output is written only by the copy in front of the one emit, and never read.
|
||||
const EmitSites atOutput = Inspect(context.get(), res);
|
||||
EXPECT_EQ(atOutput.emits, 1u);
|
||||
EXPECT_EQ(atOutput.emitsPrecededByStore, 1u);
|
||||
EXPECT_EQ(atOutput.storesToOutput, 1u);
|
||||
EXPECT_EQ(atOutput.loadsFromOutput, 0u);
|
||||
// The shader's own two writes and its read-back moved to the shadow.
|
||||
const EmitSites atShadow = Inspect(context.get(), shadow);
|
||||
EXPECT_EQ(atShadow.storesToOutput, 2u);
|
||||
EXPECT_GE(atShadow.loadsFromOutput, 2u);
|
||||
|
||||
// `p` is written before every emit, so it is not carried.
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_p"), 0u);
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_Position"), 0u);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, AStageThatWritesEveryOutputBeforeEveryEmitIsLeftAlone) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kWritesEverythingPerEmit);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
EXPECT_EQ(Retain(spirv), spirv);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, ANonGeometryStageIsLeftAlone) {
|
||||
const Vector<Uint32> spirv = CompileStage(GL_VERTEX_SHADER, kVertex);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
EXPECT_EQ(Retain(spirv), spirv);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, OnlyTheWrittenBuiltinMembersAreCarried) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kBuiltinsBeforeLoop);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
String messages;
|
||||
EXPECT_TRUE(Validates(out, messages)) << messages;
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
EXPECT_NE(VariableNamed(context.get(), "mg_retained_PointSize"), 0u);
|
||||
EXPECT_NE(VariableNamed(context.get(), "mg_retained_Layer"), 0u);
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_Position"), 0u);
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_ClipDistance"), 0u);
|
||||
|
||||
const Uint32 perVertex = PerVertexBlock(context.get());
|
||||
ASSERT_NE(perVertex, 0u);
|
||||
const EmitSites pointSize = Inspect(context.get(), perVertex, 1);
|
||||
EXPECT_EQ(pointSize.emitsPrecededByStore, pointSize.emits);
|
||||
EXPECT_EQ(pointSize.storesToOutput, pointSize.emits);
|
||||
const EmitSites layer = Inspect(context.get(), VariableNamed(context.get(), "gl_Layer"));
|
||||
EXPECT_EQ(layer.emitsPrecededByStore, layer.emits);
|
||||
EXPECT_EQ(layer.storesToOutput, layer.emits);
|
||||
|
||||
const String essl = Transpile(out);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_EQ(essl.find("gl_ClipDistance"), String::npos) << "an unwritten builtin member became active\n" << essl;
|
||||
EXPECT_NE(essl.find("gl_Layer = "), String::npos) << essl;
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, OnlyTheWrittenUserBlockMemberIsCarried) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kUserBlock);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
EXPECT_NE(VariableNamed(context.get(), "mg_retained_res"), 0u);
|
||||
EXPECT_EQ(VariableNamed(context.get(), "mg_retained_p"), 0u);
|
||||
const Uint32 block = VariableNamed(context.get(), "vOut");
|
||||
ASSERT_NE(block, 0u);
|
||||
const EmitSites res = Inspect(context.get(), block, 0);
|
||||
EXPECT_EQ(res.emitsPrecededByStore, res.emits);
|
||||
EXPECT_EQ(res.storesToOutput, res.emits);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, AnEmitInsideAHelperGetsTheCopyToo) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kEmitInHelper);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "res"));
|
||||
EXPECT_EQ(sites.emits, 1u);
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, 1u);
|
||||
EXPECT_EQ(sites.storesToOutput, 1u);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, AnOutputReadBackAfterAnEmitIsCarried) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kReadBackAfterEmit);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "res"));
|
||||
EXPECT_EQ(sites.storesToOutput, sites.emits);
|
||||
EXPECT_EQ(sites.loadsFromOutput, 0u) << "the read-back must see the shadow, not the emitted output";
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, APartiallyWrittenOutputIsCarriedWhole) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kPartialWrites);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
ExpectValidAndCompilable(out);
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "c"));
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, sites.emits);
|
||||
EXPECT_EQ(sites.storesToOutput, sites.emits);
|
||||
EXPECT_NE(VariableNamed(context.get(), "mg_retained_c"), 0u);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, StreamEmitsGetTheCopyToo) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kStreams);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> out = Retain(spirv);
|
||||
ASSERT_NE(out, spirv);
|
||||
String messages;
|
||||
EXPECT_TRUE(Validates(out, messages)) << messages;
|
||||
|
||||
auto context = Parse(out);
|
||||
ASSERT_NE(context, nullptr);
|
||||
for (const char* name : {"a", "b"}) {
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), name));
|
||||
EXPECT_EQ(sites.emits, 2u) << name;
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, 2u) << name;
|
||||
EXPECT_EQ(sites.storesToOutput, 2u) << name;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, TheDeferredEmitReplayCarriesTheRetainedValue) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kFlatBeforeLoopWithMemoryWrite);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
const Vector<Uint32> retained = Retain(spirv);
|
||||
ASSERT_NE(retained, spirv);
|
||||
Vector<uint32_t> deferredSpirv;
|
||||
bool deferred = false;
|
||||
ASSERT_TRUE(ShaderCompiler::DeferGeometryEmitsForEssl(retained, deferredSpirv, deferred, true));
|
||||
ASSERT_TRUE(deferred);
|
||||
ExpectValidAndCompilable(deferredSpirv);
|
||||
|
||||
// The snapshot taken at each recorded emit reads `res` after the copy, so the replay restores it.
|
||||
auto context = Parse(deferredSpirv);
|
||||
ASSERT_NE(context, nullptr);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "res"));
|
||||
EXPECT_EQ(sites.emits, 1u);
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, 1u);
|
||||
EXPECT_GE(sites.loadsFromOutput, 1u);
|
||||
}
|
||||
|
||||
TEST(RetainGeometryOutputs, TheSanitizeChainBothBackendsConsumeRunsIt) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kFlatBeforeLoop);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
Vector<uint32_t> sanitized;
|
||||
ASSERT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(spirv, sanitized, true, true));
|
||||
ExpectValidAndCompilable(sanitized);
|
||||
|
||||
auto context = Parse(sanitized);
|
||||
ASSERT_NE(context, nullptr);
|
||||
ASSERT_NE(VariableNamed(context.get(), "mg_retained_res"), 0u);
|
||||
const EmitSites sites = Inspect(context.get(), VariableNamed(context.get(), "res"));
|
||||
EXPECT_EQ(sites.emitsPrecededByStore, sites.emits);
|
||||
EXPECT_EQ(sites.storesToOutput, sites.emits);
|
||||
}
|
||||
@@ -52,6 +52,7 @@
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
#include "SpirvPasses/StripIoBlockLocationsPass.h"
|
||||
#include "SpirvPasses/DeferGeometryEmitsPass.h"
|
||||
#include "SpirvPasses/RetainGeometryOutputsPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
@@ -1375,6 +1376,12 @@ namespace MobileGL {
|
||||
RenameBuiltinShadowingFunctionsPass::CreateRenameBuiltinShadowingFunctionsPass());
|
||||
optimizer.RegisterPass(EliminateFloatEqualsZeroPass::CreateEliminateFloatEqualsZeroPass());
|
||||
optimizer.RegisterPass(DecomposeWorkgroupVec3Pass::CreateDecomposeWorkgroupVec3Pass());
|
||||
// Geometry outputs keep their last written value across EmitVertex(), as every
|
||||
// desktop driver keeps them; here rather than in a backend so both backends, and
|
||||
// every pass after this chain that works at emit time, see the retained values.
|
||||
// A non-geometry module, or one that writes every output before every emit, is
|
||||
// returned unchanged.
|
||||
optimizer.RegisterPass(RetainGeometryOutputsPass::CreateRetainGeometryOutputsPass());
|
||||
// The fp64 tail, and the ONE part of this chain that is not the same on every
|
||||
// backend. Both passes are skipped when the backend can consume Float64 itself
|
||||
// (`nativeFloat64`, i.e. VkPhysicalDeviceFeatures::shaderFloat64 on DirectVulkan):
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RetainGeometryOutputsPass.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 "RetainGeometryOutputsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
#include "source/opt/cfg.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/decoration_manager.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/function.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/util/make_unique.h"
|
||||
#include "source/util/string_utils.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
using OperandList = std::vector<Operand>;
|
||||
// One bit per slot: whether the slot was fully written since the last emit.
|
||||
using WrittenSet = std::vector<Bool>;
|
||||
|
||||
Operand Id(uint32_t id) {
|
||||
return {SPV_OPERAND_TYPE_ID, {id}};
|
||||
}
|
||||
|
||||
std::unique_ptr<Instruction> Make(IRContext* context, spv::Op op, uint32_t typeId, uint32_t resultId,
|
||||
OperandList operands) {
|
||||
return spvtools::MakeUnique<Instruction>(context, op, typeId, resultId, operands);
|
||||
}
|
||||
|
||||
Bool IsEmit(spv::Op op) {
|
||||
return op == spv::Op::OpEmitVertex || op == spv::Op::OpEmitStreamVertex;
|
||||
}
|
||||
|
||||
Bool IsAccessChain(spv::Op op) {
|
||||
return op == spv::Op::OpAccessChain || op == spv::Op::OpInBoundsAccessChain;
|
||||
}
|
||||
|
||||
// Users that name a variable without touching its memory.
|
||||
Bool IsDeclarationUser(spv::Op op) {
|
||||
return op == spv::Op::OpName || op == spv::Op::OpDecorate || op == spv::Op::OpDecorateId ||
|
||||
op == spv::Op::OpDecorateString || op == spv::Op::OpEntryPoint ||
|
||||
op == spv::Op::OpGroupDecorate;
|
||||
}
|
||||
|
||||
// The value of an OpConstant integer, or nullopt for anything else.
|
||||
Optional<uint32_t> ConstantValue(IRContext* context, uint32_t id) {
|
||||
const Instruction* def = context->get_def_use_mgr()->GetDef(id);
|
||||
if (def == nullptr || def->opcode() != spv::Op::OpConstant || def->NumInOperands() < 1)
|
||||
return std::nullopt;
|
||||
return def->GetSingleWordInOperand(0);
|
||||
}
|
||||
|
||||
Bool IsBlockStruct(IRContext* context, uint32_t typeId) {
|
||||
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
||||
return type != nullptr && type->opcode() == spv::Op::OpTypeStruct &&
|
||||
context->get_decoration_mgr()->HasDecoration(typeId, uint32_t(spv::Decoration::Block));
|
||||
}
|
||||
|
||||
// Whether a Block-decorated struct sits anywhere inside this type: such a type
|
||||
// cannot be copied into a Private shadow.
|
||||
Bool ContainsBlock(IRContext* context, uint32_t typeId) {
|
||||
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
||||
if (type == nullptr) return false;
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeArray:
|
||||
case spv::Op::OpTypeRuntimeArray:
|
||||
return ContainsBlock(context, type->GetSingleWordInOperand(0));
|
||||
case spv::Op::OpTypeStruct:
|
||||
if (IsBlockStruct(context, typeId)) return true;
|
||||
for (uint32_t i = 0; i < type->NumInOperands(); ++i) {
|
||||
if (ContainsBlock(context, type->GetSingleWordInOperand(i))) return true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether every use of this pointer, followed through access chains, is one the
|
||||
// redirection can retarget (loads, stores, copies, further access chains), whether
|
||||
// any of them writes, and every pointer derived from it. A pointer handed to a call
|
||||
// or used as a value is not redirectable: the callee's parameter type names the
|
||||
// Output storage class.
|
||||
struct PointerUse {
|
||||
Bool redirectable = true;
|
||||
Bool stored = false;
|
||||
std::vector<uint32_t> pointers;
|
||||
};
|
||||
void ScanPointerUses(IRContext* context, Instruction* pointer, PointerUse& use) {
|
||||
use.pointers.push_back(pointer->result_id());
|
||||
context->get_def_use_mgr()->ForEachUse(pointer, [&](Instruction* user, uint32_t operandIndex) {
|
||||
const spv::Op op = user->opcode();
|
||||
if (IsDeclarationUser(op)) return;
|
||||
switch (op) {
|
||||
case spv::Op::OpLoad:
|
||||
break;
|
||||
case spv::Op::OpStore:
|
||||
if (operandIndex == 0) {
|
||||
use.stored = true;
|
||||
} else {
|
||||
use.redirectable = false;
|
||||
}
|
||||
break;
|
||||
case spv::Op::OpCopyMemory:
|
||||
case spv::Op::OpCopyMemorySized:
|
||||
if (operandIndex == 0) use.stored = true;
|
||||
break;
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
if (operandIndex == 2) {
|
||||
ScanPointerUses(context, user, use);
|
||||
} else {
|
||||
use.redirectable = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
use.redirectable = false;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// One output value that may be retained: a whole output variable, or one member
|
||||
// of an output block.
|
||||
struct Slot {
|
||||
uint32_t variableId = 0;
|
||||
Int64 member = -1;
|
||||
uint32_t valueTypeId = 0;
|
||||
uint32_t blockTypeId = 0;
|
||||
// Filled for the slots that are retained.
|
||||
uint32_t shadowId = 0;
|
||||
uint32_t privatePointerTypeId = 0;
|
||||
uint32_t outputPointerTypeId = 0;
|
||||
uint32_t memberIndexId = 0;
|
||||
};
|
||||
|
||||
String NameOf(IRContext* context, uint32_t id) {
|
||||
for (const Instruction& debugInst : context->debugs2()) {
|
||||
if (debugInst.opcode() == spv::Op::OpName && debugInst.GetSingleWordInOperand(0) == id)
|
||||
return debugInst.GetInOperand(1).AsString();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
String MemberNameOf(IRContext* context, uint32_t structTypeId, uint32_t member) {
|
||||
for (const Instruction& debugInst : context->debugs2()) {
|
||||
if (debugInst.opcode() == spv::Op::OpMemberName &&
|
||||
debugInst.GetSingleWordInOperand(0) == structTypeId &&
|
||||
debugInst.GetSingleWordInOperand(1) == member)
|
||||
return debugInst.GetInOperand(2).AsString();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// The shadow's debug name. A builtin's `gl_` prefix is dropped so the generated
|
||||
// source never spells a reserved name, nor one a text pass searches for.
|
||||
String ShadowName(String outputName) {
|
||||
if (outputName.empty()) return {};
|
||||
if (outputName.rfind("gl_", 0) == 0) outputName.erase(0, 3);
|
||||
while (!outputName.empty() && outputName.front() == '_')
|
||||
outputName.erase(0, 1);
|
||||
return outputName.empty() ? String{} : "mg_retained_" + outputName;
|
||||
}
|
||||
|
||||
// Which slot each pointer addresses: `whole` holds the pointers a store through
|
||||
// which rewrites the whole slot, `any` every pointer into a slot.
|
||||
struct SlotPointers {
|
||||
std::unordered_map<uint32_t, SizeT> whole;
|
||||
std::unordered_map<uint32_t, SizeT> any;
|
||||
};
|
||||
|
||||
// Runs a must-be-written dataflow over one function and marks every slot whose
|
||||
// value from before an earlier emit can be observed: at an emit (not fully written
|
||||
// since the previous one) or by a read (same condition). The entry function starts
|
||||
// with every slot counted as written, as nothing has been emitted yet; any other
|
||||
// function starts with none, since its caller may have emitted. A call that can
|
||||
// emit acts like an emit; writes inside callees are not followed.
|
||||
void MarkStaleSlots(IRContext* context, Function& function, Bool isEntryFunction,
|
||||
const SlotPointers& slotPointers,
|
||||
const std::unordered_set<uint32_t>& emittingFunctions, SizeT slotCount,
|
||||
std::vector<Bool>& stale) {
|
||||
const WrittenSet all(slotCount, true);
|
||||
const WrittenSet none(slotCount, false);
|
||||
auto transfer = [&](BasicBlock& block, WrittenSet& written, Bool check) {
|
||||
auto read = [&](uint32_t pointer) {
|
||||
const auto slot = slotPointers.any.find(pointer);
|
||||
if (check && slot != slotPointers.any.end() && !written[slot->second])
|
||||
stale[slot->second] = true;
|
||||
};
|
||||
for (Instruction& inst : block) {
|
||||
const spv::Op op = inst.opcode();
|
||||
if (IsEmit(op)) {
|
||||
if (check) {
|
||||
for (SizeT slot = 0; slot < slotCount; ++slot) {
|
||||
if (!written[slot]) stale[slot] = true;
|
||||
}
|
||||
}
|
||||
written = none;
|
||||
} else if (op == spv::Op::OpFunctionCall) {
|
||||
if (emittingFunctions.count(inst.GetSingleWordInOperand(0)) != 0) written = none;
|
||||
} else if (op == spv::Op::OpLoad) {
|
||||
read(inst.GetSingleWordInOperand(0));
|
||||
} else if (op == spv::Op::OpStore || op == spv::Op::OpCopyMemory ||
|
||||
op == spv::Op::OpCopyMemorySized) {
|
||||
if (op != spv::Op::OpStore) read(inst.GetSingleWordInOperand(1));
|
||||
const auto target = slotPointers.whole.find(inst.GetSingleWordInOperand(0));
|
||||
if (target != slotPointers.whole.end()) written[target->second] = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
spvtools::opt::CFG* cfg = context->cfg();
|
||||
const uint32_t entryLabel = function.begin()->id();
|
||||
std::unordered_map<uint32_t, WrittenSet> out;
|
||||
for (BasicBlock& block : function)
|
||||
out[block.id()] = all;
|
||||
auto entryState = [&](BasicBlock& block) {
|
||||
if (block.id() == entryLabel) return isEntryFunction ? all : none;
|
||||
WrittenSet written = all;
|
||||
for (uint32_t predecessor : cfg->preds(block.id())) {
|
||||
const auto found = out.find(predecessor);
|
||||
if (found == out.end()) continue;
|
||||
for (SizeT slot = 0; slot < slotCount; ++slot)
|
||||
written[slot] = written[slot] && found->second[slot];
|
||||
}
|
||||
return written;
|
||||
};
|
||||
for (Bool changed = true; changed;) {
|
||||
changed = false;
|
||||
for (BasicBlock& block : function) {
|
||||
WrittenSet written = entryState(block);
|
||||
transfer(block, written, false);
|
||||
if (written != out[block.id()]) {
|
||||
out[block.id()] = std::move(written);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (BasicBlock& block : function) {
|
||||
WrittenSet written = entryState(block);
|
||||
transfer(block, written, true);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status RetainGeometryOutputsPass::Process() {
|
||||
IRContext* irContext = context();
|
||||
auto* defUse = irContext->get_def_use_mgr();
|
||||
|
||||
// One geometry entry point.
|
||||
Instruction* entryPoint = nullptr;
|
||||
for (Instruction& candidate : irContext->module()->entry_points()) {
|
||||
if (entryPoint != nullptr) return Status::SuccessWithoutChange;
|
||||
entryPoint = &candidate;
|
||||
}
|
||||
if (entryPoint == nullptr || static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0)) !=
|
||||
spv::ExecutionModel::Geometry) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
const uint32_t entryFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
|
||||
// Every emit, and every function a call to which can emit.
|
||||
std::vector<Instruction*> emits;
|
||||
std::unordered_set<uint32_t> emittingFunctions;
|
||||
for (Function& function : *irContext->module()) {
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (!IsEmit(inst->opcode())) return;
|
||||
emits.push_back(inst);
|
||||
emittingFunctions.insert(function.result_id());
|
||||
});
|
||||
}
|
||||
if (emits.empty()) return Status::SuccessWithoutChange;
|
||||
for (Bool grew = true; grew;) {
|
||||
grew = false;
|
||||
for (Function& function : *irContext->module()) {
|
||||
if (emittingFunctions.count(function.result_id()) != 0) continue;
|
||||
Bool callsEmitter = false;
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
if (inst->opcode() == spv::Op::OpFunctionCall &&
|
||||
emittingFunctions.count(inst->GetSingleWordInOperand(0)) != 0)
|
||||
callsEmitter = true;
|
||||
});
|
||||
if (callsEmitter) {
|
||||
emittingFunctions.insert(function.result_id());
|
||||
grew = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The candidate slots: every output, or output block member, the shader writes and
|
||||
// whose every use can be redirected.
|
||||
std::vector<Slot> slots;
|
||||
SlotPointers slotPointers;
|
||||
for (Instruction& global : irContext->types_values()) {
|
||||
if (global.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) != spv::StorageClass::Output) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t valueTypeId = defUse->GetDef(global.type_id())->GetSingleWordInOperand(1);
|
||||
if (!IsBlockStruct(irContext, valueTypeId)) {
|
||||
if (ContainsBlock(irContext, valueTypeId)) continue;
|
||||
PointerUse use;
|
||||
ScanPointerUses(irContext, &global, use);
|
||||
if (!use.stored || !use.redirectable) continue;
|
||||
slotPointers.whole[global.result_id()] = slots.size();
|
||||
for (uint32_t pointer : use.pointers)
|
||||
slotPointers.any[pointer] = slots.size();
|
||||
slots.push_back({global.result_id(), -1, valueTypeId});
|
||||
continue;
|
||||
}
|
||||
|
||||
// A block, member by member. A use of the block as a whole (never emitted by
|
||||
// the front end) leaves the whole block alone.
|
||||
const Instruction* blockType = defUse->GetDef(valueTypeId);
|
||||
const uint32_t memberCount = blockType->NumInOperands();
|
||||
std::vector<PointerUse> memberUses(memberCount);
|
||||
std::vector<std::vector<uint32_t>> memberWholePointers(memberCount);
|
||||
Bool wholeBlockUse = false;
|
||||
defUse->ForEachUse(&global, [&](Instruction* user, uint32_t operandIndex) {
|
||||
if (IsDeclarationUser(user->opcode())) return;
|
||||
if (!IsAccessChain(user->opcode()) || operandIndex != 2 || user->NumInOperands() < 2) {
|
||||
wholeBlockUse = true;
|
||||
return;
|
||||
}
|
||||
const auto member = ConstantValue(irContext, user->GetSingleWordInOperand(1));
|
||||
if (!member || *member >= memberCount) {
|
||||
wholeBlockUse = true;
|
||||
return;
|
||||
}
|
||||
ScanPointerUses(irContext, user, memberUses[*member]);
|
||||
if (user->NumInOperands() == 2) memberWholePointers[*member].push_back(user->result_id());
|
||||
});
|
||||
if (wholeBlockUse) continue;
|
||||
for (uint32_t member = 0; member < memberCount; ++member) {
|
||||
if (!memberUses[member].stored || !memberUses[member].redirectable) continue;
|
||||
for (uint32_t pointer : memberWholePointers[member])
|
||||
slotPointers.whole[pointer] = slots.size();
|
||||
for (uint32_t pointer : memberUses[member].pointers)
|
||||
slotPointers.any[pointer] = slots.size();
|
||||
slots.push_back({global.result_id(), Int64(member), blockType->GetSingleWordInOperand(member),
|
||||
valueTypeId});
|
||||
}
|
||||
}
|
||||
if (slots.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
// Which slots can be observed holding a value from before an earlier emit.
|
||||
std::vector<Bool> stale(slots.size(), false);
|
||||
for (Function& function : *irContext->module()) {
|
||||
MarkStaleSlots(irContext, function, function.result_id() == entryFunctionId, slotPointers,
|
||||
emittingFunctions, slots.size(), stale);
|
||||
}
|
||||
std::vector<Slot> retained;
|
||||
for (SizeT slot = 0; slot < slots.size(); ++slot) {
|
||||
if (stale[slot]) retained.push_back(slots[slot]);
|
||||
}
|
||||
if (retained.empty()) return Status::SuccessWithoutChange;
|
||||
|
||||
// ---- Types and constants, then the shadows, before any instruction changes ----
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constMgr = irContext->get_constant_mgr();
|
||||
for (Slot& slot : retained) {
|
||||
slot.privatePointerTypeId =
|
||||
typeMgr->FindPointerToType(slot.valueTypeId, spv::StorageClass::Private);
|
||||
if (slot.member >= 0) {
|
||||
slot.outputPointerTypeId =
|
||||
typeMgr->FindPointerToType(slot.valueTypeId, spv::StorageClass::Output);
|
||||
slot.memberIndexId = constMgr->GetUIntConstId(uint32_t(slot.member));
|
||||
}
|
||||
}
|
||||
|
||||
// Every access chain derived from a redirected pointer changes storage class with
|
||||
// it; collected (with the Private pointer type it needs) while def-use is intact.
|
||||
std::vector<std::pair<Instruction*, uint32_t>> retypes;
|
||||
std::unordered_set<Instruction*> retypeSeen;
|
||||
auto collectRetypes = [&](auto&& self, Instruction* pointer) -> void {
|
||||
defUse->ForEachUser(pointer, [&](Instruction* user) {
|
||||
if (!IsAccessChain(user->opcode()) || user->GetSingleWordInOperand(0) != pointer->result_id())
|
||||
return;
|
||||
if (!retypeSeen.insert(user).second) return;
|
||||
const uint32_t pointeeTypeId = defUse->GetDef(user->type_id())->GetSingleWordInOperand(1);
|
||||
retypes.emplace_back(user,
|
||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Private));
|
||||
self(self, user);
|
||||
});
|
||||
};
|
||||
|
||||
// Operand rewrites: (instruction, operand index counting result type and id, new id).
|
||||
struct Rewrite {
|
||||
Instruction* inst;
|
||||
uint32_t operand;
|
||||
uint32_t newId;
|
||||
};
|
||||
std::vector<Rewrite> rewrites;
|
||||
// Single-index chains onto a retained block member: the shadow replaces them.
|
||||
std::vector<Instruction*> aliasChains;
|
||||
// Deeper chains onto a retained block member: rebased onto the shadow.
|
||||
std::vector<std::pair<Instruction*, Slot*>> rebasedChains;
|
||||
|
||||
for (Slot& slot : retained) {
|
||||
slot.shadowId = irContext->TakeNextId();
|
||||
Instruction* variable = defUse->GetDef(slot.variableId);
|
||||
if (slot.member < 0) {
|
||||
defUse->ForEachUse(variable, [&](Instruction* user, uint32_t operandIndex) {
|
||||
if (!IsDeclarationUser(user->opcode()))
|
||||
rewrites.push_back({user, operandIndex, slot.shadowId});
|
||||
});
|
||||
collectRetypes(collectRetypes, variable);
|
||||
continue;
|
||||
}
|
||||
defUse->ForEachUser(variable, [&](Instruction* user) {
|
||||
if (!IsAccessChain(user->opcode()) || user->NumInOperands() < 2) return;
|
||||
const auto member = ConstantValue(irContext, user->GetSingleWordInOperand(1));
|
||||
if (!member || Int64(*member) != slot.member) return;
|
||||
if (user->NumInOperands() == 2) {
|
||||
aliasChains.push_back(user);
|
||||
defUse->ForEachUse(user, [&](Instruction* aliasUser, uint32_t operandIndex) {
|
||||
if (!IsDeclarationUser(aliasUser->opcode()))
|
||||
rewrites.push_back({aliasUser, operandIndex, slot.shadowId});
|
||||
});
|
||||
} else {
|
||||
rebasedChains.emplace_back(user, &slot);
|
||||
const uint32_t pointeeTypeId = defUse->GetDef(user->type_id())->GetSingleWordInOperand(1);
|
||||
retypes.emplace_back(user,
|
||||
typeMgr->FindPointerToType(pointeeTypeId, spv::StorageClass::Private));
|
||||
retypeSeen.insert(user);
|
||||
}
|
||||
collectRetypes(collectRetypes, user);
|
||||
});
|
||||
}
|
||||
|
||||
for (Slot& slot : retained) {
|
||||
irContext->AddGlobalValue(
|
||||
Make(irContext, spv::Op::OpVariable, slot.privatePointerTypeId, slot.shadowId,
|
||||
{{SPV_OPERAND_TYPE_STORAGE_CLASS, {uint32_t(spv::StorageClass::Private)}}}));
|
||||
const String outputName = slot.member < 0
|
||||
? NameOf(irContext, slot.variableId)
|
||||
: MemberNameOf(irContext, slot.blockTypeId, uint32_t(slot.member));
|
||||
const String shadowName = ShadowName(outputName);
|
||||
if (!shadowName.empty()) {
|
||||
irContext->AddDebug2Inst(
|
||||
Make(irContext, spv::Op::OpName, 0, 0,
|
||||
{Id(slot.shadowId),
|
||||
{SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(shadowName.c_str())}}));
|
||||
}
|
||||
// From SPIR-V 1.4 the interface lists every global the entry point uses.
|
||||
if (irContext->module()->version() >= 0x00010400u) entryPoint->AddOperand(Id(slot.shadowId));
|
||||
}
|
||||
|
||||
// ---- Hand-built from here on; analyses are rebuilt once at the end ----------
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
|
||||
for (const Rewrite& rewrite : rewrites)
|
||||
rewrite.inst->SetOperand(rewrite.operand, {rewrite.newId});
|
||||
for (auto& [chain, slot] : rebasedChains) {
|
||||
OperandList operands;
|
||||
operands.push_back(Id(slot->shadowId));
|
||||
for (uint32_t i = 2; i < chain->NumInOperands(); ++i)
|
||||
operands.push_back(chain->GetInOperand(i));
|
||||
chain->SetInOperands(std::move(operands));
|
||||
}
|
||||
for (auto& [chain, typeId] : retypes)
|
||||
chain->SetResultType(typeId);
|
||||
for (Instruction* chain : aliasChains)
|
||||
irContext->KillInst(chain);
|
||||
|
||||
// The copy into the real outputs, immediately before every emit.
|
||||
for (Instruction* emit : emits) {
|
||||
for (const Slot& slot : retained) {
|
||||
const uint32_t value = irContext->TakeNextId();
|
||||
emit->InsertBefore(
|
||||
Make(irContext, spv::Op::OpLoad, slot.valueTypeId, value, {Id(slot.shadowId)}));
|
||||
uint32_t target = slot.variableId;
|
||||
if (slot.member >= 0) {
|
||||
target = irContext->TakeNextId();
|
||||
emit->InsertBefore(Make(irContext, spv::Op::OpAccessChain, slot.outputPointerTypeId, target,
|
||||
{Id(slot.variableId), Id(slot.memberIndexId)}));
|
||||
}
|
||||
emit->InsertBefore(Make(irContext, spv::Op::OpStore, 0, 0, {Id(target), Id(value)}));
|
||||
}
|
||||
}
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken RetainGeometryOutputsPass::CreateRetainGeometryOutputsPass() {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<RetainGeometryOutputsPass>());
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,42 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RetainGeometryOutputsPass.h
|
||||
// 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
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Geometry outputs keep their last written value across EmitVertex(), as on every
|
||||
// desktop GL driver. GLSL leaves them undefined after an emit, but shaders rely on the
|
||||
// desktop behaviour: iterationT's terrain stage writes `flat out float
|
||||
// textureResolution` once and then emits three vertices, and the flat value is taken
|
||||
// from the LAST one (GL's default provoking vertex), which a driver that drops the
|
||||
// value after the first emit renders as random black triangles.
|
||||
//
|
||||
// Each output whose value from before an EARLIER emit can be observed - at a later emit
|
||||
// or by a read - gets a Private shadow; every load, store and access chain of it moves
|
||||
// to the shadow, which is copied into the real output immediately before every emit
|
||||
// (stream emits included). Block members are carried one by one, and only those the
|
||||
// shader writes, so no unwritten builtin member becomes active. A must-be-written
|
||||
// dataflow over each function's CFG finds them; a stage that writes every output
|
||||
// before every emit is left byte-identical. Outputs whose pointer escapes into a call,
|
||||
// and arrays of blocks, are left alone.
|
||||
class RetainGeometryOutputsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
const char* name() const override { return "retain-geometry-outputs"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateRetainGeometryOutputsPass();
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
Reference in New Issue
Block a user