From cdf43a4a59ca795c4883108b3c9f8ec9d2f15bb3 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sat, 7 Nov 2026 02:47:30 -0400 Subject: [PATCH] [Fix] (MG_Backend/DirectVulkan, MG_Util/ShaderTranspiler): apply GL_CLIP_DISTANCEi in the shader where Magma clips natively --- MobileGL/BackendCaps.h | 8 +++ .../BackendObject_DirectVulkan.cpp | 9 ++- .../Harness/strict-known-limitations.md | 10 +-- .../Scenarios/ClipDistanceScenario.cpp | 13 ++-- MobileGL/MG_Pipe/PipeFields.def | 2 +- MobileGL/MG_Pipe/generated/PipeWireLayout.inc | 1 + .../GLState/ProgramState/ProgramSpirvTask.cpp | 20 ++++-- .../GLState/ProgramState/ProgramSpirvTask.h | 2 +- MobileGL/MG_Test/SanityTest.cpp | 4 ++ .../EmulateClipDistanceTest.cpp | 71 +++++++++++++++++++ MobileGL/MG_Util/SelfTest/DriverPost.cpp | 5 +- .../MG_Util/ShaderTranspiler/CompileEnv.cpp | 1 + .../MG_Util/ShaderTranspiler/CompileEnv.h | 6 ++ .../ShaderTranspiler/ShaderCompiler.cpp | 8 ++- .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 7 +- .../ShaderSourceProcessor.cpp | 2 +- .../SpirvPasses/EmulateClipDistancePass.cpp | 43 ++++++++++- .../SpirvPasses/EmulateClipDistancePass.h | 12 ++++ 18 files changed, 198 insertions(+), 26 deletions(-) diff --git a/MobileGL/BackendCaps.h b/MobileGL/BackendCaps.h index 3d1e2ae7..0d1a8902 100644 --- a/MobileGL/BackendCaps.h +++ b/MobileGL/BackendCaps.h @@ -358,6 +358,14 @@ namespace MobileGL { // overrides the detection in either direction at backend init. Defaults FALSE: true // ARMS a rewrite, so the no-backend answer is the one that leaves modules untouched. Bool EmulatesClipDistance = false; + // The backend HOSTS gl_ClipDistance but has no per-distance enable state: Vulkan + // clips by every distance a shader writes, so GL_CLIP_DISTANCEi would mean nothing. + // The producing stage then writes `enabled(i) ? d[i] : 1.0` back into the built-in + // (EmulateClipDistanceOptions::nativeBuiltin) from the same reserved enables uniform + // the emulation uses. DirectVulkan sets it whenever it clips natively; DirectGLES + // never does - ES drivers it keeps native passed the POST probe, which checks the + // driver honors GL_CLIP_DISTANCEi_EXT itself. Exclusive with EmulatesClipDistance. + Bool MasksNativeClipDistances = false; Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024; Uint32 SubgroupSize = 0; Uint32 SubgroupSupportedStages = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 1c574ecd..0093c0b8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -1366,11 +1366,18 @@ namespace MobileGL::MG_Backend::DirectVulkan { break; } m_dynamicParameters.EmulatesClipDistance = emulateClipDistance; + // Hosted natively, but Vulkan has no per-distance enable state: apply GL_CLIP_DISTANCEi + // in the producing stage instead (EmulateClipDistanceOptions::nativeBuiltin). + m_dynamicParameters.MasksNativeClipDistances = !emulateClipDistance && m_vulkanCaps.SupportsShaderClipDistance; m_dynamicParameters.MaxClipDistances = emulateClipDistance ? 8 : (m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0); MGLOG_I("DirectVulkan: gl_ClipDistance %s (GL_MAX_CLIP_DISTANCES = %d)", - emulateClipDistance ? "EMULATED (varyings + fragment discard)" : "native", m_dynamicParameters.MaxClipDistances); + emulateClipDistance ? "EMULATED (varyings + fragment discard)" + : (m_dynamicParameters.MasksNativeClipDistances + ? "native, GL_CLIP_DISTANCEi applied in the shader" + : "unsupported"), + m_dynamicParameters.MaxClipDistances); // The cull pair, gated on its own feature. shaderCullDistance is separate from // shaderClipDistance and VulkanRenderer enables it independently, so it gets its own // gate rather than riding on the clip one. diff --git a/MobileGL/MG_IntegrationTest/Harness/strict-known-limitations.md b/MobileGL/MG_IntegrationTest/Harness/strict-known-limitations.md index 9332e156..be4b03b8 100644 --- a/MobileGL/MG_IntegrationTest/Harness/strict-known-limitations.md +++ b/MobileGL/MG_IntegrationTest/Harness/strict-known-limitations.md @@ -7,11 +7,11 @@ Configuration-specific controls are selected separately and must pass without sk ## Unresolved behavior - `ClipDistanceScenario.ADisabledClipDistanceRemovesNothing` and - `TheEnablesAreIndependentPerDistance`: Magma's NATIVE clip distances (lavapipe, - Adreno) still ignore the GL_CLIP_DISTANCEi enables - Vulkan has no such state. - Every emulating backend has them (EmulateClipDistancePass); Espryt on llvmpipe - now emulates, because its native probe catches llvmpipe clipping by disabled - distances, so the GLES entries pass wherever the emulation is armed. + `TheEnablesAreIndependentPerDistance`: resolved on both backends. Espryt on + llvmpipe emulates clip distances (its native probe catches llvmpipe clipping by + disabled distances); Magma on lavapipe applies GL_CLIP_DISTANCEi in the shader + (Vulkan has no per-distance enable state). The entries stay listed only so a + stack that ever skips them again keeps a documented reason. - `PartialUploadOverGpuContentScenario.OneDimensionalArraySubImageKeepsTheGpuClearedLayer`: the Vulkan path cannot attach the 1D array layer. GLES passes, which does not establish Vulkan coverage. diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.cpp index d577a6cd..91652dcb 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/ClipDistanceScenario.cpp @@ -31,9 +31,9 @@ // that lane Espryt EMULATES clip distances (EmulateClipDistancePass: varyings, a fragment // discard and a per-draw enables uniform), and the per-distance cases below run and pass // there. That is the emulation every Mali device takes. lavapipe hosts shaderClipDistance, so -// Magma clips natively, Vulkan has no per-distance enable state, and the two per-distance cases -// still skip on that lane (MOBILEGL_CLIP_DISTANCE_EMULATION=1 runs them green through the -// emulation). The native ES path - Adreno, which honors GL_CLIP_DISTANCEi_EXT - is pinned on +// Magma clips natively; Vulkan has no per-distance enable state, so Magma applies GL_CLIP_DISTANCEi in +// the producing stage (BackendCaps::MasksNativeClipDistances) and the per-distance cases run +// green there too. The native ES path - Adreno, which honors GL_CLIP_DISTANCEi_EXT - is pinned on // the device, through KHR-GLxx.clip_distance.functional. // // Every case disables all eight distances on entry rather than assuming they start off: @@ -200,9 +200,10 @@ void main() { // EmulateClipDistancePass gives those semantics to every backend that EMULATES clip // distances (the disabled slots are forced to +1 from a per-draw uniform, so nothing // joins the pipeline key), and the DirectGLES loader arms that emulation on a driver - // whose native clip distances ignore the enables. Magma's native path does not apply - // the enables yet. The cases that need the real semantics gate on this probe and say - // so when they skip, rather than being deleted or silently weakened. + // whose native clip distances ignore the enables. Magma's native path applies them in + // the producing stage (MasksNativeClipDistances). The cases that need the real + // semantics still gate on this probe and say so if a stack ever skips, rather than + // being deleted or silently weakened. bool DriverHonorsPerDistanceEnables(GLuint program, GLuint vao) const { for (int i = 0; i < 8; ++i) { glDisable(static_cast(GL_CLIP_DISTANCE0 + i)); diff --git a/MobileGL/MG_Pipe/PipeFields.def b/MobileGL/MG_Pipe/PipeFields.def index 0738227c..1d84211b 100644 --- a/MobileGL/MG_Pipe/PipeFields.def +++ b/MobileGL/MG_Pipe/PipeFields.def @@ -389,7 +389,7 @@ F(FragmentInterpolationOffsetBits) F(SupportsWideLines) \ F(SupportsDistinctDepthStencilAttachments) F(PerLayerFramebufferAttachmentTargets) \ F(SupportsShaderFloat64) F(SupportsFloat64VertexAttributes) F(SupportsTessellationPointSize) \ - F(SupportsGeometryPointSize) F(EmulatesClipDistance) F(MaxShaderStorageBlockSize) F(SubgroupSize) \ + F(SupportsGeometryPointSize) F(EmulatesClipDistance) F(MasksNativeClipDistances) F(MaxShaderStorageBlockSize) F(SubgroupSize) \ F(SubgroupSupportedStages) F(SubgroupSupportedFeatures) F(SamplePositions) \ F(SamplePositionCountMask) F(SubgroupQuadOperationsInAllStages) \ F(GpuVendor) F(SupportsFragmentShadingRate) F(SupportsPrimitiveFragmentShadingRate) \ diff --git a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc index 4bdc884a..ed45c24c 100644 --- a/MobileGL/MG_Pipe/generated/PipeWireLayout.inc +++ b/MobileGL/MG_Pipe/generated/PipeWireLayout.inc @@ -661,6 +661,7 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = { {"DynamicBackendParameters.SupportsTessellationPointSize", offsetof(DynamicBackendParameters, SupportsTessellationPointSize), sizeof(((DynamicBackendParameters*)nullptr)->SupportsTessellationPointSize)}, {"DynamicBackendParameters.SupportsGeometryPointSize", offsetof(DynamicBackendParameters, SupportsGeometryPointSize), sizeof(((DynamicBackendParameters*)nullptr)->SupportsGeometryPointSize)}, {"DynamicBackendParameters.EmulatesClipDistance", offsetof(DynamicBackendParameters, EmulatesClipDistance), sizeof(((DynamicBackendParameters*)nullptr)->EmulatesClipDistance)}, + {"DynamicBackendParameters.MasksNativeClipDistances", offsetof(DynamicBackendParameters, MasksNativeClipDistances), sizeof(((DynamicBackendParameters*)nullptr)->MasksNativeClipDistances)}, {"DynamicBackendParameters.MaxShaderStorageBlockSize", offsetof(DynamicBackendParameters, MaxShaderStorageBlockSize), sizeof(((DynamicBackendParameters*)nullptr)->MaxShaderStorageBlockSize)}, {"DynamicBackendParameters.SubgroupSize", offsetof(DynamicBackendParameters, SubgroupSize), sizeof(((DynamicBackendParameters*)nullptr)->SubgroupSize)}, {"DynamicBackendParameters.SubgroupSupportedStages", offsetof(DynamicBackendParameters, SubgroupSupportedStages), sizeof(((DynamicBackendParameters*)nullptr)->SubgroupSupportedStages)}, diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp index 188cd916..02630916 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp @@ -162,8 +162,11 @@ namespace MobileGL::MG_State::GLState { m_phaseA->in.env != nullptr && m_phaseA->in.env->DemotesGeometryPointSize(); // Part of the frontend fingerprint (CompileEnv), so the L1 key already separates the two. const Bool emulateClipDistance = m_phaseA->in.env != nullptr && m_phaseA->in.env->EmulatesClipDistance(); + const Bool maskNativeClipDistance = + m_phaseA->in.env != nullptr && m_phaseA->in.env->MasksNativeClipDistances(); GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation, - nativeFloat64, demoteTessellationPointSize, demoteGeometryPointSize, emulateClipDistance); + nativeFloat64, demoteTessellationPointSize, demoteGeometryPointSize, emulateClipDistance, + maskNativeClipDistance); // GlslangToSpv was the only consumer of the parsed ASTs; everything after this point // works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop // them here rather than at the end of the body, which is ~87% of this node's runtime @@ -223,7 +226,7 @@ namespace MobileGL::MG_State::GLState { const Bool deferOutputValidationForDirectVulkan, const Bool enableSpirvValidation, const Bool nativeFloat64, const Bool demoteTessellationPointSize, const Bool demoteGeometryPointSize, - const Bool emulateClipDistance) { + const Bool emulateClipDistance, const Bool maskNativeClipDistance) { /* As we passed first stage compilation/linking, * we'll assume all the operations here should * pass. We may be able to employ some optimizations @@ -285,16 +288,25 @@ namespace MobileGL::MG_State::GLState { // to clipping by every distance. Running first adds the read that keeps it alive. The // point-size demotion below places its carrier past these carriers. artifacts.clipDistanceEmulated = false; - if (emulateClipDistance) { + if (emulateClipDistance || maskNativeClipDistance) { ShaderCompiler::ClipDistanceEmulationOutcome outcome; if (!ShaderCompiler::EmulateClipDistanceForProgram( artifacts.generatedSpirv, handoff.shaderTypes, handoff.captureRequestsClipDistance, kClipDistanceCarrierLocationBudget, outcome, !deferOutputValidationForDirectVulkan, - enableSpirvValidation)) { + enableSpirvValidation, maskNativeClipDistance)) { DeferLog(std::format("ProgramObject {}: clip-distance emulation failed in the optimizer; the " "program keeps gl_ClipDistance, which this backend cannot host", externalIndex), FirstTimeWarnLevel(g_clipDistanceOptimizerFailureReported)); + } else if (outcome.emulated && maskNativeClipDistance) { + // Native masking: the built-in is still hosted, only the enables are applied in + // the shader. Not "emulated" for the artifact (nothing reads it differently). + const Bool first = !g_clipDistanceEmulationReported.test_and_set(std::memory_order_relaxed); + DeferLog(std::format("ProgramObject {}: GL_CLIP_DISTANCEi enables applied in the shader to {} " + "native gl_ClipDistance output(s){}", + externalIndex, outcome.distanceCount, + outcome.enablesFound ? "" : "; no enables uniform, every distance clips"), + first ? MOBILEGL_LOG_LEVEL_INFO : MOBILEGL_LOG_LEVEL_DEBUG); } else if (outcome.emulated) { artifacts.clipDistanceEmulated = true; // THE ARMING SIGNAL, INFO and latched: the integration lane that pins diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h index 8f041f6b..11334e30 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h @@ -68,7 +68,7 @@ namespace MobileGL::MG_State::GLState { void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex, Bool deferOutputValidationForDirectVulkan, Bool enableSpirvValidation, Bool nativeFloat64, Bool demoteTessellationPointSize, Bool demoteGeometryPointSize, - Bool emulateClipDistance); + Bool emulateClipDistance, Bool maskNativeClipDistance); void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex); // Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 6e3425e9..2b3a70a9 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -715,10 +715,14 @@ TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) { EXPECT_TRUE(backend.GetDynamicParameters().EmulatesClipDistance); EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); + EXPECT_FALSE(backend.GetDynamicParameters().MasksNativeClipDistances); + caps.SupportsShaderClipDistance = true; caps.MaxClipDistances = 8; backend.ApplyVulkanCapabilitiesForTesting(caps); EXPECT_FALSE(backend.GetDynamicParameters().EmulatesClipDistance); + // Vulkan has no per-distance enable state, so the native path masks in the shader. + EXPECT_TRUE(backend.GetDynamicParameters().MasksNativeClipDistances); EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); } diff --git a/MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp index a507a526..e23929e1 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp @@ -339,6 +339,69 @@ void main() { EXPECT_TRUE(Contains(Disassemble(modules[0]), "mg_ClipDistanceEnables")); } +// Native masking (Vulkan with shaderClipDistance): the producer keeps the built-in and the +// capability, writes `enabled(i) ? d[i] : 1.0` back into it, and nothing else changes - no +// carriers, no discard, the fragment stage byte-identical. +TEST(EmulateClipDistance, NativeMaskingWritesTheMaskedDistancesBackIntoTheBuiltIn) { + Vector types; + auto modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kEightPlaneVertex}, {GL_FRAGMENT_SHADER, kPlainFragment}}, + types); + ASSERT_EQ(modules.size(), 2u); + const auto fragmentBefore = modules[1]; + ShaderCompiler::ClipDistanceEmulationOutcome outcome; + ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true, + /*nativeMask=*/true)); + EXPECT_TRUE(outcome.emulated) << outcome.declineDetail; + EXPECT_EQ(outcome.distanceCount, 8u); + EXPECT_TRUE(outcome.enablesFound); + EXPECT_EQ(modules[1], fragmentBefore); + const String vertexRaw = Disassemble(modules[0]); + EXPECT_TRUE(Contains(vertexRaw, "OpCapability ClipDistance")); + EXPECT_TRUE(Contains(vertexRaw, "OpBitwiseAnd")); + EXPECT_FALSE(Contains(vertexRaw, "mg_ClipDistanceEmu0")); + SanitizeAll(modules); + for (const auto& module : modules) + EXPECT_TRUE(Validates(module)); + EXPECT_FALSE(Contains(Disassemble(modules[1]), "OpKill")); +} + +// Native masking leaves gl_in[] reads native: a geometry stage that reads the vertex stage's +// distances and re-emits them is the producer, and the vertex stage is untouched. +TEST(EmulateClipDistance, NativeMaskingKeepsGlInReadsAndMasksOnlyTheProducer) { + const char* vertex = R"(#version 330 core +void main() { + gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0); + gl_ClipDistance[0] = gl_Position.x; +} +)"; + const char* geometry = R"(#version 330 core +uniform uint mg_ClipDistanceEnables; +layout(triangles) in; +layout(triangle_strip, max_vertices = 3) out; +void main() { + for (int i = 0; i < 3; ++i) { + gl_Position = gl_in[i].gl_Position; + gl_ClipDistance[0] = gl_in[i].gl_ClipDistance[0]; + EmitVertex(); + } +} +)"; + Vector types; + auto modules = CompileProgramToSpirv( + {{GL_VERTEX_SHADER, vertex}, {GL_GEOMETRY_SHADER, geometry}, {GL_FRAGMENT_SHADER, kPlainFragment}}, types); + ASSERT_EQ(modules.size(), 3u); + const auto vertexBefore = modules[0]; + ShaderCompiler::ClipDistanceEmulationOutcome outcome; + ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true, + /*nativeMask=*/true)); + EXPECT_TRUE(outcome.emulated) << outcome.declineDetail; + EXPECT_TRUE(outcome.enablesFound); + EXPECT_EQ(modules[0], vertexBefore); + SanitizeAll(modules); + for (const auto& module : modules) + EXPECT_TRUE(Validates(module)); +} + TEST(EmulateClipDistance, TheEnablesShimArmsOnlyOnAnEmulatingBackendAndOnlyWhereItIsNamed) { CompileEnv emulating{}; emulating.backend = BackendType::DirectGLES; @@ -365,4 +428,12 @@ TEST(EmulateClipDistance, TheEnablesShimArmsOnlyOnAnEmulatingBackendAndOnlyWhere source = nonClipping; MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Vertex, source, emulating); EXPECT_FALSE(Contains(source, MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) << source; + + // A native backend that masks in the shader (Magma) needs the uniform too. + CompileEnv masking = native; + masking.backend = BackendType::DirectVulkan; + masking.params.MasksNativeClipDistances = true; + source = vertex; + MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Vertex, source, masking); + EXPECT_TRUE(Contains(source, MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) << source; } diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index 6547ed6b..edae95f5 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -2845,8 +2845,9 @@ namespace MobileGL::MG_Util::SelfTest { if (MG_Config::Features.ClipDistanceEmulation == MG_Config::QuirkOverride::ForceOff) emulate = false; if (!emulate && hasFeature) { builder.Pass("shaderClipDistance", - format("native ({} distances); GL_CLIP_DISTANCEi enables are not applied on this backend " - "yet, so every distance a shader writes clips", + format("native ({} distances); Vulkan has no per-distance enable state, so MobileGL " + "applies GL_CLIP_DISTANCEi in the last pre-rasterization stage (a disabled " + "distance is written as +1)", properties.limits.maxClipDistances)); } else if (emulate) { builder.Warn("shaderClipDistance", diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp index d5bcc2c6..f5f02e8d 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp @@ -79,6 +79,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler { // source pipeline declares the reserved enables uniform in every pre-rasterization // stage that names gl_ClipDistance, so glslang parses a different source. HashValue(state, env.params.EmulatesClipDistance); + HashValue(state, env.params.MasksNativeClipDistances); // The cull-distance pair, added when the GL 4.6 API-surface wave made them env-derived: // they were bare literals (8/8) in BuildTBuiltInResource while no backend had ever been // asked whether it can host a cull distance. Exactly the MaxClipDistances class - glslang diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h index 73b9dbc7..0ffac441 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h @@ -169,6 +169,12 @@ namespace MobileGL::MG_Util::ShaderTranspiler { // (BackendCaps::EmulatesClipDistance). Part of the frontend fingerprint, not a separate // L1 bit: it changes the parsed source (the reserved enables uniform), not only phase B. Bool EmulatesClipDistance() const { return HasBackend() && params.EmulatesClipDistance; } + // BackendCaps::MasksNativeClipDistances, same fingerprint reasoning. + Bool MasksNativeClipDistances() const { + return HasBackend() && params.MasksNativeClipDistances && !params.EmulatesClipDistance; + } + // Either way the program needs the reserved enables uniform. + Bool NeedsClipDistanceEnables() const { return EmulatesClipDistance() || MasksNativeClipDistances(); } // Matches the historical rule exactly: with no active backend every extension counts // as advertised, because the frontend then has nothing to gate against. Bool IsExtensionAdvertised(GLExtension extension) const { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index d11088c4..95880b62 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -1222,7 +1222,7 @@ namespace MobileGL { const Uint32 maxVaryingLocations, ClipDistanceEmulationOutcome& outcome, const bool validateOutput, - const bool enableSpirvValidation) { + const bool enableSpirvValidation, const bool nativeMask) { outcome = {}; // Pipeline order: VS, TCS, TES, GS, FS. Int stageIndex[5] = {-1, -1, -1, -1, -1}; @@ -1275,7 +1275,7 @@ namespace MobileGL { distanceCount); return true; } - const Uint32 carrierCount = (distanceCount + 3) / 4; + const Uint32 carrierCount = nativeMask ? 0 : (distanceCount + 3) / 4; if (carrierCount > 0 && carrierLocation + carrierCount > maxVaryingLocations) { outcome.declineDetail = std::format( "the program's varyings already reach location {}, leaving no room for {} clip carrier(s) " @@ -1292,6 +1292,10 @@ namespace MobileGL { if (moduleIndex < 0) continue; EmulateClipDistanceOptions options; options.location = carrierLocation; + // Native masking touches the producer alone: every other stage hosts the + // built-in, and only the producer's values reach the clipper. + if (nativeMask && !(stage == producer && distanceCount > 0)) continue; + options.nativeBuiltin = nativeMask; if (stage == 4) { options.role = EmulateClipDistanceOptions::Role::Consumer; options.distanceCount = distanceCount; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 0d445fdd..4a3e6ef9 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -676,12 +676,17 @@ namespace MobileGL { // `captureRequestsClipDistance` declines (a capture would need the unmasked // value at a varying the pass does not create); `maxVaryingLocations` is the // carrier budget. Returns false only when the optimizer itself failed. + // + // `nativeMask`: the backend HOSTS gl_ClipDistance but has no per-distance enable + // state (Vulkan). Only the producing stage is rewritten - it writes the masked + // values back into the built-in (EmulateClipDistanceOptions::nativeBuiltin) - and + // no carrier, location or fragment discard is involved. static Bool EmulateClipDistanceForProgram(Vector>& modules, const Vector& shaderTypes, Bool captureRequestsClipDistance, Uint32 maxVaryingLocations, ClipDistanceEmulationOutcome& outcome, bool validateOutput = true, - bool enableSpirvValidation = false); + bool enableSpirvValidation = false, bool nativeMask = false); // True when the module still declares a 64-bit float type. After // SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index 5f9f9cfa..086421f7 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -2041,7 +2041,7 @@ namespace MobileGL { ModernizeLegacyGLSL(stage, source, afterVersion); InjectDepthRangeBuiltinShim(stage, source, afterVersion); InjectNumSamplesBuiltinShim(stage, originalLanguage, source, afterVersion); - if (env.EmulatesClipDistance()) InjectClipDistanceEnablesShim(stage, source, afterVersion); + if (env.NeedsClipDistanceEnables()) InjectClipDistanceEnablesShim(stage, source, afterVersion); } Vector ExternalSamplerUniformNames(const String& preprocessedSource) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp index 10952b94..ab391113 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp @@ -318,12 +318,18 @@ namespace MobileGL { if (m_options.role == Role::Producer && model == spv::ExecutionModel::TessellationControl) { return decline("a control stage cannot be the last pre-rasterization stage"); } + const bool native = m_options.nativeBuiltin; + if (native && m_options.role != Role::Producer) return decline("native masking outside the producer"); const ClipSites sites = FindClipSites(ctx); // ---- Phase 1: ANALYSIS ONLY. A decline leaves the module byte-identical. ---- std::vector memberChains; // [clipMember, rest...] on a non-arrayed block std::vector standaloneVars; // standalone ClipDistance variables + // Native masking: where the masked values are written back to. + Instruction* nativeBlockVar = nullptr; + uint32_t nativeBlockMember = 0; + Instruction* nativeStandaloneVar = nullptr; uint32_t shadowTypeId = 0; String reason; const auto noteShadowType = [&](uint32_t typeId) { @@ -338,9 +344,12 @@ namespace MobileGL { if (inst.opcode() != spv::Op::OpVariable) continue; const auto storage = static_cast(inst.GetSingleWordInOperand(0)); if (storage != spv::StorageClass::Input && storage != spv::StorageClass::Output) continue; + // Native masking leaves every INPUT alone: gl_in[] reads are hosted natively. + if (native && storage == spv::StorageClass::Input) continue; const uint32_t pointeeId = PointeeOf(ctx, inst.type_id()); if (sites.standaloneVars.count(inst.result_id()) != 0) { + if (native) nativeStandaloneVar = &inst; if (FloatArrayLength(ctx, pointeeId) == 0) { return decline("a standalone gl_ClipDistance that is not a sized float array"); } @@ -370,6 +379,10 @@ namespace MobileGL { } const uint32_t memberTypeId = pointee->GetSingleWordInOperand(clipMember); const uint32_t memberOperand = arrayed ? 2u : 1u; // [vertex,] member, ... + if (native && !arrayed && storage == spv::StorageClass::Output) { + nativeBlockVar = &inst; + nativeBlockMember = clipMember; + } bool declined = false; defUse->ForEachUser(&inst, [&](Instruction* user) { if (declined) return; @@ -511,6 +524,9 @@ namespace MobileGL { if (!hasAccesses && sites.capability == nullptr && producedCount == 0 && consumedCount == 0) { return Status::SuccessWithoutChange; } + if (native && producedCount > 0 && nativeBlockVar == nullptr && nativeStandaloneVar == nullptr) { + return decline("no gl_ClipDistance output to write the masked distances back into"); + } // ---- Phase 2: MUTATION. Nothing below may decline. ---- const bool interfaceListsAllGlobals = ctx->module()->version() >= 0x00010400u; @@ -540,13 +556,22 @@ namespace MobileGL { } for (Instruction* var : standaloneVars) { const uint32_t varId = var->result_id(); + if (native) { + // The variable stays the built-in the flush writes; only the shader's + // own accesses move to the shadow. + ctx->ReplaceAllUsesWithPredicate(varId, shadowId, [](Instruction* user) { + return user->opcode() != spv::Op::OpDecorate && user->opcode() != spv::Op::OpName && + user->opcode() != spv::Op::OpEntryPoint; + }); + continue; + } ctx->KillNamesAndDecorates(varId); RemoveFromInterface(entryPoint, varId); ctx->ReplaceAllUsesWith(varId, shadowId); ctx->KillInst(var); } if (shadowId != 0) RetypeChainUsers(ctx, defUse->GetDef(shadowId)); - if (sites.capability != nullptr) ctx->KillInst(sites.capability); + if (sites.capability != nullptr && !native) ctx->KillInst(sites.capability); analysis::Float floatTy(32); const uint32_t floatTypeId = typeMgr->GetTypeInstruction(&floatTy); @@ -561,8 +586,11 @@ namespace MobileGL { // ---- producer: write the carriers ---- if (producedCount > 0) { - const Uint32 carrierCount = (producedCount + 3) / 4; + const Uint32 carrierCount = native ? 0 : (producedCount + 3) / 4; uint32_t carriers[kMaxCarriers] = {0, 0}; + const uint32_t ptrOutputFloat = typeMgr->FindPointerToType(floatTypeId, spv::StorageClass::Output); + const uint32_t nativeMemberConst = + nativeBlockVar != nullptr ? constMgr->GetSIntConstId(static_cast(nativeBlockMember)) : 0; for (Uint32 k = 0; k < carrierCount; ++k) { carriers[k] = AddGlobalVariable(ctx, vec4TypeId, spv::StorageClass::Output, CarrierName(k)); AddLocationDecoration(ctx, carriers[k], m_options.location + k); @@ -622,6 +650,17 @@ namespace MobileGL { } values.push_back(value); } + if (native) { + for (Uint32 i = 0; i < producedCount; ++i) { + const uint32_t index = constMgr->GetUIntConstId(i); + Instruction* ptr = + nativeBlockVar != nullptr + ? b.AddAccessChain(ptrOutputFloat, nativeBlockVar->result_id(), + {nativeMemberConst, index}) + : b.AddAccessChain(ptrOutputFloat, nativeStandaloneVar->result_id(), {index}); + b.AddStore(ptr->result_id(), values[i]); + } + } while (values.size() % 4 != 0) values.push_back(oneId); for (Uint32 k = 0; k < carrierCount; ++k) { const uint32_t composite = diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h index 59e42299..b127fbf7 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h @@ -58,6 +58,10 @@ namespace MobileGL { // member index, whole-gl_PerVertex loads/stores/copies, and a pointer into the // built-in that escapes into an opcode the pass cannot retype (a function argument). // + // NATIVE MASKING (EmulateClipDistanceOptions::nativeBuiltin) is the same producer + // rewrite for a backend that hosts the built-in but cannot switch distances off: the + // masked values go back into gl_ClipDistance instead of into carriers. + // // One module per run; the program-wide contract (one shared location, which stage // is the producer, every stage rewritten or none) is owned by // ShaderCompiler::EmulateClipDistanceForProgram, the only caller. @@ -79,6 +83,14 @@ namespace MobileGL { // Producer only: the default-block uniform member holding the enable bits. When // the module has no such member every distance counts as enabled. String enablesMemberName; + // Producer only: the backend HOSTS the built-in (a Vulkan device with + // shaderClipDistance) but has no per-distance enable state - Vulkan clips by + // every distance a shader writes. Instead of carriers, the producer writes + // `enabled(i) ? d[i] : 1.0` back into gl_ClipDistance itself at the same flush + // points, so hardware clipping stays and a disabled distance clips nothing. The + // capability is kept, nothing else in the program is touched, and gl_in / + // fragment-stage reads stay native. + Bool nativeBuiltin = false; }; struct EmulateClipDistanceReport {