mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Fix] (Espryt): defer geometry emits behind memory writes on drivers that drop them
This commit is contained in:
@@ -409,6 +409,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
|
||||
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/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||
|
||||
@@ -214,6 +214,13 @@ namespace MobileGL::MG_Config {
|
||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS: DirectGLES moves a geometry shader's
|
||||
// EmitVertex()/EndPrimitive() behind its memory writes (DeferGeometryEmitsPass). The
|
||||
// Adreno ES driver silently drops storage writes a geometry invocation makes after
|
||||
// emitting; the driver POST measures that and arms this by itself, so Auto is right
|
||||
// everywhere. ForceOn exercises the rewrite on a healthy driver; ForceOff is the
|
||||
// negative control that replays the lost writes.
|
||||
QuirkOverride EsprytDeferGeometryEmits = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
|
||||
@@ -394,6 +394,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.EsprytDeferGeometryEmits = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
|
||||
@@ -11629,6 +11629,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
effectiveSpirv = &loweredSpirv;
|
||||
}
|
||||
|
||||
// A geometry stage that writes memory, on a driver that drops the writes it makes
|
||||
// after EmitVertex(): every emit is deferred behind them (DeferGeometryEmitsPass). FIRST
|
||||
// in the chain, while every output is still an Output variable - the viewport-index
|
||||
// lowering below demotes one, and must see the replay's stores to it as well.
|
||||
// Self-gated on the module (an emit and a memory write), so a geometry stage that
|
||||
// writes nothing comes back untouched. Keyed by deferGeometryEmitsArmed.
|
||||
Vector<unsigned int> deferredEmitSpirv;
|
||||
if (glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit) {
|
||||
bool deferred = false;
|
||||
if (MG_Util::ShaderTranspiler::ShaderCompiler::DeferGeometryEmitsForEssl(
|
||||
*effectiveSpirv, deferredEmitSpirv, deferred, enableSpirvValidation) &&
|
||||
deferred && !deferredEmitSpirv.empty()) {
|
||||
effectiveSpirv = &deferredEmitSpirv;
|
||||
MGLOG_I_ONCE("DirectGLES is deferring a geometry stage's EmitVertex() calls behind its "
|
||||
"memory writes, because this driver drops storage writes made after an emit.");
|
||||
}
|
||||
}
|
||||
|
||||
// ESSL cannot express gl_ViewportIndex either, but unlike the draw parameters
|
||||
// there IS an extension that provides it - so this runs only when the driver does
|
||||
// NOT advertise GL_OES_viewport_array. A driver that does keeps the builtin and
|
||||
@@ -12641,6 +12659,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
(glShaderType == GL_VERTEX_SHADER || glShaderType == GL_TESS_EVALUATION_SHADER ||
|
||||
glShaderType == GL_GEOMETRY_SHADER) &&
|
||||
(ViewportArrayEmulationEnabled() || !g_GLESCapabilities.SupportsViewportArray);
|
||||
esslKeyInputs.deferGeometryEmitsArmed =
|
||||
glShaderType == GL_GEOMETRY_SHADER && g_GLESCapabilities.DropsGeometryWritesAfterEmit;
|
||||
esslKeyInputs.supportsNoperspectiveInterpolation =
|
||||
g_GLESCapabilities.SupportsNoperspectiveInterpolation;
|
||||
esslKeyInputs.supportsExtendedImageFormats =
|
||||
|
||||
@@ -13,6 +13,7 @@ add_executable(
|
||||
UniquifyIoBlockNamesTest.cpp
|
||||
StripIoBlockLocationsTest.cpp
|
||||
LowerViewportIndexTest.cpp
|
||||
DeferGeometryEmitsTest.cpp
|
||||
ClampMultisampleFetchTest.cpp
|
||||
LegalizeResourceArrayIndexTest.cpp
|
||||
FlattenAtomicCounterBlockTest.cpp
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/DeferGeometryEmitsTest.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
|
||||
//
|
||||
// DeferGeometryEmitsPass moves a geometry stage's emits behind its memory writes for an ES driver
|
||||
// that drops storage writes made after EmitVertex(). What has to hold: the rewritten module is
|
||||
// valid, its ESSL calls the original body BEFORE the only EmitVertex() left (the replay), output
|
||||
// members the shader never writes stay out of the ESSL, and a geometry stage that writes no memory
|
||||
// is left alone.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include <spirv-tools/libspirv.hpp>
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
|
||||
namespace {
|
||||
Vector<Uint32> CompileGeometry(const String& source) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
ShaderAttrib shaderAttrib{.shaderType = GL_GEOMETRY_SHADER, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
ProgramAttrib programAttrib{.shaders = {shaderResult.value()}};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_GEOMETRY_SHADER}, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult || binaryResult->empty()) return {};
|
||||
return binaryResult->front();
|
||||
}
|
||||
|
||||
Bool Validates(const Vector<Uint32>& spirv, 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{};
|
||||
}
|
||||
|
||||
SizeT Count(const String& haystack, const String& needle) {
|
||||
SizeT count = 0;
|
||||
for (SizeT at = haystack.find(needle); at != String::npos; at = haystack.find(needle, at + 1)) ++count;
|
||||
return count;
|
||||
}
|
||||
|
||||
// KHR-GL4x.shader_storage_buffer_object.basic-atomic-case3's geometry stage: emit first, write after.
|
||||
const char* const kWritesAfterEmit = R"(#version 430 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
layout(std430, binding = 2) buffer GSBuffer { uint u[4]; } g_gs_buffer;
|
||||
void main() {
|
||||
gl_Position = gl_in[0].gl_Position;
|
||||
gl_PrimitiveID = gl_PrimitiveIDIn;
|
||||
EmitVertex();
|
||||
if (atomicAdd(g_gs_buffer.u[gl_PrimitiveIDIn], 1u) != 0u) return;
|
||||
atomicAdd(g_gs_buffer.u[0], 2u);
|
||||
}
|
||||
)";
|
||||
|
||||
// A strip with a cut in the middle and a user varying, so the replay has to carry a non-block
|
||||
// output and reproduce EndPrimitive at the right vertex.
|
||||
const char* const kStripWithCut = R"(#version 430 core
|
||||
layout(points) in;
|
||||
layout(triangle_strip, max_vertices = 6) out;
|
||||
layout(std430, binding = 0) buffer Counter { uint n; };
|
||||
out vec4 color;
|
||||
void main() {
|
||||
for (int i = 0; i < 3; ++i) { gl_Position = vec4(float(i)); color = vec4(1.0); EmitVertex(); }
|
||||
EndPrimitive();
|
||||
for (int i = 0; i < 3; ++i) { gl_Position = vec4(float(-i)); color = vec4(0.5); EmitVertex(); }
|
||||
atomicAdd(n, 1u);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* const kNoMemoryWrites = R"(#version 430 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main() { gl_Position = gl_in[0].gl_Position; EmitVertex(); }
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(DeferGeometryEmits, WritesAfterEmitAreMovedInFrontOfTheReplay) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kWritesAfterEmit);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
Vector<uint32_t> out;
|
||||
bool deferred = false;
|
||||
ASSERT_TRUE(ShaderCompiler::DeferGeometryEmitsForEssl(spirv, out, deferred, true));
|
||||
ASSERT_TRUE(deferred);
|
||||
String messages;
|
||||
EXPECT_TRUE(Validates(out, messages)) << messages;
|
||||
|
||||
const String essl = Transpile(out);
|
||||
ASSERT_FALSE(essl.empty());
|
||||
EXPECT_EQ(Count(essl, "EmitVertex()"), 1u) << essl;
|
||||
const SizeT call = essl.find("mg_geometryBody();");
|
||||
const SizeT emit = essl.find("EmitVertex();");
|
||||
ASSERT_NE(call, String::npos) << essl;
|
||||
EXPECT_LT(call, emit) << "the body (and its atomics) must run before the replayed emit\n" << essl;
|
||||
EXPECT_EQ(essl.find("gl_ClipDistance"), String::npos) << "an unwritten builtin member became active\n" << essl;
|
||||
EXPECT_EQ(essl.find("gl_PointSize"), String::npos) << essl;
|
||||
}
|
||||
|
||||
TEST(DeferGeometryEmits, AStripCutAndAUserVaryingAreReplayed) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kStripWithCut);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
Vector<uint32_t> out;
|
||||
bool deferred = false;
|
||||
ASSERT_TRUE(ShaderCompiler::DeferGeometryEmitsForEssl(spirv, out, deferred, true));
|
||||
ASSERT_TRUE(deferred);
|
||||
String messages;
|
||||
EXPECT_TRUE(Validates(out, messages)) << messages;
|
||||
const String essl = Transpile(out);
|
||||
EXPECT_EQ(Count(essl, "EmitVertex()"), 1u) << essl;
|
||||
EXPECT_EQ(Count(essl, "EndPrimitive()"), 1u) << essl;
|
||||
EXPECT_NE(essl.find("color = "), String::npos) << essl;
|
||||
}
|
||||
|
||||
TEST(DeferGeometryEmits, AStageThatWritesNoMemoryIsLeftAlone) {
|
||||
const Vector<Uint32> spirv = CompileGeometry(kNoMemoryWrites);
|
||||
ASSERT_FALSE(spirv.empty());
|
||||
Vector<uint32_t> out;
|
||||
bool deferred = true;
|
||||
ASSERT_TRUE(ShaderCompiler::DeferGeometryEmitsForEssl(spirv, out, deferred, true));
|
||||
EXPECT_FALSE(deferred);
|
||||
}
|
||||
@@ -982,6 +982,11 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) {
|
||||
v.viewportIndexLoweringArmed = true;
|
||||
variants.emplace_back("viewportIndexLoweringArmed", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // arms DeferGeometryEmitsForEssl
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.deferGeometryEmitsArmed = true;
|
||||
variants.emplace_back("deferGeometryEmitsArmed", BuildEsslTranslationKey(v));
|
||||
}
|
||||
{ // arms EmulateNoPerspectiveForEssl
|
||||
EsslTranslationKeyInputs v = base;
|
||||
v.supportsNoperspectiveInterpolation = true;
|
||||
|
||||
@@ -2061,6 +2061,23 @@ namespace MobileGL::MG_Util::BackendLoader {
|
||||
: "false (DirectGLES will emit tessellation/geometry programs' interface "
|
||||
"blocks without a location qualifier)");
|
||||
|
||||
switch (MG_Config::Features.EsprytDeferGeometryEmits) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
caps.DropsGeometryWritesAfterEmit = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
caps.DropsGeometryWritesAfterEmit = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
default:
|
||||
caps.DropsGeometryWritesAfterEmit = SelfTest::GeometryStageSsboWriteAfterEmitDropped(glesFuncs);
|
||||
break;
|
||||
}
|
||||
MGLOG_I(" Geometry-stage storage writes after EmitVertex are dropped: %s",
|
||||
caps.DropsGeometryWritesAfterEmit
|
||||
? "true (DirectGLES defers a writing geometry stage's emits behind its writes)"
|
||||
: "false");
|
||||
|
||||
caps.IsAngleRenderer = caps.GLESRendererString.find("ANGLE") != String::npos;
|
||||
caps.IsAngleLlvmpipeRenderer =
|
||||
caps.IsAngleRenderer && caps.GLESRendererString.find("llvmpipe") != String::npos;
|
||||
|
||||
@@ -1319,6 +1319,11 @@ namespace MobileGL {
|
||||
// behaviour every driver had before the probe existed - a capability like this
|
||||
// must never be assumed broken on a driver nobody measured.
|
||||
Bool SupportsLocatedInterStageIoBlocks = true;
|
||||
// True when a geometry shader's storage writes made AFTER EmitVertex() are dropped
|
||||
// (SelfTest::GeometryStageSsboWriteAfterEmitDropped - measured on Adreno). DirectGLES
|
||||
// then defers every emit of a geometry stage that writes memory behind those writes
|
||||
// (DeferGeometryEmitsPass). Defaults FALSE: never assumed broken on an unmeasured driver.
|
||||
Bool DropsGeometryWritesAfterEmit = false;
|
||||
Int UniformBufferOffsetAlignment = 256;
|
||||
// Its storage-buffer counterpart, queried separately because it is a separate limit:
|
||||
// Adreno 830 answers 32 for GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT and 64 for
|
||||
|
||||
@@ -2275,15 +2275,23 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
|
||||
Optional<DriverBugFinding> ProbeGeometryWriteAfterEmitBug(const GLESFunctionsTable& gl) {
|
||||
if (!GeometryStageSsboWriteAfterEmitDropped(gl)) return std::nullopt;
|
||||
return DriverBugFinding{
|
||||
"Geometry-stage storage writes after EmitVertex",
|
||||
DriverBugVerdict::Unfixable,
|
||||
String detail =
|
||||
"the driver silently discards shader storage buffer writes a geometry shader "
|
||||
"issues after its last EmitVertex()/EndPrimitive(); the identical write issued "
|
||||
"BEFORE the emit lands, for both point and triangle geometry shaders. "
|
||||
"GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS is therefore NOT withdrawn - doing so "
|
||||
"would break the shaders that write before emitting, which work correctly. "
|
||||
"A shader that must write after emitting has no substitute on this driver"};
|
||||
"BEFORE the emit lands, for both point and triangle geometry shaders. ";
|
||||
// The repair follows the knob, so the row describes what this process does.
|
||||
if (MG_Config::Features.EsprytDeferGeometryEmits == MG_Config::QuirkOverride::ForceOff) {
|
||||
detail += "THE REPAIR IS DISABLED in this process (MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS), so a "
|
||||
"geometry shader that writes after emitting loses those writes";
|
||||
return DriverBugFinding{"Geometry-stage storage writes after EmitVertex",
|
||||
DriverBugVerdict::Unfixable, Move(detail)};
|
||||
}
|
||||
detail += "DirectGLES defers every EmitVertex()/EndPrimitive() of a geometry stage that writes "
|
||||
"memory: the outputs are snapshotted at each emit and the vertices replayed after the "
|
||||
"shader body returns (DeferGeometryEmitsPass), so every write lands before any emit; "
|
||||
"override with MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS";
|
||||
return DriverBugFinding{"Geometry-stage storage writes after EmitVertex", DriverBugVerdict::Fixed,
|
||||
Move(detail)};
|
||||
}
|
||||
|
||||
Optional<DriverBugFinding> ProbeR32FMultisampleSwizzleBug(const GLESFunctionsTable& gl) {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
#include "SpirvPasses/PrivateToEntryLocalPass.h"
|
||||
#include "SpirvPasses/StripUniformLocationsPass.h"
|
||||
#include "SpirvPasses/StripIoBlockLocationsPass.h"
|
||||
#include "SpirvPasses/DeferGeometryEmitsPass.h"
|
||||
#include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h"
|
||||
#include "SpirvPasses/StripNoPerspectivePass.h"
|
||||
#include "SpirvPasses/EmulateNoPerspectivePass.h"
|
||||
@@ -1454,6 +1455,19 @@ namespace MobileGL {
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeferGeometryEmitsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, bool& outDeferred,
|
||||
const bool enableSpirvValidation) {
|
||||
using namespace spvtools;
|
||||
Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
Bool deferred = false;
|
||||
optimizer.RegisterPass(DeferGeometryEmitsPass::CreateDeferGeometryEmitsPass(&deferred));
|
||||
const bool ok = RunOptimizerChecked("DeferGeometryEmitsForEssl", optimizer, inputBinary,
|
||||
outputBinary, true, enableSpirvValidation);
|
||||
outDeferred = ok && deferred;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::DeclaresViewportIndexBuiltin(const Vector<Uint32>& binary) {
|
||||
return LowerViewportIndexPass::DeclaresViewportIndexBuiltin(binary);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,12 @@ namespace MobileGL {
|
||||
static bool LowerViewportIndexForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary,
|
||||
bool enableSpirvValidation = false);
|
||||
// Moves a geometry stage's emits behind its memory writes (DeferGeometryEmitsPass), for
|
||||
// an ES driver that drops storage writes made after EmitVertex(). `outDeferred` says
|
||||
// whether the module was rewritten; a module the pass declines comes back unchanged.
|
||||
static bool DeferGeometryEmitsForEssl(const Vector<Uint32>& inputBinary,
|
||||
Vector<uint32_t>& outputBinary, bool& outDeferred,
|
||||
bool enableSpirvValidation = false);
|
||||
// Whether the module declares an output decorated BuiltIn ViewportIndex, i.e.
|
||||
// whether the pass above has anything to do. The gate that keeps every other
|
||||
// stage off an optimizer round trip it does not need.
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.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 "DeferGeometryEmitsPass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.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 <algorithm>
|
||||
#include <memory>
|
||||
#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>;
|
||||
|
||||
// Scalars of snapshot per invocation above which the pass declines. A geometry
|
||||
// stage's own output budget (GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS) is 1024 on
|
||||
// every ES 3.2 driver measured, so a legal shader stays well inside this; the cap
|
||||
// only guards against a pathological one turning into a huge private array.
|
||||
constexpr uint32_t kMaxSnapshotScalars = 8192;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// The value of an OpConstant integer, or nullopt for anything else (a spec
|
||||
// constant included: a length that can change after the pass cannot size a
|
||||
// snapshot).
|
||||
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);
|
||||
}
|
||||
|
||||
// Scalar components a value of this type holds, or nullopt when it holds anything
|
||||
// the snapshot cannot copy by value (a pointer, an opaque handle, a runtime array).
|
||||
Optional<uint32_t> ScalarCount(IRContext* context, uint32_t typeId) {
|
||||
const Instruction* type = context->get_def_use_mgr()->GetDef(typeId);
|
||||
if (type == nullptr) return std::nullopt;
|
||||
switch (type->opcode()) {
|
||||
case spv::Op::OpTypeInt:
|
||||
case spv::Op::OpTypeFloat:
|
||||
case spv::Op::OpTypeBool:
|
||||
return 1u;
|
||||
case spv::Op::OpTypeVector:
|
||||
case spv::Op::OpTypeMatrix: {
|
||||
const auto inner = ScalarCount(context, type->GetSingleWordInOperand(0));
|
||||
if (!inner) return std::nullopt;
|
||||
return *inner * type->GetSingleWordInOperand(1);
|
||||
}
|
||||
case spv::Op::OpTypeArray: {
|
||||
const auto inner = ScalarCount(context, type->GetSingleWordInOperand(0));
|
||||
const auto length = ConstantValue(context, type->GetSingleWordInOperand(1));
|
||||
if (!inner || !length) return std::nullopt;
|
||||
return *inner * *length;
|
||||
}
|
||||
case spv::Op::OpTypeStruct: {
|
||||
uint32_t total = 0;
|
||||
for (uint32_t i = 0; i < type->NumInOperands(); ++i) {
|
||||
const auto inner = ScalarCount(context, type->GetSingleWordInOperand(i));
|
||||
if (!inner) return std::nullopt;
|
||||
total += *inner;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
spv::StorageClass PointerStorageClass(IRContext* context, uint32_t pointerId) {
|
||||
const Instruction* pointer = context->get_def_use_mgr()->GetDef(pointerId);
|
||||
if (pointer == nullptr || pointer->type_id() == 0) return spv::StorageClass::Max;
|
||||
const Instruction* type = context->get_def_use_mgr()->GetDef(pointer->type_id());
|
||||
if (type == nullptr || type->opcode() != spv::Op::OpTypePointer) return spv::StorageClass::Max;
|
||||
return static_cast<spv::StorageClass>(type->GetSingleWordInOperand(0));
|
||||
}
|
||||
|
||||
// Every way a shader writes memory another invocation - or the application - can
|
||||
// see: a store into a buffer or image block, an image store, any atomic.
|
||||
Bool IsMemoryWrite(IRContext* context, const Instruction& inst) {
|
||||
switch (inst.opcode()) {
|
||||
case spv::Op::OpImageWrite:
|
||||
case spv::Op::OpAtomicStore:
|
||||
case spv::Op::OpAtomicExchange:
|
||||
case spv::Op::OpAtomicCompareExchange:
|
||||
case spv::Op::OpAtomicCompareExchangeWeak:
|
||||
case spv::Op::OpAtomicIIncrement:
|
||||
case spv::Op::OpAtomicIDecrement:
|
||||
case spv::Op::OpAtomicIAdd:
|
||||
case spv::Op::OpAtomicISub:
|
||||
case spv::Op::OpAtomicSMin:
|
||||
case spv::Op::OpAtomicUMin:
|
||||
case spv::Op::OpAtomicSMax:
|
||||
case spv::Op::OpAtomicUMax:
|
||||
case spv::Op::OpAtomicAnd:
|
||||
case spv::Op::OpAtomicOr:
|
||||
case spv::Op::OpAtomicXor:
|
||||
case spv::Op::OpAtomicFlagTestAndSet:
|
||||
case spv::Op::OpAtomicFlagClear:
|
||||
case spv::Op::OpAtomicFAddEXT:
|
||||
case spv::Op::OpAtomicFMinEXT:
|
||||
case spv::Op::OpAtomicFMaxEXT:
|
||||
return true;
|
||||
case spv::Op::OpStore: {
|
||||
const spv::StorageClass storage = PointerStorageClass(context, inst.GetSingleWordInOperand(0));
|
||||
return storage == spv::StorageClass::StorageBuffer || storage == spv::StorageClass::Uniform ||
|
||||
storage == spv::StorageClass::Image ||
|
||||
storage == spv::StorageClass::PhysicalStorageBuffer;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Whether anything stores through this pointer, directly or through an access
|
||||
// chain into it. A pointer handed to a function call counts as stored: the callee
|
||||
// is not followed, and an output passed by pointer is one the shader means to set.
|
||||
Bool IsStoredThrough(IRContext* context, Instruction* pointer) {
|
||||
Bool stored = false;
|
||||
context->get_def_use_mgr()->WhileEachUser(pointer, [&](Instruction* user) {
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpStore:
|
||||
if (user->GetSingleWordInOperand(0) == pointer->result_id()) stored = true;
|
||||
break;
|
||||
case spv::Op::OpCopyMemory:
|
||||
case spv::Op::OpCopyMemorySized:
|
||||
if (user->GetSingleWordInOperand(0) == pointer->result_id()) stored = true;
|
||||
break;
|
||||
case spv::Op::OpFunctionCall:
|
||||
stored = true;
|
||||
break;
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
stored = IsStoredThrough(context, user);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return !stored;
|
||||
});
|
||||
return stored;
|
||||
}
|
||||
|
||||
// One output value the snapshot carries: a whole output variable, or one member
|
||||
// of an output block.
|
||||
struct Slot {
|
||||
uint32_t variableId = 0;
|
||||
Int64 member = -1;
|
||||
uint32_t valueTypeId = 0;
|
||||
// Filled once the globals exist.
|
||||
uint32_t memberIndexId = 0;
|
||||
uint32_t outputPointerTypeId = 0;
|
||||
uint32_t snapshotVariableId = 0;
|
||||
uint32_t snapshotElementPointerTypeId = 0;
|
||||
};
|
||||
|
||||
void ReplaceName(IRContext* context, uint32_t id, const char* name) {
|
||||
for (auto& debugInst : context->debugs2()) {
|
||||
if (debugInst.opcode() == spv::Op::OpName && debugInst.GetSingleWordInOperand(0) == id) {
|
||||
debugInst.SetInOperand(
|
||||
1, spvtools::utils::MakeVector<spvtools::opt::Operand::OperandData>(name));
|
||||
return;
|
||||
}
|
||||
}
|
||||
context->AddDebug2Inst(Make(context, spv::Op::OpName, 0, 0,
|
||||
{Id(id), {SPV_OPERAND_TYPE_LITERAL_STRING,
|
||||
spvtools::utils::MakeVector(name)}}));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::opt::Pass::Status DeferGeometryEmitsPass::Process() {
|
||||
IRContext* irContext = context();
|
||||
if (m_outDeferred != nullptr) *m_outDeferred = false;
|
||||
|
||||
// One geometry entry point, sized by max_vertices.
|
||||
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 bodyFunctionId = entryPoint->GetSingleWordInOperand(1);
|
||||
uint32_t maxVertices = 0;
|
||||
for (const Instruction& mode : irContext->module()->execution_modes()) {
|
||||
if (mode.opcode() == spv::Op::OpExecutionMode &&
|
||||
mode.GetSingleWordInOperand(0) == bodyFunctionId &&
|
||||
static_cast<spv::ExecutionMode>(mode.GetSingleWordInOperand(1)) ==
|
||||
spv::ExecutionMode::OutputVertices) {
|
||||
maxVertices = mode.GetSingleWordInOperand(2);
|
||||
}
|
||||
}
|
||||
if (maxVertices == 0) return Status::SuccessWithoutChange;
|
||||
|
||||
std::vector<Instruction*> emits;
|
||||
std::vector<Instruction*> cuts;
|
||||
Bool writesMemory = false;
|
||||
for (Function& function : *irContext->module()) {
|
||||
Bool streams = false;
|
||||
function.ForEachInst([&](Instruction* inst) {
|
||||
switch (inst->opcode()) {
|
||||
case spv::Op::OpEmitVertex: emits.push_back(inst); break;
|
||||
case spv::Op::OpEndPrimitive: cuts.push_back(inst); break;
|
||||
case spv::Op::OpEmitStreamVertex:
|
||||
case spv::Op::OpEndStreamPrimitive: streams = true; break;
|
||||
default:
|
||||
if (!writesMemory && IsMemoryWrite(irContext, *inst)) writesMemory = true;
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (streams) return Status::SuccessWithoutChange;
|
||||
}
|
||||
if (emits.empty() || !writesMemory) return Status::SuccessWithoutChange;
|
||||
|
||||
// What the snapshot carries.
|
||||
auto* defUse = irContext->get_def_use_mgr();
|
||||
auto* decorations = irContext->get_decoration_mgr();
|
||||
std::vector<Slot> slots;
|
||||
uint64_t snapshotScalars = 0;
|
||||
for (Instruction& global : irContext->types_values()) {
|
||||
if (global.opcode() != spv::Op::OpVariable ||
|
||||
static_cast<spv::StorageClass>(global.GetSingleWordInOperand(0)) != spv::StorageClass::Output) {
|
||||
continue;
|
||||
}
|
||||
const Instruction* pointerType = defUse->GetDef(global.type_id());
|
||||
const uint32_t valueTypeId = pointerType->GetSingleWordInOperand(1);
|
||||
const Instruction* valueType = defUse->GetDef(valueTypeId);
|
||||
const Bool block = valueType->opcode() == spv::Op::OpTypeStruct &&
|
||||
decorations->HasDecoration(valueTypeId, uint32_t(spv::Decoration::Block));
|
||||
if (!block) {
|
||||
if (!IsStoredThrough(irContext, &global)) continue;
|
||||
const auto scalars = ScalarCount(irContext, valueTypeId);
|
||||
if (!scalars) return Status::SuccessWithoutChange;
|
||||
snapshotScalars += uint64_t(*scalars) * maxVertices;
|
||||
slots.push_back({global.result_id(), -1, valueTypeId});
|
||||
continue;
|
||||
}
|
||||
// A block: per written member, so an unwritten builtin member stays inactive.
|
||||
std::vector<Bool> written(valueType->NumInOperands(), false);
|
||||
Bool dynamicMember = false;
|
||||
defUse->ForEachUser(&global, [&](Instruction* user) {
|
||||
if (user->opcode() == spv::Op::OpStore && user->GetSingleWordInOperand(0) == global.result_id()) {
|
||||
std::fill(written.begin(), written.end(), true);
|
||||
return;
|
||||
}
|
||||
if (user->opcode() == spv::Op::OpFunctionCall) {
|
||||
std::fill(written.begin(), written.end(), true);
|
||||
return;
|
||||
}
|
||||
if (user->opcode() != spv::Op::OpAccessChain && user->opcode() != spv::Op::OpInBoundsAccessChain)
|
||||
return;
|
||||
if (user->NumInOperands() < 2) return;
|
||||
const auto member = ConstantValue(irContext, user->GetSingleWordInOperand(1));
|
||||
if (!member || *member >= written.size()) {
|
||||
dynamicMember = true;
|
||||
return;
|
||||
}
|
||||
if (IsStoredThrough(irContext, user)) written[*member] = true;
|
||||
});
|
||||
if (dynamicMember) return Status::SuccessWithoutChange;
|
||||
for (uint32_t member = 0; member < written.size(); ++member) {
|
||||
if (!written[member]) continue;
|
||||
const uint32_t memberTypeId = valueType->GetSingleWordInOperand(member);
|
||||
const auto scalars = ScalarCount(irContext, memberTypeId);
|
||||
if (!scalars) return Status::SuccessWithoutChange;
|
||||
snapshotScalars += uint64_t(*scalars) * maxVertices;
|
||||
slots.push_back({global.result_id(), Int64(member), memberTypeId});
|
||||
}
|
||||
}
|
||||
if (snapshotScalars > kMaxSnapshotScalars) return Status::SuccessWithoutChange;
|
||||
|
||||
// ---- Types, constants and Private globals, all before any code moves ----------
|
||||
auto* typeMgr = irContext->get_type_mgr();
|
||||
auto* constMgr = irContext->get_constant_mgr();
|
||||
const uint32_t uintTypeId = typeMgr->GetUIntTypeId();
|
||||
const uint32_t boolTypeId = typeMgr->GetBoolTypeId();
|
||||
const uint32_t voidTypeId = typeMgr->GetVoidTypeId();
|
||||
const uint32_t zeroId = constMgr->GetUIntConstId(0);
|
||||
const uint32_t oneId = constMgr->GetUIntConstId(1);
|
||||
const uint32_t lengthId = constMgr->GetUIntConstId(maxVertices);
|
||||
const uint32_t lastId = constMgr->GetUIntConstId(maxVertices - 1);
|
||||
const uint32_t cutLengthId = constMgr->GetUIntConstId(maxVertices + 1);
|
||||
const uint32_t privateUintPointerId = typeMgr->FindPointerToType(uintTypeId, spv::StorageClass::Private);
|
||||
const uint32_t functionUintPointerId = typeMgr->FindPointerToType(uintTypeId, spv::StorageClass::Function);
|
||||
for (Slot& slot : slots) {
|
||||
slot.snapshotElementPointerTypeId =
|
||||
typeMgr->FindPointerToType(slot.valueTypeId, spv::StorageClass::Private);
|
||||
if (slot.member >= 0) {
|
||||
slot.memberIndexId = constMgr->GetUIntConstId(uint32_t(slot.member));
|
||||
slot.outputPointerTypeId = typeMgr->FindPointerToType(slot.valueTypeId, spv::StorageClass::Output);
|
||||
}
|
||||
}
|
||||
const uint32_t bodyFunctionTypeId = irContext->GetFunction(bodyFunctionId)->DefInst().GetSingleWordInOperand(1);
|
||||
|
||||
// Hand-built from here on; analyses are rebuilt once at the end.
|
||||
auto newPrivateArray = [&](uint32_t elementTypeId, uint32_t lengthConstantId) {
|
||||
const uint32_t arrayTypeId = irContext->TakeNextId();
|
||||
irContext->AddType(Make(irContext, spv::Op::OpTypeArray, 0, arrayTypeId,
|
||||
{Id(elementTypeId), Id(lengthConstantId)}));
|
||||
const uint32_t pointerTypeId = irContext->TakeNextId();
|
||||
irContext->AddType(Make(irContext, spv::Op::OpTypePointer, 0, pointerTypeId,
|
||||
{{SPV_OPERAND_TYPE_STORAGE_CLASS, {uint32_t(spv::StorageClass::Private)}},
|
||||
Id(arrayTypeId)}));
|
||||
const uint32_t variableId = irContext->TakeNextId();
|
||||
irContext->AddGlobalValue(Make(irContext, spv::Op::OpVariable, pointerTypeId, variableId,
|
||||
{{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{uint32_t(spv::StorageClass::Private)}}}));
|
||||
return variableId;
|
||||
};
|
||||
for (Slot& slot : slots) slot.snapshotVariableId = newPrivateArray(slot.valueTypeId, lengthId);
|
||||
// One slot past max_vertices: where an EndPrimitive with no vertex before it lands.
|
||||
const uint32_t cutVariableId = newPrivateArray(uintTypeId, cutLengthId);
|
||||
const uint32_t countVariableId = irContext->TakeNextId();
|
||||
irContext->AddGlobalValue(Make(irContext, spv::Op::OpVariable, privateUintPointerId, countVariableId,
|
||||
{{SPV_OPERAND_TYPE_STORAGE_CLASS,
|
||||
{uint32_t(spv::StorageClass::Private)}}}));
|
||||
ReplaceName(irContext, countVariableId, "mg_deferredVertexCount");
|
||||
ReplaceName(irContext, cutVariableId, "mg_deferredStripEnd");
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
|
||||
// Emits a value-producing instruction before |at| and returns its id.
|
||||
auto before = [&](Instruction* at, spv::Op op, uint32_t typeId, OperandList operands) {
|
||||
const uint32_t id = typeId != 0 ? irContext->TakeNextId() : 0;
|
||||
at->InsertBefore(Make(irContext, op, typeId, id, std::move(operands)));
|
||||
return id;
|
||||
};
|
||||
auto outputPointer = [&](Instruction* at, const Slot& slot) {
|
||||
if (slot.member < 0) return slot.variableId;
|
||||
return before(at, spv::Op::OpAccessChain, slot.outputPointerTypeId,
|
||||
{Id(slot.variableId), Id(slot.memberIndexId)});
|
||||
};
|
||||
|
||||
// ---- Each EmitVertex records; each EndPrimitive marks ---------------------------
|
||||
for (Instruction* emit : emits) {
|
||||
const uint32_t count = before(emit, spv::Op::OpLoad, uintTypeId, {Id(countVariableId)});
|
||||
const uint32_t inRange = before(emit, spv::Op::OpULessThan, boolTypeId, {Id(count), Id(lengthId)});
|
||||
const uint32_t index = before(emit, spv::Op::OpSelect, uintTypeId, {Id(inRange), Id(count), Id(lastId)});
|
||||
for (const Slot& slot : slots) {
|
||||
const uint32_t source = outputPointer(emit, slot);
|
||||
const uint32_t value = before(emit, spv::Op::OpLoad, slot.valueTypeId, {Id(source)});
|
||||
const uint32_t target = before(emit, spv::Op::OpAccessChain, slot.snapshotElementPointerTypeId,
|
||||
{Id(slot.snapshotVariableId), Id(index)});
|
||||
before(emit, spv::Op::OpStore, 0, {Id(target), Id(value)});
|
||||
}
|
||||
const uint32_t cut = before(emit, spv::Op::OpAccessChain, privateUintPointerId,
|
||||
{Id(cutVariableId), Id(index)});
|
||||
before(emit, spv::Op::OpStore, 0, {Id(cut), Id(zeroId)});
|
||||
const uint32_t next = before(emit, spv::Op::OpIAdd, uintTypeId, {Id(count), Id(oneId)});
|
||||
before(emit, spv::Op::OpStore, 0, {Id(countVariableId), Id(next)});
|
||||
}
|
||||
for (Instruction* end : cuts) {
|
||||
const uint32_t count = before(end, spv::Op::OpLoad, uintTypeId, {Id(countVariableId)});
|
||||
const uint32_t any = before(end, spv::Op::OpINotEqual, boolTypeId, {Id(count), Id(zeroId)});
|
||||
const uint32_t previous = before(end, spv::Op::OpISub, uintTypeId, {Id(count), Id(oneId)});
|
||||
const uint32_t inRange = before(end, spv::Op::OpULessThan, boolTypeId, {Id(previous), Id(lengthId)});
|
||||
const uint32_t clamped =
|
||||
before(end, spv::Op::OpSelect, uintTypeId, {Id(inRange), Id(previous), Id(lastId)});
|
||||
const uint32_t index = before(end, spv::Op::OpSelect, uintTypeId, {Id(any), Id(clamped), Id(lengthId)});
|
||||
const uint32_t cut = before(end, spv::Op::OpAccessChain, privateUintPointerId,
|
||||
{Id(cutVariableId), Id(index)});
|
||||
before(end, spv::Op::OpStore, 0, {Id(cut), Id(oneId)});
|
||||
}
|
||||
for (Instruction* inst : emits) irContext->KillInst(inst);
|
||||
for (Instruction* inst : cuts) irContext->KillInst(inst);
|
||||
|
||||
// ---- The new entry point: run the body, then replay -----------------------------
|
||||
const uint32_t entryId = irContext->TakeNextId();
|
||||
auto function = spvtools::MakeUnique<Function>(
|
||||
Make(irContext, spv::Op::OpFunction, voidTypeId, entryId,
|
||||
{{SPV_OPERAND_TYPE_FUNCTION_CONTROL, {0}}, Id(bodyFunctionTypeId)}));
|
||||
Function* entry = function.get();
|
||||
const uint32_t startLabel = irContext->TakeNextId();
|
||||
const uint32_t headerLabel = irContext->TakeNextId();
|
||||
const uint32_t checkLabel = irContext->TakeNextId();
|
||||
const uint32_t replayLabel = irContext->TakeNextId();
|
||||
const uint32_t endStripLabel = irContext->TakeNextId();
|
||||
const uint32_t afterStripLabel = irContext->TakeNextId();
|
||||
const uint32_t continueLabel = irContext->TakeNextId();
|
||||
const uint32_t mergeLabel = irContext->TakeNextId();
|
||||
auto newBlock = [&](uint32_t label) {
|
||||
auto block = spvtools::MakeUnique<BasicBlock>(Make(irContext, spv::Op::OpLabel, 0, label, {}));
|
||||
block->SetParent(entry);
|
||||
return block;
|
||||
};
|
||||
auto add = [&](BasicBlock& block, spv::Op op, uint32_t typeId, OperandList operands) {
|
||||
const uint32_t id = typeId != 0 ? irContext->TakeNextId() : 0;
|
||||
block.AddInstruction(Make(irContext, op, typeId, id, std::move(operands)));
|
||||
return id;
|
||||
};
|
||||
|
||||
auto start = newBlock(startLabel);
|
||||
const uint32_t iterator = add(*start, spv::Op::OpVariable, functionUintPointerId,
|
||||
{{SPV_OPERAND_TYPE_STORAGE_CLASS, {uint32_t(spv::StorageClass::Function)}}});
|
||||
add(*start, spv::Op::OpStore, 0, {Id(countVariableId), Id(zeroId)});
|
||||
add(*start, spv::Op::OpFunctionCall, voidTypeId, {Id(bodyFunctionId)});
|
||||
const uint32_t recorded = add(*start, spv::Op::OpLoad, uintTypeId, {Id(countVariableId)});
|
||||
const uint32_t fits = add(*start, spv::Op::OpULessThan, boolTypeId, {Id(recorded), Id(lengthId)});
|
||||
const uint32_t limit = add(*start, spv::Op::OpSelect, uintTypeId, {Id(fits), Id(recorded), Id(lengthId)});
|
||||
add(*start, spv::Op::OpStore, 0, {Id(iterator), Id(zeroId)});
|
||||
add(*start, spv::Op::OpBranch, 0, {Id(headerLabel)});
|
||||
|
||||
auto header = newBlock(headerLabel);
|
||||
add(*header, spv::Op::OpLoopMerge, 0,
|
||||
{Id(mergeLabel), Id(continueLabel), {SPV_OPERAND_TYPE_LOOP_CONTROL, {0}}});
|
||||
add(*header, spv::Op::OpBranch, 0, {Id(checkLabel)});
|
||||
|
||||
auto check = newBlock(checkLabel);
|
||||
const uint32_t vertex = add(*check, spv::Op::OpLoad, uintTypeId, {Id(iterator)});
|
||||
const uint32_t more = add(*check, spv::Op::OpULessThan, boolTypeId, {Id(vertex), Id(limit)});
|
||||
add(*check, spv::Op::OpBranchConditional, 0, {Id(more), Id(replayLabel), Id(mergeLabel)});
|
||||
|
||||
auto replay = newBlock(replayLabel);
|
||||
for (const Slot& slot : slots) {
|
||||
const uint32_t source = add(*replay, spv::Op::OpAccessChain, slot.snapshotElementPointerTypeId,
|
||||
{Id(slot.snapshotVariableId), Id(vertex)});
|
||||
const uint32_t value = add(*replay, spv::Op::OpLoad, slot.valueTypeId, {Id(source)});
|
||||
uint32_t target = slot.variableId;
|
||||
if (slot.member >= 0) {
|
||||
target = add(*replay, spv::Op::OpAccessChain, slot.outputPointerTypeId,
|
||||
{Id(slot.variableId), Id(slot.memberIndexId)});
|
||||
}
|
||||
add(*replay, spv::Op::OpStore, 0, {Id(target), Id(value)});
|
||||
}
|
||||
add(*replay, spv::Op::OpEmitVertex, 0, {});
|
||||
const uint32_t cutPointer =
|
||||
add(*replay, spv::Op::OpAccessChain, privateUintPointerId, {Id(cutVariableId), Id(vertex)});
|
||||
const uint32_t cutValue = add(*replay, spv::Op::OpLoad, uintTypeId, {Id(cutPointer)});
|
||||
const uint32_t endsStrip = add(*replay, spv::Op::OpINotEqual, boolTypeId, {Id(cutValue), Id(zeroId)});
|
||||
add(*replay, spv::Op::OpSelectionMerge, 0,
|
||||
{Id(afterStripLabel), {SPV_OPERAND_TYPE_SELECTION_CONTROL, {0}}});
|
||||
add(*replay, spv::Op::OpBranchConditional, 0, {Id(endsStrip), Id(endStripLabel), Id(afterStripLabel)});
|
||||
|
||||
auto endStrip = newBlock(endStripLabel);
|
||||
add(*endStrip, spv::Op::OpEndPrimitive, 0, {});
|
||||
add(*endStrip, spv::Op::OpBranch, 0, {Id(afterStripLabel)});
|
||||
|
||||
auto afterStrip = newBlock(afterStripLabel);
|
||||
add(*afterStrip, spv::Op::OpBranch, 0, {Id(continueLabel)});
|
||||
|
||||
auto continueBlock = newBlock(continueLabel);
|
||||
const uint32_t current = add(*continueBlock, spv::Op::OpLoad, uintTypeId, {Id(iterator)});
|
||||
const uint32_t next = add(*continueBlock, spv::Op::OpIAdd, uintTypeId, {Id(current), Id(oneId)});
|
||||
add(*continueBlock, spv::Op::OpStore, 0, {Id(iterator), Id(next)});
|
||||
add(*continueBlock, spv::Op::OpBranch, 0, {Id(headerLabel)});
|
||||
|
||||
auto merge = newBlock(mergeLabel);
|
||||
add(*merge, spv::Op::OpReturn, 0, {});
|
||||
|
||||
entry->AddBasicBlock(std::move(start));
|
||||
entry->AddBasicBlock(std::move(header));
|
||||
entry->AddBasicBlock(std::move(check));
|
||||
entry->AddBasicBlock(std::move(replay));
|
||||
entry->AddBasicBlock(std::move(endStrip));
|
||||
entry->AddBasicBlock(std::move(afterStrip));
|
||||
entry->AddBasicBlock(std::move(continueBlock));
|
||||
entry->AddBasicBlock(std::move(merge));
|
||||
entry->SetFunctionEnd(Make(irContext, spv::Op::OpFunctionEnd, 0, 0, {}));
|
||||
irContext->AddFunction(std::move(function));
|
||||
|
||||
// The entry point and its execution modes now name the new function.
|
||||
entryPoint->SetInOperand(1, {entryId});
|
||||
for (Instruction& mode : irContext->module()->execution_modes()) {
|
||||
if (mode.GetSingleWordInOperand(0) == bodyFunctionId) mode.SetInOperand(0, {entryId});
|
||||
}
|
||||
ReplaceName(irContext, bodyFunctionId, "mg_geometryBody");
|
||||
ReplaceName(irContext, entryId, "main");
|
||||
|
||||
irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
if (m_outDeferred != nullptr) *m_outDeferred = true;
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken DeferGeometryEmitsPass::CreateDeferGeometryEmitsPass(Bool* outDeferred) {
|
||||
return spvtools::Optimizer::PassToken(spvtools::MakeUnique<DeferGeometryEmitsPass>(outDeferred));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,57 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.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 {
|
||||
// A geometry shader's memory writes, issued after its EmitVertex() calls, moved IN
|
||||
// FRONT of every emit - for an ES driver that silently drops shader storage writes a
|
||||
// geometry invocation makes after emitting (SelfTest::GeometryStageSsboWriteAfterEmitDropped,
|
||||
// measured on Adreno; KHR-GL4x.shader_storage_buffer_object.basic-atomic-case1/3 is the
|
||||
// CTS shape: EmitVertex() first, then a chain of atomics on a geometry-stage buffer).
|
||||
//
|
||||
// The emits are DEFERRED rather than the writes hoisted, because only the emits can be
|
||||
// moved without changing what the shader computes: an emit reads nothing but the
|
||||
// output variables' values at that moment, and those are snapshotted.
|
||||
// * The original entry function becomes an ordinary function (mg_geometryBody), and a
|
||||
// new entry point calls it and then replays the vertices - so a `return` anywhere
|
||||
// in the original body, however deep, still ends up at the replay.
|
||||
// * Each OpEmitVertex becomes a copy of every output the shader writes into a Private
|
||||
// array sized by the OutputVertices execution mode (max_vertices), plus a bump of
|
||||
// the vertex count; each OpEndPrimitive marks the last recorded vertex as the end
|
||||
// of a strip. Both are straight-line code - no new control flow inside the body.
|
||||
// * The replay loop restores each vertex's outputs, emits it, and ends the strip
|
||||
// where the body did.
|
||||
// Only outputs the shader STORES to are carried (a block member it never writes, such
|
||||
// as gl_ClipDistance, would otherwise become an active builtin the ESSL stage cannot
|
||||
// declare). Emits past max_vertices are undefined in GL; they overwrite the last slot.
|
||||
//
|
||||
// Declines (leaves the module untouched) when: the module is not a geometry stage, it
|
||||
// has no emit or no memory write at all, it uses vertex streams, a block output is
|
||||
// indexed dynamically at its top level, or the snapshot would exceed a per-invocation
|
||||
// budget. Only for the DirectGLES transpile path on a driver the POST measured.
|
||||
class DeferGeometryEmitsPass : public spvtools::opt::Pass {
|
||||
public:
|
||||
explicit DeferGeometryEmitsPass(Bool* outDeferred = nullptr) : m_outDeferred(outDeferred) {}
|
||||
const char* name() const override { return "defer-geometry-emits"; }
|
||||
Status Process() override;
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateDeferGeometryEmitsPass(Bool* outDeferred = nullptr);
|
||||
|
||||
private:
|
||||
Bool* m_outDeferred = nullptr;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -181,6 +181,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
AppendCommonKeyPrefix(builder, kEsslKeyTag);
|
||||
builder.Value(static_cast<Uint32>(inputs.shaderType));
|
||||
builder.Value(static_cast<Uint8>(inputs.viewportIndexLoweringArmed));
|
||||
builder.Value(static_cast<Uint8>(inputs.deferGeometryEmitsArmed));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsNoperspectiveInterpolation));
|
||||
builder.Value(static_cast<Uint8>(inputs.supportsExtendedImageFormats));
|
||||
builder.Value(inputs.maxColorTextureSamples);
|
||||
|
||||
@@ -643,6 +643,10 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// decides, and a key carrying only it would serve a module lowered under one setting to a
|
||||
// link made under the other.
|
||||
Bool viewportIndexLoweringArmed = false;
|
||||
// Whether DeferGeometryEmitsForEssl runs on this (geometry) module: the driver POST
|
||||
// measured dropped storage writes after EmitVertex, or MOBILEGL_ESPRYT_DEFER_GEOMETRY_EMITS
|
||||
// forced it.
|
||||
Bool deferGeometryEmitsArmed = false;
|
||||
Bool supportsNoperspectiveInterpolation = false;
|
||||
// GL_NV_image_formats. Arms WidenImageFormatsForEssl, which re-declares every storage
|
||||
// image whose format GLSL ES core cannot spell in the core format that carries it and
|
||||
|
||||
Reference in New Issue
Block a user