mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[Fix] (MG_Backend/DirectVulkan, MG_Util/ShaderTranspiler): apply GL_CLIP_DISTANCEi in the shader where Magma clips natively
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user