mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-08 02:23:23 +09:00
[Fix] (MG_Util/ShaderTranspiler, MG_Util/SelfTest, MG_Test): split clip distance code to fit the size and format checks
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<CapabilityInput>(static_cast<Uint>(CapabilityInput::ClipDistance0) + i);
|
||||
const auto cap = static_cast<CapabilityInput>(static_cast<Uint>(CapabilityInput::ClipDistance0) + i);
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(cap)) enables |= 1u << i;
|
||||
}
|
||||
program->WriteReservedClipDistanceEnables(enables);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,6 +45,7 @@ enable_testing()
|
||||
add_executable(
|
||||
SanityTest
|
||||
SanityTest.cpp
|
||||
SanityClipDistanceTest.cpp
|
||||
)
|
||||
target_link_libraries(
|
||||
SanityTest
|
||||
|
||||
@@ -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 <gtest/gtest.h>
|
||||
|
||||
#include <MG_Backend/DirectGLES/BackendObject_DirectGLES.h>
|
||||
#include <MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<GLenum> 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<GLenum> 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<GLenum> 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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ClipDistancePostRow> 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
|
||||
|
||||
@@ -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 <Config.h>
|
||||
#include <format>
|
||||
|
||||
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<ClipDistancePostRow> GlesClipDistancePostRows(const MG_External::GLESCapabilities& caps,
|
||||
const MG_External::GLESFunctionsTable& glesFuncs) {
|
||||
Vector<ClipDistancePostRow> 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<ClipDistancePostRow> VulkanClipDistancePostRows(const Bool hasClipDistance, const Bool hasCullDistance,
|
||||
const Uint32 maxClipDistances,
|
||||
const Uint32 maxCullDistances) {
|
||||
Vector<ClipDistancePostRow> 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
|
||||
@@ -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 <Includes.h>
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
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<ClipDistancePostRow> 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<ClipDistancePostRow> VulkanClipDistancePostRows(Bool hasClipDistance, Bool hasCullDistance,
|
||||
Uint32 maxClipDistances, Uint32 maxCullDistances);
|
||||
} // namespace MobileGL::MG_Util::SelfTest
|
||||
@@ -1216,24 +1216,33 @@ namespace MobileGL {
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::EmulateClipDistanceForProgram(Vector<Vector<Uint32>>& modules,
|
||||
const Vector<GLenum>& shaderTypes,
|
||||
const Bool captureRequestsClipDistance,
|
||||
const Uint32 maxVaryingLocations,
|
||||
ClipDistanceEmulationOutcome& outcome,
|
||||
const bool validateOutput,
|
||||
const bool enableSpirvValidation, const bool nativeMask) {
|
||||
Bool ShaderCompiler::EmulateClipDistanceForProgram(
|
||||
Vector<Vector<Uint32>>& modules, const Vector<GLenum>& 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<Int>(i); break;
|
||||
case GL_TESS_CONTROL_SHADER: stageIndex[1] = static_cast<Int>(i); break;
|
||||
case GL_TESS_EVALUATION_SHADER: stageIndex[2] = static_cast<Int>(i); break;
|
||||
case GL_GEOMETRY_SHADER: stageIndex[3] = static_cast<Int>(i); break;
|
||||
case GL_FRAGMENT_SHADER: stageIndex[4] = static_cast<Int>(i); break;
|
||||
default: break;
|
||||
case GL_VERTEX_SHADER:
|
||||
stageIndex[0] = static_cast<Int>(i);
|
||||
break;
|
||||
case GL_TESS_CONTROL_SHADER:
|
||||
stageIndex[1] = static_cast<Int>(i);
|
||||
break;
|
||||
case GL_TESS_EVALUATION_SHADER:
|
||||
stageIndex[2] = static_cast<Int>(i);
|
||||
break;
|
||||
case GL_GEOMETRY_SHADER:
|
||||
stageIndex[3] = static_cast<Int>(i);
|
||||
break;
|
||||
case GL_FRAGMENT_SHADER:
|
||||
stageIndex[4] = static_cast<Int>(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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<Instruction*>& 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<uint32_t>(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<spv::StorageClass>(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<spv::StorageClass>(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<Instruction*> FlushSites(IRContext* ctx, Instruction* entryPoint, bool isGeometry) {
|
||||
std::vector<Instruction*> 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<BasicBlock>(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLabel, 0, killLabel, std::initializer_list<Operand>{}));
|
||||
killBlock->AddInstruction(spvtools::MakeUnique<Instruction>(ctx, spv::Op::OpKill, 0, 0,
|
||||
std::initializer_list<Operand>{}));
|
||||
entry->InsertBasicBlockAfter(std::move(killBlock), head);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ClipDistanceModuleProbe EmulateClipDistancePass::ProbeModule(const Vector<Uint32>& 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<Instruction*> memberChains; // [clipMember, rest...] on a non-arrayed block
|
||||
std::vector<Instruction*> standaloneVars; // standalone ClipDistance variables
|
||||
std::vector<Instruction*> memberChains; // [clipMember, rest...] on a non-arrayed block
|
||||
std::vector<Instruction*> 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<uint32_t>(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<spv::StorageClass>(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<Uint32>(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<spv::StorageClass>(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<int32_t>(nativeBlockMember)) : 0;
|
||||
nativeBlockVar != nullptr ? constMgr->GetSIntConstId(static_cast<int32_t>(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<int32_t>(enablesMemberIndex));
|
||||
enablesMemberConst = constMgr->GetSIntConstId(static_cast<int32_t>(enables.memberIndex));
|
||||
if (interfaceListsAllGlobals) {
|
||||
bool listed = false;
|
||||
for (uint32_t i = 3; i < entryPoint->NumInOperands(); ++i) {
|
||||
@@ -610,24 +745,7 @@ namespace MobileGL {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Instruction*> 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<uint32_t> 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<BasicBlock>(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpLabel, 0, killLabel, std::initializer_list<Operand>{}));
|
||||
killBlock->AddInstruction(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpKill, 0, 0, std::initializer_list<Operand>{}));
|
||||
entry->InsertBasicBlockAfter(std::move(killBlock), head);
|
||||
}
|
||||
EmitCarrierDiscard(ctx, entryPoint, ids, consumedCount, m_options.location, shadowId, shadowLength);
|
||||
}
|
||||
|
||||
if (m_report != nullptr) m_report->rewrote = true;
|
||||
|
||||
@@ -128,8 +128,8 @@ namespace MobileGL {
|
||||
|
||||
static ClipDistanceModuleProbe ProbeModule(const Vector<Uint32>& spirv);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateEmulateClipDistancePass(
|
||||
EmulateClipDistanceOptions options, EmulateClipDistanceReport* report);
|
||||
static spvtools::Optimizer::PassToken CreateEmulateClipDistancePass(EmulateClipDistanceOptions options,
|
||||
EmulateClipDistanceReport* report);
|
||||
|
||||
private:
|
||||
EmulateClipDistanceOptions m_options;
|
||||
|
||||
Reference in New Issue
Block a user