[Fix] (MG_Backend/DirectVulkan, MG_Util/ShaderTranspiler): apply GL_CLIP_DISTANCEi in the shader where Magma clips natively

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