[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:
2026-11-07 07:27:58 -04:00
parent cdf43a4a59
commit cefb70d1a7
19 changed files with 718 additions and 517 deletions
+1
View File
@@ -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
+1
View File
@@ -45,6 +45,7 @@ enable_testing()
add_executable(
SanityTest
SanityTest.cpp
SanityClipDistanceTest.cpp
)
target_link_libraries(
SanityTest
+121
View File
@@ -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);
}
-105
View File
@@ -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).
+2 -2
View File
@@ -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
+19 -90
View File
@@ -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;