From cfbaffd7579926b18f53729806dc4dae5e979d49 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 12 Oct 2026 23:57:48 -0400 Subject: [PATCH] [Fix] (Espryt): defer geometry emits behind memory writes on drivers that drop them --- CMakeLists.txt | 1 + MobileGL/Config.h | 7 + MobileGL/ConfigLoader.cpp | 1 + MobileGL/MG_Backend/DirectGLES/Managers.cpp | 20 + .../MG_Test/ShaderTranspiler/CMakeLists.txt | 1 + .../DeferGeometryEmitsTest.cpp | 150 ++++++ .../ShaderTranspiler/TranslationCacheTest.cpp | 5 + .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 17 + .../MG_Util/BackendLoaders/OpenGL/Loader.h | 5 + MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp | 22 +- .../ShaderTranspiler/ShaderCompiler.cpp | 14 + .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 6 + .../SpirvPasses/DeferGeometryEmitsPass.cpp | 510 ++++++++++++++++++ .../SpirvPasses/DeferGeometryEmitsPass.h | 57 ++ .../ShaderTranspiler/TranslationCache.cpp | 1 + .../ShaderTranspiler/TranslationCache.h | 4 + 16 files changed, 814 insertions(+), 7 deletions(-) create mode 100644 MobileGL/MG_Test/ShaderTranspiler/DeferGeometryEmitsTest.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 41be723c..9208d01b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 097c6128..8b8a99e1 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -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 diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index 05684b1d..2b529975 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -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"); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 3fd530ed..27921f4d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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 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 = diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index b6a08124..e60a0591 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable( UniquifyIoBlockNamesTest.cpp StripIoBlockLocationsTest.cpp LowerViewportIndexTest.cpp + DeferGeometryEmitsTest.cpp ClampMultisampleFetchTest.cpp LegalizeResourceArrayIndexTest.cpp FlattenAtomicCounterBlockTest.cpp diff --git a/MobileGL/MG_Test/ShaderTranspiler/DeferGeometryEmitsTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/DeferGeometryEmitsTest.cpp new file mode 100644 index 00000000..72c98db7 --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/DeferGeometryEmitsTest.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 + +#include +#include + +#include "Includes.h" +#include "Init.h" +#include +#include +#include + +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; +using MobileGL::MG_Util::ShaderTranspiler::SpvcSession; + +namespace { + Vector 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& 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& 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 spirv = CompileGeometry(kWritesAfterEmit); + ASSERT_FALSE(spirv.empty()); + Vector 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 spirv = CompileGeometry(kStripWithCut); + ASSERT_FALSE(spirv.empty()); + Vector 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 spirv = CompileGeometry(kNoMemoryWrites); + ASSERT_FALSE(spirv.empty()); + Vector out; + bool deferred = true; + ASSERT_TRUE(ShaderCompiler::DeferGeometryEmitsForEssl(spirv, out, deferred, true)); + EXPECT_FALSE(deferred); +} diff --git a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp index 26707ae6..27044448 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp @@ -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; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 40b04a98..c8115a23 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -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; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index 720c6dc5..2c55251f 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -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 diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index c864031c..9beb23fb 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -2275,15 +2275,23 @@ namespace MobileGL::MG_Util::SelfTest { Optional 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 ProbeR32FMultisampleSwizzleBug(const GLESFunctionsTable& gl) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index b4ae0ef7..8c1728aa 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -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& inputBinary, + Vector& 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& binary) { return LowerViewportIndexPass::DeclaresViewportIndexBuiltin(binary); } diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index b239c365..c96a0761 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -67,6 +67,12 @@ namespace MobileGL { static bool LowerViewportIndexForEssl(const Vector& inputBinary, Vector& 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& inputBinary, + Vector& 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. diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.cpp new file mode 100644 index 00000000..5e98c0a1 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.cpp @@ -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 +#include +#include + +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; + + // 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 Make(IRContext* context, spv::Op op, uint32_t typeId, uint32_t resultId, + OperandList operands) { + return spvtools::MakeUnique(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 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 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(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(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(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(mode.GetSingleWordInOperand(1)) == + spv::ExecutionMode::OutputVertices) { + maxVertices = mode.GetSingleWordInOperand(2); + } + } + if (maxVertices == 0) return Status::SuccessWithoutChange; + + std::vector emits; + std::vector 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 slots; + uint64_t snapshotScalars = 0; + for (Instruction& global : irContext->types_values()) { + if (global.opcode() != spv::Op::OpVariable || + static_cast(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 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( + 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(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(outDeferred)); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.h new file mode 100644 index 00000000..d0240c5c --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeferGeometryEmitsPass.h @@ -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 + +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 diff --git a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp index bc7af4a9..f18ed561 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp @@ -181,6 +181,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler { AppendCommonKeyPrefix(builder, kEsslKeyTag); builder.Value(static_cast(inputs.shaderType)); builder.Value(static_cast(inputs.viewportIndexLoweringArmed)); + builder.Value(static_cast(inputs.deferGeometryEmitsArmed)); builder.Value(static_cast(inputs.supportsNoperspectiveInterpolation)); builder.Value(static_cast(inputs.supportsExtendedImageFormats)); builder.Value(inputs.maxColorTextureSamples); diff --git a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h index dda3ea67..33199f03 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h +++ b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h @@ -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