diff --git a/CMakeLists.txt b/CMakeLists.txt index d2909e7e..fc7c5af3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -452,6 +452,7 @@ set(SOURCE_FILES MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp MobileGL/MG_Util/SelfTest/DriverPost.cpp + MobileGL/MG_Util/SelfTest/DriverPostClipDistance.cpp MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 0093c0b8..c37a979d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -1117,6 +1117,28 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + // Without the shaderClipDistance feature the built-in is emulated instead + // (EmulateClipDistancePass: varyings plus a fragment discard, which needs nothing from the + // device), so the frontend can still answer the GL 3.0+ minimum of eight. Every Mali device + // measured so far reports shaderClipDistance = VK_FALSE. MOBILEGL_CLIP_DISTANCE_EMULATION=1 + // arms it on a device that has the feature (the pinned integration lane); =0 restores the + // plain zero. + static Bool DecideClipDistanceEmulation(const Bool deviceHasClipDistance) { + switch (MG_Config::Features.ClipDistanceEmulation) { + case MG_Config::QuirkOverride::ForceOn: + MGLOG_I("DirectVulkan: MOBILEGL_CLIP_DISTANCE_EMULATION=1 - emulating gl_ClipDistance even though " + "the device %s shaderClipDistance", + deviceHasClipDistance ? "has" : "lacks"); + return true; + case MG_Config::QuirkOverride::ForceOff: + MGLOG_I("DirectVulkan: MOBILEGL_CLIP_DISTANCE_EMULATION=0 - no gl_ClipDistance emulation"); + return false; + case MG_Config::QuirkOverride::Auto: + default: + return !deviceHasClipDistance; + } + } + void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() { const auto mapShaderStages = [](Uint32 vkStages) { Uint32 glStages = 0; @@ -1343,40 +1365,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { // by every device, but declaring ClipDistance in a module needs the shaderClipDistance // FEATURE, which VulkanRenderer enables exactly where the physical device has it. Without // it the limit describes a capacity no shader may use, so report none. - // - // Without the feature the built-in is emulated instead (EmulateClipDistancePass: varyings - // plus a fragment discard, which needs nothing from the device), so the frontend can - // still answer the GL 3.0+ minimum of eight. Every Mali device measured so far reports - // shaderClipDistance = VK_FALSE. MOBILEGL_CLIP_DISTANCE_EMULATION=1 arms it on a device - // that has the feature (the pinned integration lane); =0 restores the plain zero. - Bool emulateClipDistance = !m_vulkanCaps.SupportsShaderClipDistance; - switch (MG_Config::Features.ClipDistanceEmulation) { - case MG_Config::QuirkOverride::ForceOn: - emulateClipDistance = true; - MGLOG_I("DirectVulkan: MOBILEGL_CLIP_DISTANCE_EMULATION=1 - emulating gl_ClipDistance even though " - "the device %s shaderClipDistance", - m_vulkanCaps.SupportsShaderClipDistance ? "has" : "lacks"); - break; - case MG_Config::QuirkOverride::ForceOff: - emulateClipDistance = false; - MGLOG_I("DirectVulkan: MOBILEGL_CLIP_DISTANCE_EMULATION=0 - no gl_ClipDistance emulation"); - break; - case MG_Config::QuirkOverride::Auto: - default: - break; - } + const Bool emulateClipDistance = DecideClipDistanceEmulation(m_vulkanCaps.SupportsShaderClipDistance); 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); + 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)" - : (m_dynamicParameters.MasksNativeClipDistances - ? "native, GL_CLIP_DISTANCEi applied in the shader" - : "unsupported"), + 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 diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index fe8e743c..75d6c153 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -116,8 +116,7 @@ namespace MobileGL::MG_Impl::GLImpl { if (!program || !program->UsesReservedClipDistanceEnables()) return; Uint32 enables = 0; for (Uint i = 0; i < 8; ++i) { - const auto cap = - static_cast(static_cast(CapabilityInput::ClipDistance0) + i); + const auto cap = static_cast(static_cast(CapabilityInput::ClipDistance0) + i); if (MG_State::pGLContext->IsCapabilityEnabled(cap)) enables |= 1u << i; } program->WriteReservedClipDistanceEnables(enables); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp index 02630916..a150c59e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp @@ -162,8 +162,7 @@ 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(); + const Bool maskNativeClipDistance = m_phaseA->in.env != nullptr && m_phaseA->in.env->MasksNativeClipDistances(); GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation, nativeFloat64, demoteTessellationPointSize, demoteGeometryPointSize, emulateClipDistance, maskNativeClipDistance); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h index 11334e30..24d29d7d 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h @@ -67,8 +67,8 @@ 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 maskNativeClipDistance); + Bool demoteTessellationPointSize, Bool demoteGeometryPointSize, 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/CMakeLists.txt b/MobileGL/MG_Test/CMakeLists.txt index 988e00ab..20b0bec8 100644 --- a/MobileGL/MG_Test/CMakeLists.txt +++ b/MobileGL/MG_Test/CMakeLists.txt @@ -45,6 +45,7 @@ enable_testing() add_executable( SanityTest SanityTest.cpp + SanityClipDistanceTest.cpp ) target_link_libraries( SanityTest diff --git a/MobileGL/MG_Test/SanityClipDistanceTest.cpp b/MobileGL/MG_Test/SanityClipDistanceTest.cpp new file mode 100644 index 00000000..6d6c6d84 --- /dev/null +++ b/MobileGL/MG_Test/SanityClipDistanceTest.cpp @@ -0,0 +1,121 @@ +// MobileGL - MobileGL/MG_Test/SanityClipDistanceTest.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 +// +// The clip and cull distance limits each backend advertises: native where the device clips, +// the emulated eight where it does not, and cull only where it is hosted. + +#include + +#include +#include +#include + +// maxClipDistances is a LIMIT every Vulkan device reports; declaring ClipDistance in a module +// needs the shaderClipDistance FEATURE, which is separate and which VulkanRenderer enables only +// where the physical device has it. Without the feature the device's limit is never forwarded: +// the backend EMULATES the built-in instead (EmulateClipDistancePass), which is what lets it +// answer GL 3.0's minimum of eight on every Mali device. With the feature it keeps its real +// number and the native path. +TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) { + using namespace MobileGL; + + MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; + MG_External::VulkanCapabilities caps; + caps.MaxClipDistances = 8; + + caps.SupportsShaderClipDistance = false; + caps.MaxClipDistances = 0; // what a Mali device reports alongside the missing feature + backend.ApplyVulkanCapabilitiesForTesting(caps); + 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); +} + +// The cull half of the same contract. shaderCullDistance is a SEPARATE feature from +// shaderClipDistance - VulkanRenderer enables each independently - so it gets its own gate, and +// the combined limit is gated on either being present because GL 4.6 core 11.1.3.10 makes it at +// least as large as both halves. These three used to be literal 8s inside BuildTBuiltInResource +// with no device consulted at all, which let glslang accept a gl_CullDistance write that then +// discarded every primitive it touched. +TEST(DirectVulkanSanity, GatesCullDistancesOnTheShaderCullDistanceFeature) { + using namespace MobileGL; + + MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; + MG_External::VulkanCapabilities caps; + caps.MaxClipDistances = 8; + caps.MaxCullDistances = 8; + caps.MaxCombinedClipAndCullDistances = 8; + + // Neither feature: cull stays zero (a per-primitive cull has no per-fragment substitute), + // while clip is emulated, so the combined limit describes the emulated eight. + caps.SupportsShaderClipDistance = false; + caps.SupportsShaderCullDistance = false; + backend.ApplyVulkanCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); + + // Clip only: cull stays zero, and the combined limit still describes the clip capacity. + caps.SupportsShaderClipDistance = true; + backend.ApplyVulkanCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); + + caps.SupportsShaderCullDistance = true; + backend.ApplyVulkanCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); +} + +// DirectGLES reaches clip AND cull distances only through GL_EXT_clip_cull_distance, so the +// loader leaves all three at zero without it and the backend forwards that verbatim. Zero is the +// answer that stops a gl_CullDistance shader from reaching an ESSL compiler that would reject it. +TEST(DirectGLESSanity, ForwardsTheProbedClipAndCullDistanceLimits) { + using namespace MobileGL; + + MG_Backend::DirectGLES::BackendObject_DirectGLES backend; + MG_External::GLESCapabilities caps; + backend.ApplyGLESCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0); + + caps.SupportsClipDistance = true; + caps.MaxClipDistances = 8; + caps.MaxCullDistances = 8; + caps.MaxCombinedClipAndCullDistances = 8; + backend.ApplyGLESCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); + EXPECT_FALSE(backend.GetDynamicParameters().EmulatesClipDistance); +} + +// The loader's verdict that the driver cannot clip natively (no extension, or the POST clip probe +// caught it misbehaving) turns the clip limit into the emulated eight - GL 3.0's minimum - while +// cull keeps the driver's own answer: a per-primitive cull has no per-fragment substitute. +TEST(DirectGLESSanity, AnEmulatingLoaderVerdictAdvertisesEightClipDistancesAndNoCull) { + using namespace MobileGL; + + MG_Backend::DirectGLES::BackendObject_DirectGLES backend; + MG_External::GLESCapabilities caps; // no GL_EXT_clip_cull_distance, as on every Mali driver + caps.EmulatesClipDistance = true; + backend.ApplyGLESCapabilitiesForTesting(caps); + EXPECT_TRUE(backend.GetDynamicParameters().EmulatesClipDistance); + EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); +} diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 2b3a70a9..803669ab 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -696,111 +696,6 @@ TEST(DirectGLESSanity, PreservesHostPerStageImageUniformLimits) { EXPECT_EQ(params.MaxComputeImageUniforms, 5); } -// maxClipDistances is a LIMIT every Vulkan device reports; declaring ClipDistance in a module -// needs the shaderClipDistance FEATURE, which is separate and which VulkanRenderer enables only -// where the physical device has it. Without the feature the device's limit is never forwarded: -// the backend EMULATES the built-in instead (EmulateClipDistancePass), which is what lets it -// answer GL 3.0's minimum of eight on every Mali device. With the feature it keeps its real -// number and the native path. -TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) { - using namespace MobileGL; - - MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; - MG_External::VulkanCapabilities caps; - caps.MaxClipDistances = 8; - - caps.SupportsShaderClipDistance = false; - caps.MaxClipDistances = 0; // what a Mali device reports alongside the missing feature - backend.ApplyVulkanCapabilitiesForTesting(caps); - 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); -} - -// The cull half of the same contract. shaderCullDistance is a SEPARATE feature from -// shaderClipDistance - VulkanRenderer enables each independently - so it gets its own gate, and -// the combined limit is gated on either being present because GL 4.6 core 11.1.3.10 makes it at -// least as large as both halves. These three used to be literal 8s inside BuildTBuiltInResource -// with no device consulted at all, which let glslang accept a gl_CullDistance write that then -// discarded every primitive it touched. -TEST(DirectVulkanSanity, GatesCullDistancesOnTheShaderCullDistanceFeature) { - using namespace MobileGL; - - MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; - MG_External::VulkanCapabilities caps; - caps.MaxClipDistances = 8; - caps.MaxCullDistances = 8; - caps.MaxCombinedClipAndCullDistances = 8; - - // Neither feature: cull stays zero (a per-primitive cull has no per-fragment substitute), - // while clip is emulated, so the combined limit describes the emulated eight. - caps.SupportsShaderClipDistance = false; - caps.SupportsShaderCullDistance = false; - backend.ApplyVulkanCapabilitiesForTesting(caps); - EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); - EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); - - // Clip only: cull stays zero, and the combined limit still describes the clip capacity. - caps.SupportsShaderClipDistance = true; - backend.ApplyVulkanCapabilitiesForTesting(caps); - EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); - EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); - - caps.SupportsShaderCullDistance = true; - backend.ApplyVulkanCapabilitiesForTesting(caps); - EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8); - EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); -} - -// DirectGLES reaches clip AND cull distances only through GL_EXT_clip_cull_distance, so the -// loader leaves all three at zero without it and the backend forwards that verbatim. Zero is the -// answer that stops a gl_CullDistance shader from reaching an ESSL compiler that would reject it. -TEST(DirectGLESSanity, ForwardsTheProbedClipAndCullDistanceLimits) { - using namespace MobileGL; - - MG_Backend::DirectGLES::BackendObject_DirectGLES backend; - MG_External::GLESCapabilities caps; - backend.ApplyGLESCapabilitiesForTesting(caps); - EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0); - EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); - EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0); - - caps.SupportsClipDistance = true; - caps.MaxClipDistances = 8; - caps.MaxCullDistances = 8; - caps.MaxCombinedClipAndCullDistances = 8; - backend.ApplyGLESCapabilitiesForTesting(caps); - EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); - EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8); - EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); - EXPECT_FALSE(backend.GetDynamicParameters().EmulatesClipDistance); -} - -// The loader's verdict that the driver cannot clip natively (no extension, or the POST clip probe -// caught it misbehaving) turns the clip limit into the emulated eight - GL 3.0's minimum - while -// cull keeps the driver's own answer: a per-primitive cull has no per-fragment substitute. -TEST(DirectGLESSanity, AnEmulatingLoaderVerdictAdvertisesEightClipDistancesAndNoCull) { - using namespace MobileGL; - - MG_Backend::DirectGLES::BackendObject_DirectGLES backend; - MG_External::GLESCapabilities caps; // no GL_EXT_clip_cull_distance, as on every Mali driver - caps.EmulatesClipDistance = true; - backend.ApplyGLESCapabilitiesForTesting(caps); - EXPECT_TRUE(backend.GetDynamicParameters().EmulatesClipDistance); - EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); - EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); - EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); -} - // GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded // GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test // devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan diff --git a/MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp index e23929e1..9bb2cddb 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp @@ -88,7 +88,9 @@ namespace { return ok; } - Bool Contains(const String& haystack, const char* needle) { return haystack.find(needle) != String::npos; } + Bool Contains(const String& haystack, const char* needle) { + return haystack.find(needle) != String::npos; + } // The shape from the field report (Mali-G615, Espryt): a #version 330 core vertex stage // writing all eight distances from uniform planes. The enables uniform is what @@ -123,8 +125,8 @@ void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); } TEST(EmulateClipDistance, TheReportedEightPlaneVertexStageClipsByDiscard) { Vector types; - auto modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kEightPlaneVertex}, {GL_FRAGMENT_SHADER, kPlainFragment}}, - types); + auto modules = + CompileProgramToSpirv({{GL_VERTEX_SHADER, kEightPlaneVertex}, {GL_FRAGMENT_SHADER, kPlainFragment}}, types); ASSERT_EQ(modules.size(), 2u); ASSERT_TRUE(Contains(Disassemble(modules[0]), "OpCapability ClipDistance")); @@ -291,8 +293,8 @@ void main() { TEST(EmulateClipDistance, ACaptureOfGlClipDistanceDeclines) { Vector types; - auto modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, kEightPlaneVertex}, {GL_FRAGMENT_SHADER, kPlainFragment}}, - types); + auto modules = + CompileProgramToSpirv({{GL_VERTEX_SHADER, kEightPlaneVertex}, {GL_FRAGMENT_SHADER, kPlainFragment}}, types); ASSERT_EQ(modules.size(), 2u); const auto before = modules; ShaderCompiler::ClipDistanceEmulationOutcome outcome; @@ -344,8 +346,8 @@ void main() { // 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); + 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; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 484dcd3b..4391f023 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -1059,6 +1059,43 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxFragmentShadingRateAttachmentTexelHeight); } + // Native clip distances or MobileGL's emulation. Native only where the driver both advertises + // GL_EXT_clip_cull_distance AND passes the probe (its own control first): an extension that is + // listed but does not clip, or clips by disabled distances, is a wrong answer the application + // sees, and the emulation answers it correctly. The probe is skipped entirely without the + // extension - there is nothing to measure. + static void DecideClipDistanceEmulation(MG_External::GLESCapabilities& caps, + const MG_External::GLESFunctionsTable& glesFuncs) { + if (caps.SupportsClipDistance && caps.MaxClipDistances > 0) { + const SelfTest::NativeClipDistanceMeasurement& clip = SelfTest::NativeClipDistance(glesFuncs); + caps.NativeClipDistanceClips = clip.ran && clip.clips; + caps.NativeClipDistanceHonorsEnables = clip.ran && clip.honorsEnables; + const String verdict = !clip.ran ? "inconclusive (" + clip.failure + ")" + : !clip.clips ? "an enabled negative distance did NOT clip" + : !clip.honorsEnables ? "clips, but by DISABLED distances too" + : "clips, and honors the per-distance enables"; + MGLOG_I(" Native gl_ClipDistance probe: %s", verdict.c_str()); + } + caps.EmulatesClipDistance = !(caps.SupportsClipDistance && caps.MaxClipDistances > 0 && + caps.NativeClipDistanceClips && caps.NativeClipDistanceHonorsEnables); + switch (MG_Config::Features.ClipDistanceEmulation) { + case MG_Config::QuirkOverride::ForceOn: + caps.EmulatesClipDistance = true; + MGLOG_I(" gl_ClipDistance: emulation forced ON by MOBILEGL_CLIP_DISTANCE_EMULATION"); + break; + case MG_Config::QuirkOverride::ForceOff: + caps.EmulatesClipDistance = false; + MGLOG_I(" gl_ClipDistance: emulation forced OFF by MOBILEGL_CLIP_DISTANCE_EMULATION"); + break; + case MG_Config::QuirkOverride::Auto: + default: + break; + } + MGLOG_I(" gl_ClipDistance: %s", caps.EmulatesClipDistance + ? "EMULATED (varyings + fragment discard; 8 distances advertised)" + : "native (GL_EXT_clip_cull_distance)"); + } + Bool FillInGLESCapabilities(MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& glesFuncs) { if (!glesFuncs.glGetString || !glesFuncs.glGetIntegerv) { MGLOG_E("Required GLES functions are not loaded, cannot query capabilities"); @@ -1997,39 +2034,7 @@ namespace MobileGL::MG_Util::BackendLoader { MGLOG_I(" GL_MAX_COMPUTE_IMAGE_UNIFORMS: %d", caps.MaxComputeImageUniforms); MGLOG_I(" GL_MAX_DRAW_BUFFERS: %d", caps.MaxDrawBuffers); MGLOG_I(" GL_MAX_COLOR_ATTACHMENTS: %d", caps.MaxColorAttachments); - // Native clip distances or MobileGL's emulation. Native only where the driver both - // advertises GL_EXT_clip_cull_distance AND passes the probe (its own control first): - // an extension that is listed but does not clip, or clips by disabled distances, is a - // wrong answer the application sees, and the emulation answers it correctly. The probe - // is skipped entirely without the extension - there is nothing to measure. - if (caps.SupportsClipDistance && caps.MaxClipDistances > 0) { - const SelfTest::NativeClipDistanceMeasurement& clip = SelfTest::NativeClipDistance(glesFuncs); - caps.NativeClipDistanceClips = clip.ran && clip.clips; - caps.NativeClipDistanceHonorsEnables = clip.ran && clip.honorsEnables; - MGLOG_I(" Native gl_ClipDistance probe: %s", - !clip.ran ? ("inconclusive (" + clip.failure + ")").c_str() - : !clip.clips ? "an enabled negative distance did NOT clip" - : !clip.honorsEnables ? "clips, but by DISABLED distances too" - : "clips, and honors the per-distance enables"); - } - caps.EmulatesClipDistance = !(caps.SupportsClipDistance && caps.MaxClipDistances > 0 && - caps.NativeClipDistanceClips && caps.NativeClipDistanceHonorsEnables); - switch (MG_Config::Features.ClipDistanceEmulation) { - case MG_Config::QuirkOverride::ForceOn: - caps.EmulatesClipDistance = true; - MGLOG_I(" gl_ClipDistance: emulation forced ON by MOBILEGL_CLIP_DISTANCE_EMULATION"); - break; - case MG_Config::QuirkOverride::ForceOff: - caps.EmulatesClipDistance = false; - MGLOG_I(" gl_ClipDistance: emulation forced OFF by MOBILEGL_CLIP_DISTANCE_EMULATION"); - break; - case MG_Config::QuirkOverride::Auto: - default: - break; - } - MGLOG_I(" gl_ClipDistance: %s", caps.EmulatesClipDistance - ? "EMULATED (varyings + fragment discard; 8 distances advertised)" - : "native (GL_EXT_clip_cull_distance)"); + DecideClipDistanceEmulation(caps, glesFuncs); // Worth spelling the reason out for the same reason the per-stage storage block counts // are: a zero here is what stops an application's gl_ClipDistance from ever clipping, and // reading it back from an artifact is the difference between "MobileGL dropped my draw" diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp index 925d0875..5e8b8cd0 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp @@ -2098,12 +2098,15 @@ namespace MobileGL::MG_Util::SelfTest { " gl_ClipDistance[0] = 1.0;\n" " gl_ClipDistance[1] = 1.0;\n" "}\n"; - const char* const kClipDistanceProbeFragmentSource = "#version 300 es\n" - "precision mediump float;\n" - "layout(location = 0) out vec4 mg_probeColor;\n" - "void main() { mg_probeColor = vec4(0.0, 1.0, 0.0, 1.0); }\n"; + const char* const kClipDistanceProbeFragmentSource = + "#version 300 es\n" + "precision mediump float;\n" + "layout(location = 0) out vec4 mg_probeColor;\n" + "void main() { mg_probeColor = vec4(0.0, 1.0, 0.0, 1.0); }\n"; - Bool IsProbeGreen(const GLubyte* pixel) { return pixel[0] < 64 && pixel[1] > 192; } + Bool IsProbeGreen(const GLubyte* pixel) { + return pixel[0] < 64 && pixel[1] > 192; + } // One draw into the bound 4x1 target, cleared red first. Returns false when the draw or // the read-back raised an error (inconclusive). diff --git a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h index 517fa080..9c69ba58 100644 --- a/MobileGL/MG_Util/SelfTest/DriverBugProbes.h +++ b/MobileGL/MG_Util/SelfTest/DriverBugProbes.h @@ -116,8 +116,8 @@ namespace MobileGL::MG_Util::SelfTest { // must paint whatever the driver does with enables, or the extension's draw itself is broken // and the probe has nothing to say. Then the SUBJECT, writing gl_ClipDistance[0] = x: with // distance 0 enabled the left half must vanish, and with it disabled nothing may vanish - // (Mesa's llvmpipe ES driver clips by every written distance regardless of the enables, a - // wrong answer GL applications see). + // (the llvmpipe ES driver clips by every written distance regardless of the enables, a wrong + // answer GL applications see). // // Requires the extension; the caller only runs it where the extension string names it. // Restores every piece of GL state it touches and leaves both distances disabled - the diff --git a/MobileGL/MG_Util/SelfTest/DriverPost.cpp b/MobileGL/MG_Util/SelfTest/DriverPost.cpp index edae95f5..d0c761e9 100644 --- a/MobileGL/MG_Util/SelfTest/DriverPost.cpp +++ b/MobileGL/MG_Util/SelfTest/DriverPost.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "DriverPost.h" +#include "DriverPostClipDistance.h" #include "DriverPostIterationRPWitness.h" #include "DriverPostIterationRPWitnessSpv.h" #include "PrimitivesGeneratedNoXfbProbe.h" @@ -340,62 +341,19 @@ namespace MobileGL::MG_Util::SelfTest { } } - // What the emulation still cannot do, shared by both backends' WARN rows. - constexpr const char* kClipDistanceEmulationResidue = - "MobileGL emulates 8 distances: gl_ClipDistance becomes two ordinary varyings and the " - "fragment stage discards where a distance is negative, honoring GL_CLIP_DISTANCEi. It costs " - "two varying locations and early depth/stencil for programs that clip; transform-feedback " - "capture of gl_ClipDistance and gl_in[].gl_ClipDistance reads are not emulated"; - - // GL_MAX_CLIP_DISTANCES >= 8 is a GL 3.0 requirement, so a backend that neither hosts nor - // emulates the built-in fails a capability every GL 3.x+ application may lean on. - void EvaluateGlesClipDistanceRows(ReportBuilder& builder, const MG_External::GLESCapabilities& caps, - const MG_External::GLESFunctionsTable& glesFuncs) { - // Only meaningful where the loader ran it (extension present); cached per process. - String probeFailure; - if (caps.SupportsClipDistance && caps.MaxClipDistances > 0) { - const NativeClipDistanceMeasurement& probe = NativeClipDistance(glesFuncs); - if (!probe.ran) probeFailure = probe.failure; - } - const Bool native = caps.SupportsClipDistance && caps.MaxClipDistances > 0 && - caps.NativeClipDistanceClips && caps.NativeClipDistanceHonorsEnables; - if (!caps.EmulatesClipDistance && native) { - builder.Pass("Clip distances", - format("native via GL_EXT_clip_cull_distance ({} distances); probe: an enabled distance " - "clips and a disabled one does not", - caps.MaxClipDistances)); - } else if (caps.EmulatesClipDistance) { - const String reason = - !caps.SupportsClipDistance ? String("no GL_EXT_clip_cull_distance") - : caps.MaxClipDistances <= 0 - ? String("GL_EXT_clip_cull_distance reports no distances") - : !probeFailure.empty() - ? "GL_EXT_clip_cull_distance is listed, but the native probe was inconclusive (" + - probeFailure + ")" - : !caps.NativeClipDistanceClips - ? String("GL_EXT_clip_cull_distance is listed, but in the probe an enabled negative " - "distance did not clip") - : !caps.NativeClipDistanceHonorsEnables - ? String("the driver clips by DISABLED distances too (probe)") - : String("forced by MOBILEGL_CLIP_DISTANCE_EMULATION=1"); - builder.Warn("Clip distances", format("{}; {}", reason, kClipDistanceEmulationResidue)); - } else { - builder.FailOptional("Clip distances", - caps.SupportsClipDistance - ? "GL_EXT_clip_cull_distance is listed but misbehaves in the probe, and the " - "emulation is disabled (MOBILEGL_CLIP_DISTANCE_EMULATION=0)" - : "no GL_EXT_clip_cull_distance and the emulation is disabled " - "(MOBILEGL_CLIP_DISTANCE_EMULATION=0): GL_MAX_CLIP_DISTANCES = 0, so a " - "gl_ClipDistance shader fails to compile"); - } - if (caps.SupportsClipDistance && caps.MaxCullDistances > 0) { - builder.Pass("Cull distances", - format("native via GL_EXT_clip_cull_distance ({} distances)", caps.MaxCullDistances)); - } else { - builder.FailOptional("Cull distances", - "no GL_EXT_clip_cull_distance: GL_MAX_CULL_DISTANCES = 0. Culling is per " - "primitive, which a per-fragment discard cannot reproduce, so a " - "gl_CullDistance shader fails to compile"); + void EmitClipDistanceRows(ReportBuilder& builder, Vector rows) { + for (ClipDistancePostRow& row : rows) { + switch (row.verdict) { + case ClipDistancePostRow::Verdict::Pass: + builder.Pass(Move(row.name), Move(row.detail)); + break; + case ClipDistancePostRow::Verdict::Warn: + builder.Warn(Move(row.name), Move(row.detail)); + break; + case ClipDistancePostRow::Verdict::FailOptional: + builder.FailOptional(Move(row.name), Move(row.detail)); + break; + } } } @@ -447,7 +405,7 @@ namespace MobileGL::MG_Util::SelfTest { "hard-fails, and a second fragment output cannot be produced any other " "way"); } - EvaluateGlesClipDistanceRows(builder, caps, glesFuncs); + EmitClipDistanceRows(builder, GlesClipDistancePostRows(caps, glesFuncs)); if (es31) { GLint maxVertexSsboBlocks = 0; @@ -2836,39 +2794,10 @@ namespace MobileGL::MG_Util::SelfTest { builder.FailOptional("dualSrcBlend", "unsupported; a draw using a GL_SRC1_* blend factor hard-fails, and a " "second fragment output cannot be produced any other way"); } - // Mirrors BackendObject_DirectVulkan's decision: emulate exactly when the feature is - // absent, unless MOBILEGL_CLIP_DISTANCE_EMULATION says otherwise. - { - const Bool hasFeature = features.shaderClipDistance == VK_TRUE; - Bool emulate = !hasFeature; - if (MG_Config::Features.ClipDistanceEmulation == MG_Config::QuirkOverride::ForceOn) emulate = true; - if (MG_Config::Features.ClipDistanceEmulation == MG_Config::QuirkOverride::ForceOff) emulate = false; - if (!emulate && hasFeature) { - builder.Pass("shaderClipDistance", - 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", - format("{}; {}", - hasFeature ? "forced by MOBILEGL_CLIP_DISTANCE_EMULATION=1" - : "unsupported by the device", - kClipDistanceEmulationResidue)); - } else { - builder.FailOptional("shaderClipDistance", - "unsupported, and the emulation is disabled (MOBILEGL_CLIP_DISTANCE_EMULATION=0): " - "GL_MAX_CLIP_DISTANCES = 0, so a gl_ClipDistance shader fails to compile"); - } - if (features.shaderCullDistance == VK_TRUE) { - builder.Pass("shaderCullDistance", format("native ({} distances)", properties.limits.maxCullDistances)); - } else { - builder.FailOptional("shaderCullDistance", - "unsupported: GL_MAX_CULL_DISTANCES = 0. Culling is per primitive, which a " - "per-fragment discard cannot reproduce, so a gl_CullDistance shader fails to " - "compile"); - } - } + EmitClipDistanceRows(builder, VulkanClipDistancePostRows(features.shaderClipDistance == VK_TRUE, + features.shaderCullDistance == VK_TRUE, + properties.limits.maxClipDistances, + properties.limits.maxCullDistances)); // The Magma counterpart of the GLES "Buffer textures" row, so the two sections can be // read side by side. Vulkan has no optional-feature bit here: a uniform texel buffer is // core, and maxTexelBufferElements has a spec floor of 65536 - exactly the GL 3.1 floor diff --git a/MobileGL/MG_Util/SelfTest/DriverPostClipDistance.cpp b/MobileGL/MG_Util/SelfTest/DriverPostClipDistance.cpp new file mode 100644 index 00000000..994bf2c3 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverPostClipDistance.cpp @@ -0,0 +1,118 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostClipDistance.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 "DriverPostClipDistance.h" +#include "DriverBugProbes.h" +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + namespace { + using Verdict = ClipDistancePostRow::Verdict; + + // What the emulation still cannot do, shared by both backends' WARN rows. + constexpr const char* kClipDistanceEmulationResidue = + "MobileGL emulates 8 distances: gl_ClipDistance becomes two ordinary varyings and the " + "fragment stage discards where a distance is negative, honoring GL_CLIP_DISTANCEi. It costs " + "two varying locations and early depth/stencil for programs that clip; transform-feedback " + "capture of gl_ClipDistance and gl_in[].gl_ClipDistance reads are not emulated"; + + // Culling is per primitive, which a per-fragment discard cannot reproduce. + constexpr const char* kNoCullDistance = + "GL_MAX_CULL_DISTANCES = 0. Culling is per primitive, which a per-fragment discard cannot " + "reproduce, so a gl_CullDistance shader fails to compile"; + + String GlesEmulationReason(const MG_External::GLESCapabilities& caps, const String& probeFailure) { + if (!caps.SupportsClipDistance) return "no GL_EXT_clip_cull_distance"; + if (caps.MaxClipDistances <= 0) return "GL_EXT_clip_cull_distance reports no distances"; + if (!probeFailure.empty()) { + return "GL_EXT_clip_cull_distance is listed, but the native probe was inconclusive (" + probeFailure + + ")"; + } + if (!caps.NativeClipDistanceClips) { + return "GL_EXT_clip_cull_distance is listed, but in the probe an enabled negative distance did not " + "clip"; + } + if (!caps.NativeClipDistanceHonorsEnables) return "the driver clips by DISABLED distances too (probe)"; + return "forced by MOBILEGL_CLIP_DISTANCE_EMULATION=1"; + } + } // namespace + + Vector GlesClipDistancePostRows(const MG_External::GLESCapabilities& caps, + const MG_External::GLESFunctionsTable& glesFuncs) { + Vector rows; + // Only meaningful where the loader ran it (extension present); cached per process. + String probeFailure; + if (caps.SupportsClipDistance && caps.MaxClipDistances > 0) { + const NativeClipDistanceMeasurement& probe = NativeClipDistance(glesFuncs); + if (!probe.ran) probeFailure = probe.failure; + } + const Bool native = caps.SupportsClipDistance && caps.MaxClipDistances > 0 && caps.NativeClipDistanceClips && + caps.NativeClipDistanceHonorsEnables; + if (!caps.EmulatesClipDistance && native) { + rows.push_back({Verdict::Pass, "Clip distances", + std::format("native via GL_EXT_clip_cull_distance ({} distances); probe: an enabled " + "distance clips and a disabled one does not", + caps.MaxClipDistances)}); + } else if (caps.EmulatesClipDistance) { + rows.push_back( + {Verdict::Warn, "Clip distances", + std::format("{}; {}", GlesEmulationReason(caps, probeFailure), kClipDistanceEmulationResidue)}); + } else { + rows.push_back({Verdict::FailOptional, "Clip distances", + caps.SupportsClipDistance + ? "GL_EXT_clip_cull_distance is listed but misbehaves in the probe, and the emulation " + "is disabled (MOBILEGL_CLIP_DISTANCE_EMULATION=0)" + : "no GL_EXT_clip_cull_distance and the emulation is disabled " + "(MOBILEGL_CLIP_DISTANCE_EMULATION=0): GL_MAX_CLIP_DISTANCES = 0, so a " + "gl_ClipDistance shader fails to compile"}); + } + if (caps.SupportsClipDistance && caps.MaxCullDistances > 0) { + rows.push_back({Verdict::Pass, "Cull distances", + std::format("native via GL_EXT_clip_cull_distance ({} distances)", caps.MaxCullDistances)}); + } else { + rows.push_back({Verdict::FailOptional, "Cull distances", + std::format("no GL_EXT_clip_cull_distance: {}", kNoCullDistance)}); + } + return rows; + } + + Vector VulkanClipDistancePostRows(const Bool hasClipDistance, const Bool hasCullDistance, + const Uint32 maxClipDistances, + const Uint32 maxCullDistances) { + Vector rows; + Bool emulate = !hasClipDistance; + if (MG_Config::Features.ClipDistanceEmulation == MG_Config::QuirkOverride::ForceOn) emulate = true; + if (MG_Config::Features.ClipDistanceEmulation == MG_Config::QuirkOverride::ForceOff) emulate = false; + if (!emulate && hasClipDistance) { + rows.push_back({Verdict::Pass, "shaderClipDistance", + std::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)", + maxClipDistances)}); + } else if (emulate) { + rows.push_back({Verdict::Warn, "shaderClipDistance", + std::format("{}; {}", + hasClipDistance ? "forced by MOBILEGL_CLIP_DISTANCE_EMULATION=1" + : "unsupported by the device", + kClipDistanceEmulationResidue)}); + } else { + rows.push_back({Verdict::FailOptional, "shaderClipDistance", + "unsupported, and the emulation is disabled (MOBILEGL_CLIP_DISTANCE_EMULATION=0): " + "GL_MAX_CLIP_DISTANCES = 0, so a gl_ClipDistance shader fails to compile"}); + } + if (hasCullDistance) { + rows.push_back( + {Verdict::Pass, "shaderCullDistance", std::format("native ({} distances)", maxCullDistances)}); + } else { + rows.push_back( + {Verdict::FailOptional, "shaderCullDistance", std::format("unsupported: {}", kNoCullDistance)}); + } + return rows; + } +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/SelfTest/DriverPostClipDistance.h b/MobileGL/MG_Util/SelfTest/DriverPostClipDistance.h new file mode 100644 index 00000000..de9837c6 --- /dev/null +++ b/MobileGL/MG_Util/SelfTest/DriverPostClipDistance.h @@ -0,0 +1,40 @@ +// MobileGL - MobileGL/MG_Util/SelfTest/DriverPostClipDistance.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 +// +// The POST rows for clip and cull distances on both backends. GL_MAX_CLIP_DISTANCES >= 8 is a +// GL 3.0 requirement: PASS where the backend clips natively, WARN where MobileGL emulates the +// built-in, FAIL where it does neither. + +#pragma once + +#include +#include + +namespace MobileGL::MG_Util::SelfTest { + struct ClipDistancePostRow { + enum class Verdict { + Pass, + Warn, + FailOptional + }; + Verdict verdict = Verdict::Pass; + String name; + String detail; + }; + + // "Clip distances" and "Cull distances" for the GLES backend, from the loader's decision and + // its cached native probe. + Vector GlesClipDistancePostRows(const MG_External::GLESCapabilities& caps, + const MG_External::GLESFunctionsTable& glesFuncs); + + // "shaderClipDistance" and "shaderCullDistance" for the Vulkan backend, mirroring + // BackendObject_DirectVulkan's decision: emulate exactly when the feature is absent, unless + // MOBILEGL_CLIP_DISTANCE_EMULATION says otherwise. + Vector VulkanClipDistancePostRows(Bool hasClipDistance, Bool hasCullDistance, + Uint32 maxClipDistances, Uint32 maxCullDistances); +} // namespace MobileGL::MG_Util::SelfTest diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 95880b62..91e83bd5 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -1216,24 +1216,33 @@ namespace MobileGL { return true; } - Bool ShaderCompiler::EmulateClipDistanceForProgram(Vector>& modules, - const Vector& shaderTypes, - const Bool captureRequestsClipDistance, - const Uint32 maxVaryingLocations, - ClipDistanceEmulationOutcome& outcome, - const bool validateOutput, - const bool enableSpirvValidation, const bool nativeMask) { + Bool ShaderCompiler::EmulateClipDistanceForProgram( + Vector>& modules, const Vector& shaderTypes, + const Bool captureRequestsClipDistance, const Uint32 maxVaryingLocations, + ClipDistanceEmulationOutcome& outcome, const bool validateOutput, const bool enableSpirvValidation, + const bool nativeMask) { outcome = {}; // Pipeline order: VS, TCS, TES, GS, FS. Int stageIndex[5] = {-1, -1, -1, -1, -1}; for (SizeT i = 0; i < shaderTypes.size() && i < modules.size(); ++i) { switch (shaderTypes[i]) { - case GL_VERTEX_SHADER: stageIndex[0] = static_cast(i); break; - case GL_TESS_CONTROL_SHADER: stageIndex[1] = static_cast(i); break; - case GL_TESS_EVALUATION_SHADER: stageIndex[2] = static_cast(i); break; - case GL_GEOMETRY_SHADER: stageIndex[3] = static_cast(i); break; - case GL_FRAGMENT_SHADER: stageIndex[4] = static_cast(i); break; - default: break; + case GL_VERTEX_SHADER: + stageIndex[0] = static_cast(i); + break; + case GL_TESS_CONTROL_SHADER: + stageIndex[1] = static_cast(i); + break; + case GL_TESS_EVALUATION_SHADER: + stageIndex[2] = static_cast(i); + break; + case GL_GEOMETRY_SHADER: + stageIndex[3] = static_cast(i); + break; + case GL_FRAGMENT_SHADER: + stageIndex[4] = static_cast(i); + break; + default: + break; } } @@ -1271,8 +1280,8 @@ namespace MobileGL { distanceCount = probes[producer].outputDistanceCount; } if (distanceCount > EmulateClipDistancePass::kMaxCarriers * 4) { - outcome.declineDetail = std::format("{} clip distances, more than the eight the carriers hold", - distanceCount); + outcome.declineDetail = + std::format("{} clip distances, more than the eight the carriers hold", distanceCount); return true; } const Uint32 carrierCount = nativeMask ? 0 : (distanceCount + 3) / 4; @@ -1321,9 +1330,9 @@ namespace MobileGL { } if (options.role == EmulateClipDistanceOptions::Role::Producer) { if (report.distanceCount != distanceCount) { - outcome.declineDetail = std::format( - "the producing stage carries {} clip distances where {} were planned", - report.distanceCount, distanceCount); + outcome.declineDetail = + std::format("the producing stage carries {} clip distances where {} were planned", + report.distanceCount, distanceCount); return true; } enablesFound = report.enablesFound; diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index 086421f7..5d0f945b 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -1895,7 +1895,8 @@ namespace { if (source.find("gl_ClipDistance") == MobileGL::String::npos) return; const MobileGL::String masked = MaskCommentsAndQuotedText(source); if (!MaskedSourceHasIdentifier(masked, "gl_ClipDistance")) return; - if (MaskedSourceHasIdentifier(masked, MobileGL::MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) { + if (MaskedSourceHasIdentifier(masked, + MobileGL::MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) { return; } source.insert(afterVersion.Get(source), "uniform uint mg_ClipDistanceEnables;\n"); diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp index ab391113..d43f2c96 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp @@ -252,6 +252,260 @@ namespace MobileGL { } return false; } + + // Every redirected access must agree on one gl_ClipDistance array type. + bool NoteShadowType(uint32_t& shadowTypeId, uint32_t typeId) { + if (shadowTypeId == 0) { + shadowTypeId = typeId; + return true; + } + return shadowTypeId == typeId; + } + + // A chain that stops short of the gl_PerVertex member (a whole block or gl_in[i] + // pointer) is harmless unless something then reaches the clip member through it or + // copies the whole struct. + bool ShortChainTouchesClipMember(IRContext* ctx, Instruction* chain, uint32_t clipMember) { + bool touches = false; + ctx->get_def_use_mgr()->ForEachUser(chain, [&](Instruction* next) { + if (next->opcode() == spv::Op::OpAccessChain || + next->opcode() == spv::Op::OpInBoundsAccessChain) { + uint32_t member = 0; + if (next->NumInOperands() < 2 || + !PlainConstantValue(ctx, next->GetSingleWordInOperand(1), member) || + member == clipMember) { + touches = true; + } + } else if (next->opcode() != spv::Op::OpName) { + touches = true; // whole-struct load/store/copy + } + }); + return touches; + } + + struct BlockVariable { + Instruction* var = nullptr; + spv::StorageClass storage = spv::StorageClass::Output; + bool arrayed = false; + uint32_t clipMember = 0; + uint32_t memberTypeId = 0; + }; + + // Classifies every use of a gl_PerVertex block variable: chains onto the clip member + // of a non-arrayed block are collected, anything else that reaches it declines. + bool CollectBlockClipChains(IRContext* ctx, const BlockVariable& block, uint32_t& shadowTypeId, + std::vector& memberChains, String& reason) { + const uint32_t memberOperand = block.arrayed ? 2u : 1u; // [vertex,] member, ... + bool declined = false; + ctx->get_def_use_mgr()->ForEachUser(block.var, [&](Instruction* user) { + if (declined) return; + switch (user->opcode()) { + case spv::Op::OpEntryPoint: + case spv::Op::OpName: + case spv::Op::OpDecorate: + return; + case spv::Op::OpAccessChain: + case spv::Op::OpInBoundsAccessChain: { + if (user->NumInOperands() <= memberOperand) { + if (ShortChainTouchesClipMember(ctx, user, block.clipMember)) { + declined = true; + reason = "a split access chain or whole-gl_PerVertex copy reaching gl_ClipDistance"; + } + return; + } + uint32_t member = 0; + if (!PlainConstantValue(ctx, user->GetSingleWordInOperand(memberOperand), member)) { + declined = true; + reason = "a non-constant gl_PerVertex member index"; + return; + } + if (member != block.clipMember) return; // another member + if (block.arrayed) { + declined = true; + reason = block.storage == spv::StorageClass::Input ? "a gl_in[].gl_ClipDistance read" + : "a gl_out[].gl_ClipDistance write"; + return; + } + if (!NoteShadowType(shadowTypeId, block.memberTypeId)) { + declined = true; + reason = "two differently sized gl_ClipDistance arrays"; + return; + } + if (!PointerUsesAreRetypable(ctx, user, reason)) { + declined = true; + return; + } + memberChains.push_back(user); + return; + } + case spv::Op::OpLoad: + case spv::Op::OpStore: + case spv::Op::OpCopyMemory: + declined = true; + reason = "a whole-aggregate load/store/copy of the gl_PerVertex interface"; + return; + default: + declined = true; + reason = std::format("SPIR-V opcode {} reaching the gl_PerVertex interface", + static_cast(user->opcode())); + return; + } + }); + return !declined; + } + + // The clip member type of the stage's gl_PerVertex OUTPUT block, or 0. A producer with + // nothing redirected may still have declared the length the driver planned with. + uint32_t DeclaredOutputClipType(IRContext* ctx, const ClipSites& sites) { + for (Instruction& inst : ctx->module()->types_values()) { + if (inst.opcode() != spv::Op::OpVariable || + static_cast(inst.GetSingleWordInOperand(0)) != + spv::StorageClass::Output) { + continue; + } + Instruction* type = ctx->get_def_use_mgr()->GetDef(PointeeOf(ctx, inst.type_id())); + uint32_t member = 0; + if (type != nullptr && type->opcode() == spv::Op::OpTypeStruct && + ClipMemberOf(sites, type->result_id(), member)) { + return type->GetSingleWordInOperand(member); + } + } + return 0; + } + + struct EnablesUniform { + uint32_t varId = 0; + uint32_t memberIndex = 0; + uint32_t typeId = 0; + }; + + // The producer's enables uniform: a 32-bit integer member, by name, of a + // Uniform-storage struct. + EnablesUniform FindEnablesUniform(IRContext* ctx, const String& memberName) { + EnablesUniform found; + for (Instruction& debug : ctx->module()->debugs2()) { + if (debug.opcode() != spv::Op::OpMemberName || debug.NumInOperands() < 3) continue; + if (debug.GetOperand(2).AsString() != memberName) continue; + const uint32_t structId = debug.GetSingleWordInOperand(0); + const uint32_t member = debug.GetSingleWordInOperand(1); + Instruction* structType = ctx->get_def_use_mgr()->GetDef(structId); + if (structType == nullptr || structType->opcode() != spv::Op::OpTypeStruct || + member >= structType->NumInOperands()) { + continue; + } + const uint32_t memberType = structType->GetSingleWordInOperand(member); + if (!IsInt32Type(ctx, memberType)) continue; + for (Instruction& var : ctx->module()->types_values()) { + if (var.opcode() == spv::Op::OpVariable && + static_cast(var.GetSingleWordInOperand(0)) == + spv::StorageClass::Uniform && + PointeeOf(ctx, var.type_id()) == structId) { + return {var.result_id(), member, memberType}; + } + } + } + return found; + } + + // Where the producer writes its distances out: before each vertex a geometry stage + // emits, else at the entry function's returns. + std::vector FlushSites(IRContext* ctx, Instruction* entryPoint, bool isGeometry) { + std::vector sites; + if (isGeometry) { + for (auto& function : *ctx->module()) { + function.ForEachInst([&](Instruction* inst) { + if (inst->opcode() == spv::Op::OpEmitVertex || + inst->opcode() == spv::Op::OpEmitStreamVertex) { + sites.push_back(inst); + } + }); + } + } else if (Function* entry = EntryFunction(ctx, entryPoint)) { + entry->ForEachInst([&](Instruction* inst) { + if (inst->opcode() == spv::Op::OpReturn || inst->opcode() == spv::Op::OpReturnValue) { + sites.push_back(inst); + } + }); + } + return sites; + } + + struct ScalarIds { + uint32_t floatType = 0; + uint32_t vec4Type = 0; + uint32_t boolType = 0; + uint32_t one = 0; + uint32_t zero = 0; + uint32_t ptrPrivateFloat = 0; + }; + + ScalarIds MakeScalarIds(IRContext* ctx) { + auto* typeMgr = ctx->get_type_mgr(); + ScalarIds ids; + analysis::Float floatTy(32); + ids.floatType = typeMgr->GetTypeInstruction(&floatTy); + analysis::Vector vec4Ty(typeMgr->GetRegisteredType(&floatTy), 4); + ids.vec4Type = typeMgr->GetTypeInstruction(&vec4Ty); + analysis::Bool boolTy; + ids.boolType = typeMgr->GetTypeInstruction(&boolTy); + ids.one = ctx->get_constant_mgr()->GetFloatConstId(1.0f); + ids.zero = ctx->get_constant_mgr()->GetFloatConstId(0.0f); + ids.ptrPrivateFloat = typeMgr->FindPointerToType(ids.floatType, spv::StorageClass::Private); + return ids; + } + + // Fragment stage: load the carriers at the top of the entry function, serve the + // shadow's gl_ClipDistance reads from them, and kill on any negative distance. + void EmitCarrierDiscard(IRContext* ctx, Instruction* entryPoint, const ScalarIds& ids, + Uint32 consumedCount, Uint32 location, uint32_t shadowId, + uint32_t shadowLength) { + Function* entry = EntryFunction(ctx, entryPoint); + if (entry == nullptr || entry->begin() == entry->end()) return; + auto* constMgr = ctx->get_constant_mgr(); + const Uint32 carrierCount = (consumedCount + 3) / 4; + uint32_t carriers[EmulateClipDistancePass::kMaxCarriers] = {0, 0}; + for (Uint32 k = 0; k < carrierCount; ++k) { + carriers[k] = AddGlobalVariable(ctx, ids.vec4Type, spv::StorageClass::Input, + EmulateClipDistancePass::CarrierName(k)); + AddLocationDecoration(ctx, carriers[k], location + k); + entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {carriers[k]}}); + } + BasicBlock* head = &*entry->begin(); + auto split = head->begin(); + while (split != head->end() && split->opcode() == spv::Op::OpVariable) + ++split; + BasicBlock* rest = head->SplitBasicBlock(ctx, ctx->TakeNextId(), split); + + InstructionBuilder b(ctx, head, kBuilderAnalyses); + uint32_t loaded[EmulateClipDistancePass::kMaxCarriers] = {0, 0}; + for (Uint32 k = 0; k < carrierCount; ++k) { + loaded[k] = b.AddLoad(ids.vec4Type, carriers[k])->result_id(); + } + uint32_t anyNegative = 0; + for (Uint32 i = 0; i < consumedCount; ++i) { + const uint32_t component = + b.AddCompositeExtract(ids.floatType, loaded[i / 4], {i % 4})->result_id(); + if (shadowId != 0 && i < shadowLength) { + Instruction* ptr = + b.AddAccessChain(ids.ptrPrivateFloat, shadowId, {constMgr->GetUIntConstId(i)}); + b.AddStore(ptr->result_id(), component); + } + const uint32_t negative = + b.AddBinaryOp(ids.boolType, spv::Op::OpFOrdLessThan, component, ids.zero)->result_id(); + anyNegative = + anyNegative == 0 + ? negative + : b.AddBinaryOp(ids.boolType, spv::Op::OpLogicalOr, anyNegative, negative)->result_id(); + } + const uint32_t killLabel = ctx->TakeNextId(); + b.AddConditionalBranch(anyNegative, killLabel, rest->id(), rest->id()); + + auto killBlock = spvtools::MakeUnique(spvtools::MakeUnique( + ctx, spv::Op::OpLabel, 0, killLabel, std::initializer_list{})); + killBlock->AddInstruction(spvtools::MakeUnique(ctx, spv::Op::OpKill, 0, 0, + std::initializer_list{})); + entry->InsertBasicBlockAfter(std::move(killBlock), head); + } } // namespace ClipDistanceModuleProbe EmulateClipDistancePass::ProbeModule(const Vector& spirv) { @@ -311,7 +565,8 @@ namespace MobileGL { } return Status::SuccessWithoutChange; }; - if (m_options.role == Role::Consumer && !isFragment) return decline("consumer role on a non-fragment stage"); + if (m_options.role == Role::Consumer && !isFragment) + return decline("consumer role on a non-fragment stage"); if (m_options.role != Role::Consumer && !isPreRaster) { return decline("producer/redirect role on a non-pre-rasterization stage"); } @@ -324,21 +579,14 @@ namespace MobileGL { 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 + 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) { - if (shadowTypeId == 0) { - shadowTypeId = typeId; - return true; - } - return shadowTypeId == typeId; - }; for (Instruction& inst : ctx->module()->types_values()) { if (inst.opcode() != spv::Op::OpVariable) continue; @@ -359,7 +607,9 @@ namespace MobileGL { if (storage == spv::StorageClass::Input && !isFragment) { return decline("a standalone gl_ClipDistance input outside the fragment stage"); } - if (!noteShadowType(pointeeId)) return decline("two differently sized gl_ClipDistance arrays"); + if (!NoteShadowType(shadowTypeId, pointeeId)) { + return decline("two differently sized gl_ClipDistance arrays"); + } if (!PointerUsesAreRetypable(ctx, &inst, reason)) return decline(Move(reason)); standaloneVars.push_back(&inst); continue; @@ -373,112 +623,28 @@ namespace MobileGL { pointee = defUse->GetDef(pointee->GetSingleWordInOperand(0)); if (pointee == nullptr) continue; } - uint32_t clipMember = 0; - if (pointee->opcode() != spv::Op::OpTypeStruct || !ClipMemberOf(sites, pointee->result_id(), clipMember)) { + BlockVariable block{&inst, storage, arrayed, 0, 0}; + if (pointee->opcode() != spv::Op::OpTypeStruct || + !ClipMemberOf(sites, pointee->result_id(), block.clipMember)) { continue; } - const uint32_t memberTypeId = pointee->GetSingleWordInOperand(clipMember); - const uint32_t memberOperand = arrayed ? 2u : 1u; // [vertex,] member, ... + block.memberTypeId = pointee->GetSingleWordInOperand(block.clipMember); if (native && !arrayed && storage == spv::StorageClass::Output) { nativeBlockVar = &inst; - nativeBlockMember = clipMember; + nativeBlockMember = block.clipMember; + } + if (!CollectBlockClipChains(ctx, block, shadowTypeId, memberChains, reason)) { + return decline(Move(reason)); } - bool declined = false; - defUse->ForEachUser(&inst, [&](Instruction* user) { - if (declined) return; - switch (user->opcode()) { - case spv::Op::OpEntryPoint: - case spv::Op::OpName: - case spv::Op::OpDecorate: - return; - case spv::Op::OpAccessChain: - case spv::Op::OpInBoundsAccessChain: { - if (user->NumInOperands() <= memberOperand) { - // Stops short of the member: a whole block (or gl_in[i]) - // pointer. Harmless unless something then reaches the clip - // member through it or copies the whole struct. - bool touches = false; - defUse->ForEachUser(user, [&](Instruction* next) { - if (next->opcode() == spv::Op::OpAccessChain || - next->opcode() == spv::Op::OpInBoundsAccessChain) { - uint32_t member = 0; - if (next->NumInOperands() < 2 || - !PlainConstantValue(ctx, next->GetSingleWordInOperand(1), member) || - member == clipMember) { - touches = true; - } - } else if (next->opcode() != spv::Op::OpName) { - touches = true; // whole-struct load/store/copy - } - }); - if (touches) { - declined = true; - reason = "a split access chain or whole-gl_PerVertex copy reaching gl_ClipDistance"; - } - return; - } - uint32_t member = 0; - if (!PlainConstantValue(ctx, user->GetSingleWordInOperand(memberOperand), member)) { - declined = true; - reason = "a non-constant gl_PerVertex member index"; - return; - } - if (member != clipMember) return; // another member - if (arrayed) { - declined = true; - reason = storage == spv::StorageClass::Input - ? "a gl_in[].gl_ClipDistance read" - : "a gl_out[].gl_ClipDistance write"; - return; - } - if (!noteShadowType(memberTypeId)) { - declined = true; - reason = "two differently sized gl_ClipDistance arrays"; - return; - } - if (!PointerUsesAreRetypable(ctx, user, reason)) { - declined = true; - return; - } - memberChains.push_back(user); - return; - } - case spv::Op::OpLoad: - case spv::Op::OpStore: - case spv::Op::OpCopyMemory: - declined = true; - reason = "a whole-aggregate load/store/copy of the gl_PerVertex interface"; - return; - default: - declined = true; - reason = std::format("SPIR-V opcode {} reaching the gl_PerVertex interface", - static_cast(user->opcode())); - return; - } - }); - if (declined) return decline(Move(reason)); } const bool hasAccesses = !memberChains.empty() || !standaloneVars.empty(); if (shadowTypeId == 0 && m_options.role == Role::Producer) { - // A producer with nothing redirected may still have declared the carrier - // length the driver planned with; take it from its Output declaration. - for (Instruction& inst : ctx->module()->types_values()) { - if (inst.opcode() != spv::Op::OpVariable || - static_cast(inst.GetSingleWordInOperand(0)) != spv::StorageClass::Output) { - continue; - } - Instruction* type = defUse->GetDef(PointeeOf(ctx, inst.type_id())); - uint32_t member = 0; - if (type != nullptr && type->opcode() == spv::Op::OpTypeStruct && - ClipMemberOf(sites, type->result_id(), member)) { - shadowTypeId = type->GetSingleWordInOperand(member); - break; - } - } + shadowTypeId = DeclaredOutputClipType(ctx, sites); } const uint32_t shadowLength = shadowTypeId != 0 ? FloatArrayLength(ctx, shadowTypeId) : 0; - if (shadowTypeId != 0 && shadowLength == 0) return decline("gl_ClipDistance is not a sized float array"); + if (shadowTypeId != 0 && shadowLength == 0) + return decline("gl_ClipDistance is not a sized float array"); Uint32 producedCount = 0; if (m_options.role == Role::Producer) { @@ -488,38 +654,11 @@ namespace MobileGL { const Uint32 consumedCount = m_options.role == Role::Consumer ? std::min(m_options.distanceCount, kMaxCarriers * 4) : 0; - // The producer's enables uniform: a member of a Uniform-storage struct, by name. - uint32_t enablesVarId = 0; - uint32_t enablesMemberIndex = 0; - uint32_t enablesTypeId = 0; - if (producedCount > 0 && !m_options.enablesMemberName.empty()) { - for (Instruction& debug : ctx->module()->debugs2()) { - if (enablesVarId != 0) break; - if (debug.opcode() != spv::Op::OpMemberName || debug.NumInOperands() < 3) continue; - if (debug.GetOperand(2).AsString() != m_options.enablesMemberName) continue; - const uint32_t structId = debug.GetSingleWordInOperand(0); - const uint32_t member = debug.GetSingleWordInOperand(1); - Instruction* structType = defUse->GetDef(structId); - if (structType == nullptr || structType->opcode() != spv::Op::OpTypeStruct || - member >= structType->NumInOperands()) { - continue; - } - const uint32_t memberType = structType->GetSingleWordInOperand(member); - if (!IsInt32Type(ctx, memberType)) continue; - for (Instruction& var : ctx->module()->types_values()) { - if (var.opcode() != spv::Op::OpVariable || - static_cast(var.GetSingleWordInOperand(0)) != - spv::StorageClass::Uniform || - PointeeOf(ctx, var.type_id()) != structId) { - continue; - } - enablesVarId = var.result_id(); - enablesMemberIndex = member; - enablesTypeId = memberType; - break; - } - } - } + const EnablesUniform enables = producedCount > 0 && !m_options.enablesMemberName.empty() + ? FindEnablesUniform(ctx, m_options.enablesMemberName) + : EnablesUniform{}; + const uint32_t enablesVarId = enables.varId; + const uint32_t enablesTypeId = enables.typeId; if (!hasAccesses && sites.capability == nullptr && producedCount == 0 && consumedCount == 0) { return Status::SuccessWithoutChange; @@ -573,16 +712,11 @@ namespace MobileGL { if (shadowId != 0) RetypeChainUsers(ctx, defUse->GetDef(shadowId)); if (sites.capability != nullptr && !native) ctx->KillInst(sites.capability); - analysis::Float floatTy(32); - const uint32_t floatTypeId = typeMgr->GetTypeInstruction(&floatTy); - const analysis::Type* floatReg = typeMgr->GetRegisteredType(&floatTy); - analysis::Vector vec4Ty(floatReg, 4); - const uint32_t vec4TypeId = typeMgr->GetTypeInstruction(&vec4Ty); - analysis::Bool boolTy; - const uint32_t boolTypeId = typeMgr->GetTypeInstruction(&boolTy); - const uint32_t oneId = constMgr->GetFloatConstId(1.0f); - const uint32_t zeroId = constMgr->GetFloatConstId(0.0f); - const uint32_t ptrPrivateFloat = typeMgr->FindPointerToType(floatTypeId, spv::StorageClass::Private); + const ScalarIds ids = MakeScalarIds(ctx); + const uint32_t floatTypeId = ids.floatType; + const uint32_t vec4TypeId = ids.vec4Type; + const uint32_t boolTypeId = ids.boolType; + const uint32_t oneId = ids.one; // ---- producer: write the carriers ---- if (producedCount > 0) { @@ -590,7 +724,8 @@ namespace MobileGL { 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; + 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); @@ -600,7 +735,7 @@ namespace MobileGL { uint32_t enablesMemberConst = 0; if (enablesVarId != 0) { ptrUniformEnables = typeMgr->FindPointerToType(enablesTypeId, spv::StorageClass::Uniform); - enablesMemberConst = constMgr->GetSIntConstId(static_cast(enablesMemberIndex)); + enablesMemberConst = constMgr->GetSIntConstId(static_cast(enables.memberIndex)); if (interfaceListsAllGlobals) { bool listed = false; for (uint32_t i = 3; i < entryPoint->NumInOperands(); ++i) { @@ -610,24 +745,7 @@ namespace MobileGL { } } - std::vector flushSites; - if (isGeometry) { - for (auto& function : *ctx->module()) { - function.ForEachInst([&](Instruction* inst) { - if (inst->opcode() == spv::Op::OpEmitVertex || - inst->opcode() == spv::Op::OpEmitStreamVertex) { - flushSites.push_back(inst); - } - }); - } - } else if (Function* entry = EntryFunction(ctx, entryPoint)) { - entry->ForEachInst([&](Instruction* inst) { - if (inst->opcode() == spv::Op::OpReturn || inst->opcode() == spv::Op::OpReturnValue) { - flushSites.push_back(inst); - } - }); - } - for (Instruction* site : flushSites) { + for (Instruction* site : FlushSites(ctx, entryPoint, isGeometry)) { InstructionBuilder b(ctx, site, kBuilderAnalyses); uint32_t maskId = 0; if (enablesVarId != 0) { @@ -636,13 +754,13 @@ namespace MobileGL { } std::vector values; for (Uint32 i = 0; i < producedCount; ++i) { - Instruction* ptr = b.AddAccessChain(ptrPrivateFloat, shadowId, {constMgr->GetUIntConstId(i)}); + Instruction* ptr = + b.AddAccessChain(ids.ptrPrivateFloat, shadowId, {constMgr->GetUIntConstId(i)}); uint32_t value = b.AddLoad(floatTypeId, ptr->result_id())->result_id(); if (maskId != 0) { - const uint32_t bit = - b.AddBinaryOp(enablesTypeId, spv::Op::OpBitwiseAnd, maskId, - IntConstantOfType(ctx, enablesTypeId, 1u << i)) - ->result_id(); + const uint32_t bit = b.AddBinaryOp(enablesTypeId, spv::Op::OpBitwiseAnd, maskId, + IntConstantOfType(ctx, enablesTypeId, 1u << i)) + ->result_id(); const uint32_t on = b.AddBinaryOp(boolTypeId, spv::Op::OpINotEqual, bit, IntConstantOfType(ctx, enablesTypeId, 0u)) ->result_id(); @@ -661,11 +779,12 @@ namespace MobileGL { b.AddStore(ptr->result_id(), values[i]); } } - while (values.size() % 4 != 0) values.push_back(oneId); + while (values.size() % 4 != 0) + values.push_back(oneId); for (Uint32 k = 0; k < carrierCount; ++k) { const uint32_t composite = - b.AddCompositeConstruct(vec4TypeId, {values[k * 4], values[k * 4 + 1], values[k * 4 + 2], - values[k * 4 + 3]}) + b.AddCompositeConstruct(vec4TypeId, {values[k * 4], values[k * 4 + 1], + values[k * 4 + 2], values[k * 4 + 3]}) ->result_id(); b.AddStore(carriers[k], composite); } @@ -678,50 +797,7 @@ namespace MobileGL { // ---- consumer: read the carriers, discard, serve gl_ClipDistance reads ---- if (m_options.role == Role::Consumer && consumedCount > 0) { - Function* entry = EntryFunction(ctx, entryPoint); - if (entry != nullptr && entry->begin() != entry->end()) { - const Uint32 carrierCount = (consumedCount + 3) / 4; - uint32_t carriers[kMaxCarriers] = {0, 0}; - for (Uint32 k = 0; k < carrierCount; ++k) { - carriers[k] = AddGlobalVariable(ctx, vec4TypeId, spv::StorageClass::Input, CarrierName(k)); - AddLocationDecoration(ctx, carriers[k], m_options.location + k); - entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {carriers[k]}}); - } - BasicBlock* head = &*entry->begin(); - auto split = head->begin(); - while (split != head->end() && split->opcode() == spv::Op::OpVariable) ++split; - BasicBlock* rest = head->SplitBasicBlock(ctx, ctx->TakeNextId(), split); - - InstructionBuilder b(ctx, head, kBuilderAnalyses); - uint32_t loaded[kMaxCarriers] = {0, 0}; - for (Uint32 k = 0; k < carrierCount; ++k) { - loaded[k] = b.AddLoad(vec4TypeId, carriers[k])->result_id(); - } - uint32_t anyNegative = 0; - for (Uint32 i = 0; i < consumedCount; ++i) { - const uint32_t component = - b.AddCompositeExtract(floatTypeId, loaded[i / 4], {i % 4})->result_id(); - if (shadowId != 0 && i < shadowLength) { - Instruction* ptr = - b.AddAccessChain(ptrPrivateFloat, shadowId, {constMgr->GetUIntConstId(i)}); - b.AddStore(ptr->result_id(), component); - } - const uint32_t negative = - b.AddBinaryOp(boolTypeId, spv::Op::OpFOrdLessThan, component, zeroId)->result_id(); - anyNegative = anyNegative == 0 - ? negative - : b.AddBinaryOp(boolTypeId, spv::Op::OpLogicalOr, anyNegative, negative) - ->result_id(); - } - const uint32_t killLabel = ctx->TakeNextId(); - b.AddConditionalBranch(anyNegative, killLabel, rest->id(), rest->id()); - - auto killBlock = spvtools::MakeUnique(spvtools::MakeUnique( - ctx, spv::Op::OpLabel, 0, killLabel, std::initializer_list{})); - killBlock->AddInstruction(spvtools::MakeUnique( - ctx, spv::Op::OpKill, 0, 0, std::initializer_list{})); - entry->InsertBasicBlockAfter(std::move(killBlock), head); - } + EmitCarrierDiscard(ctx, entryPoint, ids, consumedCount, m_options.location, shadowId, shadowLength); } if (m_report != nullptr) m_report->rewrote = true; diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h index b127fbf7..771b2757 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h @@ -128,8 +128,8 @@ namespace MobileGL { static ClipDistanceModuleProbe ProbeModule(const Vector& spirv); - static spvtools::Optimizer::PassToken CreateEmulateClipDistancePass( - EmulateClipDistanceOptions options, EmulateClipDistanceReport* report); + static spvtools::Optimizer::PassToken CreateEmulateClipDistancePass(EmulateClipDistanceOptions options, + EmulateClipDistanceReport* report); private: EmulateClipDistanceOptions m_options;