mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[Feat] (MG_Util/ShaderTranspiler, MG_Backend, MG_Util/SelfTest): emulate gl_ClipDistance where the backend cannot clip natively and report it in POST
This commit is contained in:
@@ -444,6 +444,7 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
+19
-10
@@ -214,16 +214,16 @@ namespace MobileGL {
|
||||
Int MaxComputeImageUniforms = 8;
|
||||
Int MaxDrawBuffers = 8;
|
||||
Int MaxColorAttachments = 8;
|
||||
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
|
||||
// backend that cannot host a clip distance MUST report it: advertising eight the
|
||||
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
|
||||
// from an honest "unsupported" at query time to a backend shader-compile error the
|
||||
// frontend never surfaces, after which every draw with that program silently renders
|
||||
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
|
||||
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
|
||||
// because it describes the no-backend case (standalone shader compiles, unit tests),
|
||||
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
||||
// hand glslang a workable gl_MaxClipDistances.
|
||||
// GL_MAX_CLIP_DISTANCES. What the backend can actually CLIP with, natively or through
|
||||
// EmulatesClipDistance below - never a number the backend would then refuse:
|
||||
// advertising eight that no stage can honor only moves the failure from query time to
|
||||
// a backend shader-compile error the frontend never surfaces. DirectGLES fills it from
|
||||
// GL_EXT_clip_cull_distance, DirectVulkan from the shaderClipDistance device feature,
|
||||
// and both answer 8 when they emulate. Zero stays a legal answer for a backend that
|
||||
// neither hosts nor emulates the built-in (MOBILEGL_CLIP_DISTANCE_EMULATION=0 on such
|
||||
// a device). The DEFAULT is the GL 4.3 core minimum because it describes the
|
||||
// no-backend case (standalone shader compiles, unit tests), where
|
||||
// BuildTBuiltInResource still has to hand glslang a workable gl_MaxClipDistances.
|
||||
Int MaxClipDistances = 8;
|
||||
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
|
||||
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
|
||||
@@ -349,6 +349,15 @@ namespace MobileGL {
|
||||
Bool SupportsTessellationPointSize = true;
|
||||
// The geometry-stage twin (OpCapability GeometryPointSize).
|
||||
Bool SupportsGeometryPointSize = true;
|
||||
// The backend cannot clip with gl_ClipDistance natively - no
|
||||
// GL_EXT_clip_cull_distance, an extension the POST clip probe caught not clipping or
|
||||
// not honoring the per-distance enables, or shaderClipDistance == VK_FALSE - so
|
||||
// ProgramSpirvTask lowers the built-in to varyings plus a fragment discard
|
||||
// (ShaderCompiler::EmulateClipDistanceForProgram) and the source pipeline declares the
|
||||
// reserved enables uniform the draw path feeds. MOBILEGL_CLIP_DISTANCE_EMULATION
|
||||
// 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;
|
||||
Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -237,6 +237,14 @@ namespace MobileGL::MG_Config {
|
||||
// negative control). Cross-backend by design: the demotion runs in the shared
|
||||
// phase-B chain, so one switch covers both. See DemotePointSizePass.
|
||||
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
|
||||
// MOBILEGL_CLIP_DISTANCE_EMULATION: lower gl_ClipDistance to ordinary varyings plus a
|
||||
// fragment discard (ShaderCompiler::EmulateClipDistanceForProgram) on a backend that
|
||||
// cannot clip natively - no GL_EXT_clip_cull_distance or a driver the POST clip probe
|
||||
// caught misbehaving (DirectGLES), shaderClipDistance == VK_FALSE (DirectVulkan).
|
||||
// Auto arms it exactly there. ForceOn exercises it on a healthy driver (the pinned
|
||||
// integration lane); ForceOff restores the native-or-nothing answer, which reports
|
||||
// GL_MAX_CLIP_DISTANCES = 0 on a device without native support.
|
||||
QuirkOverride ClipDistanceEmulation = QuirkOverride::Auto;
|
||||
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
||||
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||
|
||||
@@ -402,6 +402,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.EsprytAtomicSubgroupScratch = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_ATOMIC_SUBGROUP_SCRATCH");
|
||||
features.EsprytInvalidImagePlaceholder = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_INVALID_IMAGE_PLACEHOLDER");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.ClipDistanceEmulation = QueryEnvQuirkOverride("MOBILEGL_CLIP_DISTANCE_EMULATION");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||
|
||||
@@ -1996,12 +1996,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
|
||||
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
|
||||
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
|
||||
// Eight when emulated - the GL 3.0+ minimum, and what EmulateClipDistancePass carries -
|
||||
// else the driver's own number (0 without the extension and with the emulation forced
|
||||
// off: the native-or-nothing answer).
|
||||
m_dynamicParameters.EmulatesClipDistance = m_GLESCapabilities.EmulatesClipDistance;
|
||||
m_dynamicParameters.MaxClipDistances =
|
||||
m_GLESCapabilities.EmulatesClipDistance ? 8 : m_GLESCapabilities.MaxClipDistances;
|
||||
// The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
|
||||
// is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
|
||||
// would reject.
|
||||
m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances =
|
||||
std::max(m_GLESCapabilities.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances);
|
||||
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
|
||||
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
|
||||
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
|
||||
|
||||
@@ -514,7 +514,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance) {
|
||||
if (tailSpanDirty && g_GLESCapabilities.SupportsClipDistance && !g_GLESCapabilities.EmulatesClipDistance) {
|
||||
// gl_ClipDistance clipping is per-distance enable state in GL, and ES reaches it
|
||||
// only through GL_EXT_clip_cull_distance. That extension reuses the desktop enum
|
||||
// values for CLIP_DISTANCE0_EXT..7_EXT, but the token is absent from the ES
|
||||
|
||||
@@ -1343,8 +1343,34 @@ 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;
|
||||
}
|
||||
m_dynamicParameters.EmulatesClipDistance = emulateClipDistance;
|
||||
m_dynamicParameters.MaxClipDistances =
|
||||
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)" : "native", 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.
|
||||
@@ -1357,7 +1383,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
|
||||
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
|
||||
m_dynamicParameters.MaxCullDistances})
|
||||
: 0;
|
||||
: m_dynamicParameters.MaxClipDistances;
|
||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
||||
// Assigned explicitly rather than left to the struct's defaults, like every other
|
||||
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
|
||||
|
||||
@@ -108,6 +108,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
program->WriteReservedNumSamples(static_cast<Int>(std::max<GLint>(ResolveDrawFramebufferSampleCount(), 1)));
|
||||
}
|
||||
|
||||
// The GL_CLIP_DISTANCE0..7 enables, for a program whose clip distances are emulated (see
|
||||
// EmulateClipDistancePass): the producing stage selects 1.0 for every disabled distance, so
|
||||
// the mask is a property of the render state at THIS draw, not of the program. Same cost
|
||||
// model as the sample count above.
|
||||
static void PublishClipDistanceEnables(const SharedPtr<MG_State::GLState::ProgramObject>& program) {
|
||||
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);
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(cap)) enables |= 1u << i;
|
||||
}
|
||||
program->WriteReservedClipDistanceEnables(enables);
|
||||
}
|
||||
|
||||
// The one funnel every drawing command passes through. Order is load-bearing: validate first
|
||||
// (a rejected draw must leave state alone), then publish the sample count - which reads the
|
||||
// DRAW FRAMEBUFFER binding, so it has to run after the caller's framebuffer state is settled
|
||||
@@ -133,6 +148,7 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||
if (!ValidateResolvedProgramForDraw(currentProgram, functionName)) return false;
|
||||
PublishDrawFramebufferSampleCount(currentProgram);
|
||||
PublishClipDistanceEnables(currentProgram);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@ Configuration-specific controls are selected separately and must pass without sk
|
||||
## Unresolved behavior
|
||||
|
||||
- `ClipDistanceScenario.ADisabledClipDistanceRemovesNothing` and
|
||||
`TheEnablesAreIndependentPerDistance`: per-distance enable semantics are missing
|
||||
on the tested GLES and Vulkan paths. Neither backend supplies a passing control
|
||||
on local llvmpipe/lavapipe. The strict list names the GLES and Magma Fm entries.
|
||||
`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.
|
||||
- `PartialUploadOverGpuContentScenario.OneDimensionalArraySubImageKeepsTheGpuClearedLayer`:
|
||||
the Vulkan path cannot attach the 1D array layer. GLES passes, which does not
|
||||
establish Vulkan coverage.
|
||||
|
||||
@@ -26,16 +26,15 @@
|
||||
// identical distances and the enable turned off must leave both sides painted, which is what
|
||||
// says the pixels below are being removed by clipping and not by something else.
|
||||
//
|
||||
// HONEST LIMIT OF THIS FILE IN CI. Of the four cases, only EnableIsObservableThroughIsEnabled is
|
||||
// falsifiable on the software rasterizers every automated lane runs on. llvmpipe and lavapipe
|
||||
// clip by EVERY declared gl_ClipDistance regardless of the enables, so
|
||||
// AnEnabledClipDistanceRemovesTheNegativeHalf goes green there against the broken tree as well,
|
||||
// and the two cases that need real per-distance semantics skip (see
|
||||
// DriverHonorsPerDistanceEnables). What actually pins the behavior is Adreno, through
|
||||
// KHR-GLxx.clip_distance.functional - whose "without dynamic redeclaration" variants declare all
|
||||
// gl_MaxClipDistances slots and enable only the first N, i.e. exactly the subset semantics these
|
||||
// skipped cases assert. Read a green CI run here as "the state survives the frontend", not as
|
||||
// "clipping is correct"; the second claim is a device claim.
|
||||
// WHAT CI CAN AND CANNOT FALSIFY. llvmpipe's ES driver clips by EVERY written gl_ClipDistance
|
||||
// regardless of the enables, which the DirectGLES loader's native clip probe catches - so on
|
||||
// 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
|
||||
// the device, through KHR-GLxx.clip_distance.functional.
|
||||
//
|
||||
// Every case disables all eight distances on entry rather than assuming they start off:
|
||||
// XfbAfterClipDistanceScenario deliberately leaves one enabled for the rest of the process, and
|
||||
@@ -83,13 +82,33 @@ void main() {
|
||||
const char* const kFragmentSource = R"(#version 400 core
|
||||
out vec4 fragColor;
|
||||
void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
// The field report's shape (Mali-G615 / Espryt, where GL_MAX_CLIP_DISTANCES used to be 0
|
||||
// and this did not even compile): a #version 330 core stage writing all eight distances
|
||||
// from uniform planes, implicitly sized by use.
|
||||
const char* const kEightPlaneVertexSource = R"(#version 330 core
|
||||
uniform vec4 u_ClipPlane[8];
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vec4 eyePos = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
gl_Position = eyePos;
|
||||
gl_ClipDistance[0] = dot(u_ClipPlane[0], eyePos);
|
||||
gl_ClipDistance[1] = dot(u_ClipPlane[1], eyePos);
|
||||
gl_ClipDistance[2] = dot(u_ClipPlane[2], eyePos);
|
||||
gl_ClipDistance[3] = dot(u_ClipPlane[3], eyePos);
|
||||
gl_ClipDistance[4] = dot(u_ClipPlane[4], eyePos);
|
||||
gl_ClipDistance[5] = dot(u_ClipPlane[5], eyePos);
|
||||
gl_ClipDistance[6] = dot(u_ClipPlane[6], eyePos);
|
||||
gl_ClipDistance[7] = dot(u_ClipPlane[7], eyePos);
|
||||
}
|
||||
)";
|
||||
|
||||
class ClipDistanceScenario : public ScenarioTest {
|
||||
protected:
|
||||
GLuint BuildProgram() {
|
||||
GLuint BuildProgram(const char* vertexSource = kVertexSource) {
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glShaderSource(vs, 1, &vertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
GLint compiled = 0;
|
||||
glGetShaderiv(vs, GL_COMPILE_STATUS, &compiled);
|
||||
@@ -178,11 +197,12 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
// always. DirectVulkan therefore clips by a disabled distance.
|
||||
// - llvmpipe's ES driver behaves like Vulkan here.
|
||||
//
|
||||
// Emulating GL's semantics on those two would mean forcing the disabled slots to a
|
||||
// non-negative value inside the shader, which makes the enable mask part of the
|
||||
// pipeline key - a feature, not a fix, and deliberately not attempted here. The
|
||||
// cases that need the real semantics gate on this probe and say so when they skip,
|
||||
// rather than being deleted or silently weakened.
|
||||
// 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.
|
||||
bool DriverHonorsPerDistanceEnables(GLuint program, GLuint vao) const {
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||
@@ -403,4 +423,91 @@ void main() { fragColor = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
// GL 3.0+ requires eight clip distances, and the field report was a stack answering zero: a
|
||||
// #version 330 core stage writing gl_ClipDistance[0..7] did not compile. Every backend now
|
||||
// either hosts eight or emulates them (EmulateClipDistancePass), so this case requires eight
|
||||
// and does not skip. Distance 2 clips the left half, the other seven are positive, and all
|
||||
// eight are enabled - which clips identically whether or not the stack honors per-distance
|
||||
// enables. The per-distance half (distance 5 negative on the right but DISABLED) only runs
|
||||
// where the stack honors them, which every emulating backend does by construction.
|
||||
TEST_F(ClipDistanceScenario, EightDistancesFromUniformPlanesClipLikeTheReport) {
|
||||
if (!Ready()) return;
|
||||
GLint maxClipDistances = 0;
|
||||
glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances);
|
||||
ASSERT_GE(maxClipDistances, 8) << "GL 3.0+ requires GL_MAX_CLIP_DISTANCES >= 8";
|
||||
HeadlessGL& gl = Gl();
|
||||
const int width = gl.Width();
|
||||
const int height = gl.Height();
|
||||
ASSERT_GE(width, 8);
|
||||
ASSERT_GE(height, 8);
|
||||
|
||||
GLuint vao = 0;
|
||||
glGenVertexArrays(1, &vao);
|
||||
const GLuint program = BuildProgram(kEightPlaneVertexSource);
|
||||
ASSERT_NE(program, 0u) << "the eight-plane gl_ClipDistance program did not build: " << BuildLog();
|
||||
const GLint planes = glGetUniformLocation(program, "u_ClipPlane");
|
||||
ASSERT_GE(planes, 0);
|
||||
|
||||
BindDefaultFramebuffer();
|
||||
glViewport(0, 0, width, height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_CULL_FACE);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
|
||||
// w = 1 everywhere, so (0,0,0,1) is a distance of +1 and keeps everything.
|
||||
GLfloat values[8 * 4];
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
values[i * 4 + 0] = 0.0f;
|
||||
values[i * 4 + 1] = 0.0f;
|
||||
values[i * 4 + 2] = 0.0f;
|
||||
values[i * 4 + 3] = 1.0f;
|
||||
}
|
||||
values[2 * 4 + 0] = 1.0f; // distance 2 = x: negative on the left half
|
||||
values[2 * 4 + 3] = 0.0f;
|
||||
values[5 * 4 + 0] = -1.0f; // distance 5 = -x: negative on the right half
|
||||
values[5 * 4 + 3] = 0.0f;
|
||||
glUseProgram(program);
|
||||
glUniform4fv(planes, 8, values);
|
||||
|
||||
DisableEveryClipDistance();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (i != 5) glEnable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + i));
|
||||
}
|
||||
// First with distance 5 enabled too: both halves clipped, by different distances.
|
||||
glEnable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + 5));
|
||||
DrawClippedTriangle(program, vao);
|
||||
unsigned char left[4] = {0, 0, 0, 0};
|
||||
unsigned char right[4] = {0, 0, 0, 0};
|
||||
PixelAt(width / 4, height / 2, left);
|
||||
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsRed(left)) << "distance 2 is negative on the left and enabled";
|
||||
EXPECT_TRUE(IsRed(right)) << "distance 5 is negative on the right and enabled";
|
||||
|
||||
// Then distance 5 disabled: on a stack with per-distance enables the right half returns.
|
||||
glDisable(static_cast<GLenum>(GL_CLIP_DISTANCE0 + 5));
|
||||
DrawClippedTriangle(program, vao);
|
||||
PixelAt(width / 4, height / 2, left);
|
||||
PixelAt(width - 1 - width / 4, height / 2, right);
|
||||
EXPECT_EQ(FirstGLError(), 0u);
|
||||
EXPECT_TRUE(IsRed(left)) << "distance 2 is still enabled";
|
||||
const bool rightKept = IsGreen(right);
|
||||
|
||||
DisableEveryClipDistance();
|
||||
const GLuint control = BuildProgram();
|
||||
const bool honorsEnables = control != 0 && DriverHonorsPerDistanceEnables(control, vao);
|
||||
if (control != 0) glDeleteProgram(control);
|
||||
if (honorsEnables) {
|
||||
EXPECT_TRUE(rightKept) << "distance 5 is negative on the right but GL_CLIP_DISTANCE5 is disabled, so the "
|
||||
"right half must survive";
|
||||
}
|
||||
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glDeleteProgram(program);
|
||||
glDeleteVertexArrays(1, &vao);
|
||||
gl.EndFrame();
|
||||
}
|
||||
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -389,7 +389,7 @@
|
||||
F(FragmentInterpolationOffsetBits) F(SupportsWideLines) \
|
||||
F(SupportsDistinctDepthStencilAttachments) F(PerLayerFramebufferAttachmentTargets) \
|
||||
F(SupportsShaderFloat64) F(SupportsFloat64VertexAttributes) F(SupportsTessellationPointSize) \
|
||||
F(SupportsGeometryPointSize) F(MaxShaderStorageBlockSize) F(SubgroupSize) \
|
||||
F(SupportsGeometryPointSize) F(EmulatesClipDistance) F(MaxShaderStorageBlockSize) F(SubgroupSize) \
|
||||
F(SubgroupSupportedStages) F(SubgroupSupportedFeatures) F(SamplePositions) \
|
||||
F(SamplePositionCountMask) F(SubgroupQuadOperationsInAllStages) \
|
||||
F(GpuVendor) F(SupportsFragmentShadingRate) F(SupportsPrimitiveFragmentShadingRate) \
|
||||
|
||||
@@ -660,6 +660,7 @@ inline constexpr WireLayoutMember kMGPipeWireLayoutMembers[] = {
|
||||
{"DynamicBackendParameters.SupportsFloat64VertexAttributes", offsetof(DynamicBackendParameters, SupportsFloat64VertexAttributes), sizeof(((DynamicBackendParameters*)nullptr)->SupportsFloat64VertexAttributes)},
|
||||
{"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.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)},
|
||||
|
||||
@@ -337,6 +337,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
// gate has to be answerable without joining the SPIR-V job, or every draw of every
|
||||
// program would pay a join to discover it has nothing to write.
|
||||
Bool usesReservedNumSamples = false;
|
||||
// A pre-rasterization stage of this program declared the reserved clip-distance enables
|
||||
// uniform (ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME), which the source
|
||||
// pipeline does only on a backend that emulates gl_ClipDistance, so the draw path owes it
|
||||
// the GL_CLIP_DISTANCE0..7 enables before every draw. Phase A for the reason
|
||||
// usesReservedNumSamples is.
|
||||
Bool usesReservedClipDistanceEnables = false;
|
||||
// A shader of this program declared an `es` #version (ShaderTranspiler::FindEsOrigin).
|
||||
// ESSL fixes an image uniform's unit at its binding qualifier, so glUniform* on one is
|
||||
// INVALID_OPERATION for such a program and legal for a desktop one.
|
||||
@@ -415,6 +421,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
// uniformOffsets, because the member has no GL location at all: the link task keeps
|
||||
// it out of the GL-visible uniform index space so no application can see or write it.
|
||||
Uint reservedNumSamplesOffset = kInvalidUniformOffset;
|
||||
// The same, for the reserved clip-distance enables uniform.
|
||||
Uint reservedClipDistanceEnablesOffset = kInvalidUniformOffset;
|
||||
// False for a program whose SPIR-V was never produced (phase B canceled at
|
||||
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||
@@ -443,6 +451,11 @@ namespace MobileGL::MG_State::GLState {
|
||||
// deliberately keeps answering "gl_PointSize": demotion happens after phase A,
|
||||
// so every query keeps the truthful GL spelling.
|
||||
Bool pointSizeDemoted = false;
|
||||
// Whether these modules had gl_ClipDistance lowered to varyings plus a fragment discard
|
||||
// (ShaderCompiler::EmulateClipDistanceForProgram) because the backend cannot clip
|
||||
// natively. Recorded for the same reason pointSizeDemoted is: the final bytes no longer
|
||||
// declare the capability that armed it.
|
||||
Bool clipDistanceEmulated = false;
|
||||
};
|
||||
|
||||
// ---- the archive field tables (ARCHITECTURE.md §7): ONE table per type, serving both directions ----
|
||||
@@ -579,6 +592,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
v("uniformBlocksWithoutBinding", a.uniformBlocksWithoutBinding);
|
||||
v("activeUniformCount", a.activeUniformCount);
|
||||
v("usesReservedNumSamples", a.usesReservedNumSamples);
|
||||
v("usesReservedClipDistanceEnables", a.usesReservedClipDistanceEnables);
|
||||
v("esOrigin", a.esOrigin);
|
||||
v("maxUniformLocation", a.maxUniformLocation);
|
||||
v("uniformNameMaxLength", a.uniformNameMaxLength);
|
||||
@@ -614,10 +628,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
v("uniformOffsets", a.uniformOffsets);
|
||||
v("globalUboScratch", a.globalUboScratch);
|
||||
v("reservedNumSamplesOffset", a.reservedNumSamplesOffset);
|
||||
v("reservedClipDistanceEnablesOffset", a.reservedClipDistanceEnablesOffset);
|
||||
v("spirvStatus", a.spirvStatus);
|
||||
v("nativeFloat64", a.nativeFloat64);
|
||||
v("pointSizeDemoted", a.pointSizeDemoted);
|
||||
} // 8 fields
|
||||
v("clipDistanceEmulated", a.clipDistanceEmulated);
|
||||
} // 10 fields
|
||||
|
||||
// ---- trip wires ----
|
||||
// TypeFacts is a POD on every ABI: 13 Bool + 3 bytes of padding + 7 x 4-byte scalars.
|
||||
@@ -635,7 +651,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
// P7 OQ-8 added storageBlocks, one more Vector, in the disaggregated build ONLY. The pull
|
||||
// number below is the one G1 measures and it has not moved.
|
||||
#define MGL_LINKARTIFACTS_SIZE 1080
|
||||
#define MGL_SPIRVARTIFACTS_SIZE 88
|
||||
#define MGL_SPIRVARTIFACTS_SIZE 96
|
||||
#elif defined(_LIBCPP_VERSION) && (SIZE_MAX == UINT64_MAX) && defined(MGL_ARTIFACT_SIZES_LIBCXX_PINNED)
|
||||
// The integrator pins these from the NDK build (brief C.4); until then this branch is inert.
|
||||
#endif
|
||||
|
||||
@@ -1135,7 +1135,9 @@ namespace MobileGL::MG_State::GLState {
|
||||
for (const ProgramObject::XfbVarying& varying : artifacts.xfbVaryings) {
|
||||
if (varying.name == "gl_PointSize") {
|
||||
spirvHandoff.captureRequestsPointSize = true;
|
||||
break;
|
||||
}
|
||||
if (varying.name.rfind("gl_ClipDistance", 0) == 0) {
|
||||
spirvHandoff.captureRequestsClipDistance = true;
|
||||
}
|
||||
}
|
||||
// Phase B pairs this with its own SpirvArtifacts to insert the completed front end.
|
||||
@@ -1501,6 +1503,21 @@ namespace MobileGL::MG_State::GLState {
|
||||
};
|
||||
for (Int i = 0; i < tProgramUniformCount; i++) {
|
||||
const auto& uniform = artifacts.program->getUniform(i);
|
||||
// The clip-distance enables InjectClipDistanceEnablesShim declared: MobileGL's own
|
||||
// per-draw input to the emulation, hidden from the GL surface on the same terms as
|
||||
// the gl_NumSamples stand-in below. Checked BEFORE the dead-uniform filter on
|
||||
// purpose: no application code reads it - only EmulateClipDistancePass does, after
|
||||
// this reflection ran - so glslang reports it with stages == 0, and filtering it as
|
||||
// dead would leave the draw path never writing it (every distance then selects 1.0
|
||||
// and nothing clips, with no error anywhere).
|
||||
if (isGlobalUboMember(uniform) &&
|
||||
uniform.name == MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME) {
|
||||
artifacts.usesReservedClipDistanceEnables = true;
|
||||
MGLOG_D("ProgramObject %u: Reflection - reserved clip-distance enables '%s' hidden from the GL "
|
||||
"uniform surface",
|
||||
in.externalIndex, uniform.name.c_str());
|
||||
continue;
|
||||
}
|
||||
if (isGlobalUboMember(uniform) && uniform.stages == 0) {
|
||||
MGLOG_D("ProgramObject %u: Reflection - dead default-block uniform '%s' filtered from the GL "
|
||||
"surface",
|
||||
|
||||
@@ -131,6 +131,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// this bit. No new L1 key material: the key already covers
|
||||
// `requestedXfbVaryings`, of which this is a function.
|
||||
Bool captureRequestsPointSize = false;
|
||||
// The same derived bit for gl_ClipDistance (any element): the clip-distance
|
||||
// emulation declines such a program, since the capture wants the unmasked value at a
|
||||
// varying the emulation never creates. Also a function of `requestedXfbVaryings`.
|
||||
Bool captureRequestsClipDistance = false;
|
||||
|
||||
// L1 shader-translation memo key for this program's SPIR-V (see
|
||||
// MG_Util/ShaderTranspiler/TranslationCache.h). Built HERE, at the tail of phase
|
||||
|
||||
@@ -799,6 +799,27 @@ namespace MobileGL::MG_State::GLState {
|
||||
MarkUBOContentDirtyRange(offset, static_cast<Uint>(sizeof(Int)));
|
||||
return true;
|
||||
}
|
||||
// ---- the reserved clip-distance enables (ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME) ----
|
||||
//
|
||||
// Same contract as the gl_NumSamples pair above: phase-A gate, value-identical writes
|
||||
// dropped without a content-version bump, false when there is nowhere to put it.
|
||||
Bool UsesReservedClipDistanceEnables() const { return Artifacts().usesReservedClipDistanceEnables; }
|
||||
Bool WriteReservedClipDistanceEnables(Uint32 enables) {
|
||||
if (!UsesReservedClipDistanceEnables()) return false;
|
||||
SpirvArtifacts& spirv = Spirv();
|
||||
const Uint offset = spirv.reservedClipDistanceEnablesOffset;
|
||||
if (offset == kInvalidUniformOffset) return false;
|
||||
if (static_cast<SizeT>(offset) + sizeof(Uint32) > spirv.globalUboScratch.size()) return false;
|
||||
|
||||
Uint8* const slot = spirv.globalUboScratch.data() + offset;
|
||||
Uint32 current = 0;
|
||||
Memcpy(¤t, slot, sizeof(Uint32));
|
||||
if (current == enables) return true;
|
||||
Memcpy(slot, &enables, sizeof(Uint32));
|
||||
MarkUBOContentDirtyRange(offset, static_cast<Uint>(sizeof(Uint32)));
|
||||
return true;
|
||||
}
|
||||
Bool ClipDistanceEmulated() const { return Spirv().clipDistanceEmulated; }
|
||||
// ---- glUniform* inside the phase-A -> phase-B window ----
|
||||
//
|
||||
// True while the program is fully linked and fully queryable but its uniform shadow's
|
||||
|
||||
@@ -33,6 +33,12 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
std::atomic_flag g_pointSizeDeclineReported;
|
||||
std::atomic_flag g_pointSizeOptimizerFailureReported;
|
||||
std::atomic_flag g_clipDistanceDeclineReported;
|
||||
std::atomic_flag g_clipDistanceOptimizerFailureReported;
|
||||
std::atomic_flag g_clipDistanceEmulationReported;
|
||||
// Varying locations the clip carriers may reach. 32 vec4s is what both backends' target
|
||||
// drivers expose (128 output components); a program past it keeps the decline.
|
||||
constexpr Uint32 kClipDistanceCarrierLocationBudget = 32;
|
||||
} // namespace
|
||||
|
||||
void ProgramSpirvTask::DeferLog(String line, const Int level) {
|
||||
@@ -154,8 +160,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
m_phaseA->in.env != nullptr && m_phaseA->in.env->DemotesTessellationPointSize();
|
||||
const Bool demoteGeometryPointSize =
|
||||
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();
|
||||
GenerateSpirv(handoff, externalIndex, deferOutputValidationForDirectVulkan, enableSpirvValidation,
|
||||
nativeFloat64, demoteTessellationPointSize, demoteGeometryPointSize);
|
||||
nativeFloat64, demoteTessellationPointSize, demoteGeometryPointSize, emulateClipDistance);
|
||||
// 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
|
||||
@@ -214,7 +222,8 @@ namespace MobileGL::MG_State::GLState {
|
||||
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex,
|
||||
const Bool deferOutputValidationForDirectVulkan,
|
||||
const Bool enableSpirvValidation, const Bool nativeFloat64,
|
||||
const Bool demoteTessellationPointSize, const Bool demoteGeometryPointSize) {
|
||||
const Bool demoteTessellationPointSize, const Bool demoteGeometryPointSize,
|
||||
const Bool emulateClipDistance) {
|
||||
/* As we passed first stage compilation/linking,
|
||||
* we'll assume all the operations here should
|
||||
* pass. We may be able to employ some optimizations
|
||||
@@ -269,6 +278,42 @@ namespace MobileGL::MG_State::GLState {
|
||||
}
|
||||
artifacts.nativeFloat64 = keepFloat64;
|
||||
|
||||
// The clip-distance emulation, program-wide and BEFORE the sanitize chain, on glslang's
|
||||
// raw output. It cannot run after it: a stage whose only default-block uniform is the
|
||||
// reserved enables member (nothing in the application reads it - only this pass does)
|
||||
// has its whole MGL_GLOBAL_UBO deleted by ADCE, and the producer would then fall back
|
||||
// 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) {
|
||||
ShaderCompiler::ClipDistanceEmulationOutcome outcome;
|
||||
if (!ShaderCompiler::EmulateClipDistanceForProgram(
|
||||
artifacts.generatedSpirv, handoff.shaderTypes, handoff.captureRequestsClipDistance,
|
||||
kClipDistanceCarrierLocationBudget, outcome, !deferOutputValidationForDirectVulkan,
|
||||
enableSpirvValidation)) {
|
||||
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) {
|
||||
artifacts.clipDistanceEmulated = true;
|
||||
// THE ARMING SIGNAL, INFO and latched: the integration lane that pins
|
||||
// MOBILEGL_CLIP_DISTANCE_EMULATION=1 asserts on this line, because a healthy
|
||||
// driver renders the same pixels whether the emulation ran or not.
|
||||
const Bool first = !g_clipDistanceEmulationReported.test_and_set(std::memory_order_relaxed);
|
||||
DeferLog(std::format("ProgramObject {}: gl_ClipDistance emulated ({} distance(s) carried to the "
|
||||
"fragment stage as varyings and clipped by discard{})",
|
||||
externalIndex, outcome.distanceCount,
|
||||
outcome.enablesFound ? "" : "; no enables uniform, every distance clips"),
|
||||
first ? MOBILEGL_LOG_LEVEL_INFO : MOBILEGL_LOG_LEVEL_DEBUG);
|
||||
} else if (!outcome.declineDetail.empty()) {
|
||||
DeferLog(std::format("ProgramObject {}: clip-distance emulation declined ({}); the program keeps "
|
||||
"gl_ClipDistance, which this backend cannot host",
|
||||
externalIndex, outcome.declineDetail),
|
||||
FirstTimeWarnLevel(g_clipDistanceDeclineReported));
|
||||
}
|
||||
}
|
||||
|
||||
// Linked SPIR-V generated, sanitize and optimize it
|
||||
Bool allOptimized = true;
|
||||
{
|
||||
@@ -348,6 +393,7 @@ namespace MobileGL::MG_State::GLState {
|
||||
artifacts.uniformOffsets.clear();
|
||||
artifacts.globalUboScratch.clear();
|
||||
artifacts.reservedNumSamplesOffset = ProgramObject::kInvalidUniformOffset;
|
||||
artifacts.reservedClipDistanceEnablesOffset = ProgramObject::kInvalidUniformOffset;
|
||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||
// below gives those locations tail storage so glUniform* always has a target.
|
||||
@@ -386,6 +432,10 @@ namespace MobileGL::MG_State::GLState {
|
||||
// precisely so no application can address it - so the lookup below would find
|
||||
// nothing and log it as unbacked. Only the fragment stage declares it, and
|
||||
// every stage's copy sits at the same offset in the one shared global UBO.
|
||||
if (name == CLIP_DISTANCE_ENABLES_UNIFORM_NAME) {
|
||||
artifacts.reservedClipDistanceEnablesOffset = offset;
|
||||
continue;
|
||||
}
|
||||
if (name == NUM_SAMPLES_UNIFORM_NAME) {
|
||||
artifacts.reservedNumSamplesOffset = offset;
|
||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - reserved gl_NumSamples stand-in '%s' "
|
||||
|
||||
@@ -67,7 +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 demoteTessellationPointSize, Bool demoteGeometryPointSize,
|
||||
Bool emulateClipDistance);
|
||||
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||
|
||||
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||
|
||||
@@ -531,9 +531,9 @@ TEST(ProgramArtifactsCodec, TheTablesVisitEveryMemberExceptTheLiveProgram) {
|
||||
// P7 OQ-8: 59 members, 58 visited. The 59th is still the live TProgram; the 58th is
|
||||
// storageBlocks, which follows the record arm's guard (ProgramArtifacts.h). esOrigin then
|
||||
// made it 60 and 59.
|
||||
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<LinkArtifacts>(), 59u);
|
||||
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<LinkArtifacts>(), 60u);
|
||||
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<StorageBlockReflection>(), 3u);
|
||||
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<SpirvArtifacts>(), 8u);
|
||||
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<SpirvArtifacts>(), 10u);
|
||||
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<ResourceReflection>(), 14u);
|
||||
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<XfbVarying>(), 11u);
|
||||
EXPECT_EQ(ProgramArtifactsVisitedFieldCount<TypeFacts>(), 20u);
|
||||
|
||||
@@ -84,8 +84,8 @@ namespace {
|
||||
// P7 OQ-8 added LinkArtifacts::storageBlocks and its own three-field table; since P13 W5
|
||||
// the member follows the record arm (ProgramArtifacts.h), which every library builds.
|
||||
EXPECT_EQ(CountFields<StorageBlockReflection>(), 3u);
|
||||
EXPECT_EQ(CountFields<LinkArtifacts>(), 59u);
|
||||
EXPECT_EQ(CountFields<SpirvArtifacts>(), 8u);
|
||||
EXPECT_EQ(CountFields<LinkArtifacts>(), 60u);
|
||||
EXPECT_EQ(CountFields<SpirvArtifacts>(), 10u);
|
||||
}
|
||||
|
||||
// A const walk hands the visitor const references, a non-const walk mutable ones: the one
|
||||
|
||||
@@ -31,6 +31,7 @@ add_executable(
|
||||
WidenImageFormatsTest.cpp
|
||||
GlslangCaptureTest.cpp
|
||||
DemotePointSizeTest.cpp
|
||||
EmulateClipDistanceTest.cpp
|
||||
FragCoordYFlipTest.cpp
|
||||
PixelCenterIntegerTest.cpp
|
||||
ZeroLayerOutputTest.cpp
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// MobileGL - MobileGL/MG_Test/ShaderTranspiler/EmulateClipDistanceTest.cpp
|
||||
// Copyright (c) 2025-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 <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Includes.h"
|
||||
#include "Init.h"
|
||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderSourceProcessor.h>
|
||||
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||
|
||||
#include "spirv-tools/libspirv.hpp"
|
||||
|
||||
using namespace MobileGL;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::CompileEnv;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SessionUsageBit;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler;
|
||||
using MobileGL::MG_Util::ShaderTranspiler::SpvcSession;
|
||||
|
||||
namespace {
|
||||
// The exact bytes ProgramSpirvTask hands the emulation: linked, generated, sanitized.
|
||||
Vector<Vector<Uint32>> CompileProgramToSpirv(const Vector<Pair<GLenum, const char*>>& stages,
|
||||
Vector<GLenum>& types) {
|
||||
using namespace MG_Util::ShaderTranspiler;
|
||||
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||
types.clear();
|
||||
for (const auto& [stage, source] : stages) {
|
||||
ShaderAttrib shaderAttrib{.shaderType = stage, .sourceStr = source};
|
||||
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||
EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log);
|
||||
if (!shaderResult) return {};
|
||||
shaders.push_back(shaderResult.value());
|
||||
types.push_back(stage);
|
||||
}
|
||||
ProgramAttrib programAttrib{.shaders = shaders};
|
||||
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||
EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log);
|
||||
if (!programResult) return {};
|
||||
ProgramBinaryAttrib binaryAttrib{.shaderTypes = types, .program = *programResult.value()};
|
||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||
EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log);
|
||||
if (!binaryResult) return {};
|
||||
// RAW glslang output: ProgramSpirvTask runs the emulation BEFORE the sanitize chain.
|
||||
return Move(binaryResult.value());
|
||||
}
|
||||
|
||||
// What ProgramSpirvTask does right after the emulation, so every validity / transpile
|
||||
// assertion is made against the bytes the backends really receive.
|
||||
void SanitizeAll(Vector<Vector<Uint32>>& modules) {
|
||||
for (auto& module : modules) {
|
||||
EXPECT_TRUE(ShaderCompiler::SanitizeAndOptimizeBinary(module, module, true, true));
|
||||
}
|
||||
}
|
||||
|
||||
String Disassemble(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String text;
|
||||
EXPECT_TRUE(tools.Disassemble(spirv, &text));
|
||||
return text;
|
||||
}
|
||||
|
||||
String Transpile(const Vector<Uint32>& spirv) {
|
||||
SpvcSession session(spirv, SessionUsageBit::Transpile);
|
||||
auto essl = ShaderCompiler::DecompileShader(session);
|
||||
EXPECT_TRUE(essl) << (essl ? String{} : essl.error().log);
|
||||
return essl ? essl.value() : String{};
|
||||
}
|
||||
|
||||
Bool Validates(const Vector<Uint32>& spirv) {
|
||||
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||
String messages;
|
||||
tools.SetMessageConsumer([&](spv_message_level_t, const char*, const spv_position_t&, const char* message) {
|
||||
messages += message;
|
||||
messages += '\n';
|
||||
});
|
||||
const Bool ok = tools.Validate(spirv);
|
||||
EXPECT_TRUE(ok) << messages;
|
||||
return ok;
|
||||
}
|
||||
|
||||
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
|
||||
// InjectClipDistanceEnablesShim adds on an emulating backend; spelled out here because a
|
||||
// unit test has no backend to arm the shim.
|
||||
const char* kEightPlaneVertex = R"(#version 330 core
|
||||
uniform uint mg_ClipDistanceEnables;
|
||||
uniform vec4 u_ClipPlane[8];
|
||||
uniform mat4 u_ModelView;
|
||||
in vec4 a_Position;
|
||||
void main() {
|
||||
vec4 eyePos = u_ModelView * a_Position;
|
||||
gl_Position = eyePos;
|
||||
gl_ClipDistance[0] = dot(u_ClipPlane[0], eyePos);
|
||||
gl_ClipDistance[1] = dot(u_ClipPlane[1], eyePos);
|
||||
gl_ClipDistance[2] = dot(u_ClipPlane[2], eyePos);
|
||||
gl_ClipDistance[3] = dot(u_ClipPlane[3], eyePos);
|
||||
gl_ClipDistance[4] = dot(u_ClipPlane[4], eyePos);
|
||||
gl_ClipDistance[5] = dot(u_ClipPlane[5], eyePos);
|
||||
gl_ClipDistance[6] = dot(u_ClipPlane[6], eyePos);
|
||||
gl_ClipDistance[7] = dot(u_ClipPlane[7], eyePos);
|
||||
}
|
||||
)";
|
||||
|
||||
const char* kPlainFragment = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
|
||||
constexpr Uint32 kBudget = 32;
|
||||
} // namespace
|
||||
|
||||
TEST(EmulateClipDistance, TheReportedEightPlaneVertexStageClipsByDiscard) {
|
||||
Vector<GLenum> 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"));
|
||||
|
||||
ShaderCompiler::ClipDistanceEmulationOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true));
|
||||
SanitizeAll(modules);
|
||||
EXPECT_TRUE(outcome.emulated) << outcome.declineDetail;
|
||||
EXPECT_EQ(outcome.distanceCount, 8u);
|
||||
EXPECT_TRUE(outcome.enablesFound);
|
||||
|
||||
for (const auto& module : modules) {
|
||||
EXPECT_TRUE(Validates(module));
|
||||
EXPECT_FALSE(Contains(Disassemble(module), "OpCapability ClipDistance"));
|
||||
}
|
||||
const String vertexAsm = Disassemble(modules[0]);
|
||||
EXPECT_TRUE(Contains(vertexAsm, "mg_ClipDistanceEmu0"));
|
||||
EXPECT_TRUE(Contains(vertexAsm, "mg_ClipDistanceEmu1"));
|
||||
EXPECT_TRUE(Contains(vertexAsm, "OpBitwiseAnd"));
|
||||
const String fragmentAsm = Disassemble(modules[1]);
|
||||
EXPECT_TRUE(Contains(fragmentAsm, "OpKill"));
|
||||
EXPECT_TRUE(Contains(fragmentAsm, "mg_ClipDistanceEmu1"));
|
||||
|
||||
// The ES hop must no longer need GL_EXT_clip_cull_distance anywhere.
|
||||
const String vertexEssl = Transpile(modules[0]);
|
||||
const String fragmentEssl = Transpile(modules[1]);
|
||||
EXPECT_FALSE(Contains(vertexEssl, "gl_ClipDistance")) << vertexEssl;
|
||||
EXPECT_FALSE(Contains(vertexEssl, "clip_cull_distance")) << vertexEssl;
|
||||
EXPECT_TRUE(Contains(vertexEssl, "mg_ClipDistanceEmu0")) << vertexEssl;
|
||||
EXPECT_TRUE(Contains(fragmentEssl, "discard")) << fragmentEssl;
|
||||
EXPECT_FALSE(Contains(fragmentEssl, "clip_cull_distance")) << fragmentEssl;
|
||||
}
|
||||
|
||||
TEST(EmulateClipDistance, OneDistanceUsesOneCarrierAndNoEnablesMeansAllEnabled) {
|
||||
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;
|
||||
}
|
||||
)";
|
||||
Vector<GLenum> types;
|
||||
auto modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, vertex}, {GL_FRAGMENT_SHADER, kPlainFragment}}, types);
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
ShaderCompiler::ClipDistanceEmulationOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true));
|
||||
SanitizeAll(modules);
|
||||
EXPECT_TRUE(outcome.emulated) << outcome.declineDetail;
|
||||
EXPECT_EQ(outcome.distanceCount, 1u);
|
||||
EXPECT_FALSE(outcome.enablesFound);
|
||||
for (const auto& module : modules)
|
||||
EXPECT_TRUE(Validates(module));
|
||||
const String vertexAsm = Disassemble(modules[0]);
|
||||
EXPECT_TRUE(Contains(vertexAsm, "mg_ClipDistanceEmu0"));
|
||||
EXPECT_FALSE(Contains(vertexAsm, "mg_ClipDistanceEmu1"));
|
||||
}
|
||||
|
||||
TEST(EmulateClipDistance, AProgramThatNeverClipsIsUntouched) {
|
||||
const char* vertex = R"(#version 330 core
|
||||
void main() { gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0); }
|
||||
)";
|
||||
Vector<GLenum> types;
|
||||
auto modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, vertex}, {GL_FRAGMENT_SHADER, kPlainFragment}}, types);
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
const auto before = modules;
|
||||
ShaderCompiler::ClipDistanceEmulationOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true));
|
||||
EXPECT_FALSE(outcome.emulated);
|
||||
EXPECT_TRUE(outcome.declineDetail.empty());
|
||||
EXPECT_EQ(modules, before);
|
||||
}
|
||||
|
||||
TEST(EmulateClipDistance, AGeometryStageProducesBeforeEveryEmitAndTheVertexWriteIsRedirected) {
|
||||
const char* vertex = R"(#version 330 core
|
||||
uniform uint mg_ClipDistanceEnables;
|
||||
void main() {
|
||||
gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0);
|
||||
gl_ClipDistance[0] = -1.0;
|
||||
}
|
||||
)";
|
||||
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_Position.x;
|
||||
gl_ClipDistance[1] = 1.0;
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
}
|
||||
)";
|
||||
Vector<GLenum> types;
|
||||
auto modules = CompileProgramToSpirv(
|
||||
{{GL_VERTEX_SHADER, vertex}, {GL_GEOMETRY_SHADER, geometry}, {GL_FRAGMENT_SHADER, kPlainFragment}}, types);
|
||||
ASSERT_EQ(modules.size(), 3u);
|
||||
ShaderCompiler::ClipDistanceEmulationOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true));
|
||||
SanitizeAll(modules);
|
||||
EXPECT_TRUE(outcome.emulated) << outcome.declineDetail;
|
||||
EXPECT_EQ(outcome.distanceCount, 2u);
|
||||
for (const auto& module : modules) {
|
||||
EXPECT_TRUE(Validates(module));
|
||||
EXPECT_FALSE(Contains(Disassemble(module), "OpCapability ClipDistance"));
|
||||
}
|
||||
// The vertex stage is not the producer: redirected, no carriers.
|
||||
EXPECT_FALSE(Contains(Disassemble(modules[0]), "mg_ClipDistanceEmu0"));
|
||||
EXPECT_TRUE(Contains(Disassemble(modules[1]), "mg_ClipDistanceEmu0"));
|
||||
}
|
||||
|
||||
TEST(EmulateClipDistance, AFragmentStageReadOfGlClipDistanceIsServedFromTheCarriers) {
|
||||
const char* vertex = R"(#version 330 core
|
||||
void main() {
|
||||
gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0);
|
||||
gl_ClipDistance[0] = 0.5;
|
||||
}
|
||||
)";
|
||||
const char* fragment = R"(#version 330 core
|
||||
out vec4 o_color;
|
||||
void main() { o_color = vec4(gl_ClipDistance[0], 1.0, 0.0, 1.0); }
|
||||
)";
|
||||
Vector<GLenum> types;
|
||||
auto modules = CompileProgramToSpirv({{GL_VERTEX_SHADER, vertex}, {GL_FRAGMENT_SHADER, fragment}}, types);
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
ShaderCompiler::ClipDistanceEmulationOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true));
|
||||
SanitizeAll(modules);
|
||||
EXPECT_TRUE(outcome.emulated) << outcome.declineDetail;
|
||||
for (const auto& module : modules) {
|
||||
EXPECT_TRUE(Validates(module));
|
||||
EXPECT_FALSE(Contains(Disassemble(module), "OpCapability ClipDistance"));
|
||||
}
|
||||
const String fragmentEssl = Transpile(modules[1]);
|
||||
EXPECT_FALSE(Contains(fragmentEssl, "gl_ClipDistance")) << fragmentEssl;
|
||||
EXPECT_TRUE(Contains(fragmentEssl, "mg_ClipDistanceShadow")) << fragmentEssl;
|
||||
}
|
||||
|
||||
TEST(EmulateClipDistance, AGlInReadDeclinesAndLeavesEveryModuleAlone) {
|
||||
const char* vertex = R"(#version 330 core
|
||||
void main() {
|
||||
gl_Position = vec4(float(gl_VertexID), 0.0, 0.0, 1.0);
|
||||
gl_ClipDistance[0] = 1.0;
|
||||
}
|
||||
)";
|
||||
const char* geometry = R"(#version 330 core
|
||||
layout(points) in;
|
||||
layout(points, max_vertices = 1) out;
|
||||
void main() {
|
||||
gl_Position = gl_in[0].gl_Position + vec4(gl_in[0].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 before = modules;
|
||||
ShaderCompiler::ClipDistanceEmulationOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true));
|
||||
EXPECT_FALSE(outcome.emulated);
|
||||
EXPECT_NE(outcome.declineDetail.find("gl_in"), String::npos) << outcome.declineDetail;
|
||||
EXPECT_EQ(modules, before);
|
||||
}
|
||||
|
||||
TEST(EmulateClipDistance, ACaptureOfGlClipDistanceDeclines) {
|
||||
Vector<GLenum> 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;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, true, kBudget, outcome, true, true));
|
||||
EXPECT_FALSE(outcome.emulated);
|
||||
EXPECT_FALSE(outcome.declineDetail.empty());
|
||||
EXPECT_EQ(modules, before);
|
||||
}
|
||||
|
||||
// End to end through the source pipeline: the shim's uniform must survive glslang and the
|
||||
// sanitize chain so the producer finds it. Without this the draw path's mask has nowhere to
|
||||
// land and every distance silently clips (the all-enabled fallback).
|
||||
TEST(EmulateClipDistance, TheShimmedEnablesUniformReachesTheProducer) {
|
||||
CompileEnv emulating{};
|
||||
emulating.backend = BackendType::DirectGLES;
|
||||
emulating.params.EmulatesClipDistance = true;
|
||||
String vertex = R"(#version 400 core
|
||||
out float gl_ClipDistance[2];
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
|
||||
vec2 p = positions[gl_VertexID];
|
||||
gl_Position = vec4(p, 0.0, 1.0);
|
||||
gl_ClipDistance[0] = p.x;
|
||||
gl_ClipDistance[1] = p.y;
|
||||
}
|
||||
)";
|
||||
String fragment = "#version 400 core\nout vec4 o;\nvoid main() { o = vec4(1.0); }\n";
|
||||
MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Vertex, vertex, emulating);
|
||||
MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Fragment, fragment, emulating);
|
||||
ASSERT_TRUE(Contains(vertex, MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) << vertex;
|
||||
Vector<GLenum> types;
|
||||
auto modules =
|
||||
CompileProgramToSpirv({{GL_VERTEX_SHADER, vertex.c_str()}, {GL_FRAGMENT_SHADER, fragment.c_str()}}, types);
|
||||
ASSERT_EQ(modules.size(), 2u);
|
||||
EXPECT_TRUE(Contains(Disassemble(modules[0]), "mg_ClipDistanceEnables")) << Disassemble(modules[0]);
|
||||
ShaderCompiler::ClipDistanceEmulationOutcome outcome;
|
||||
ASSERT_TRUE(ShaderCompiler::EmulateClipDistanceForProgram(modules, types, false, kBudget, outcome, true, true));
|
||||
EXPECT_TRUE(outcome.emulated) << outcome.declineDetail;
|
||||
EXPECT_TRUE(outcome.enablesFound) << vertex;
|
||||
// And the read the producer added keeps the member alive through the sanitize chain.
|
||||
SanitizeAll(modules);
|
||||
for (const auto& module : modules)
|
||||
EXPECT_TRUE(Validates(module));
|
||||
EXPECT_TRUE(Contains(Disassemble(modules[0]), "mg_ClipDistanceEnables"));
|
||||
}
|
||||
|
||||
TEST(EmulateClipDistance, TheEnablesShimArmsOnlyOnAnEmulatingBackendAndOnlyWhereItIsNamed) {
|
||||
CompileEnv emulating{};
|
||||
emulating.backend = BackendType::DirectGLES;
|
||||
emulating.params.EmulatesClipDistance = true;
|
||||
CompileEnv native = emulating;
|
||||
native.params.EmulatesClipDistance = false;
|
||||
|
||||
const String vertex = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); gl_ClipDistance[0] = 1.0; }\n";
|
||||
const String fragment = "#version 330 core\nout vec4 o; void main() { o = vec4(1.0); }\n";
|
||||
const String nonClipping = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n";
|
||||
|
||||
String source = vertex;
|
||||
MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Vertex, source, emulating);
|
||||
EXPECT_TRUE(Contains(source, MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) << source;
|
||||
|
||||
source = vertex;
|
||||
MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Vertex, source, native);
|
||||
EXPECT_FALSE(Contains(source, MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) << source;
|
||||
|
||||
source = fragment;
|
||||
MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Fragment, source, emulating);
|
||||
EXPECT_FALSE(Contains(source, MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) << source;
|
||||
|
||||
source = nonClipping;
|
||||
MG_Util::ShaderTranspiler::PreprocessShaderSource(ShaderStage::Vertex, source, emulating);
|
||||
EXPECT_FALSE(Contains(source, MG_Util::ShaderTranspiler::CLIP_DISTANCE_ENABLES_UNIFORM_NAME)) << source;
|
||||
}
|
||||
@@ -1997,6 +1997,39 @@ 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)");
|
||||
// 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"
|
||||
|
||||
@@ -1274,6 +1274,19 @@ namespace MobileGL {
|
||||
// compile and the per-distance enables have nowhere to go - clipping silently never
|
||||
// happens, which is exactly what KHR-GLxx.clip_distance.functional catches.
|
||||
Bool SupportsClipDistance = false;
|
||||
// What SelfTest::ProbeNativeClipDistance measured - only run where the extension is
|
||||
// advertised: an enabled negative distance removes its fragments, and a disabled one
|
||||
// removes nothing.
|
||||
Bool NativeClipDistanceClips = false;
|
||||
Bool NativeClipDistanceHonorsEnables = false;
|
||||
// DirectGLES lowers gl_ClipDistance to varyings plus a fragment discard
|
||||
// (ShaderCompiler::EmulateClipDistanceForProgram) instead of handing it to the
|
||||
// driver: no extension, no distances behind it, or a probe that caught the driver
|
||||
// not clipping or not honoring the enables. MOBILEGL_CLIP_DISTANCE_EMULATION
|
||||
// overrides. While set, the CLIP_DISTANCEi enables are NOT forwarded to the driver -
|
||||
// no emulated module writes the built-in, and an enabled distance nobody writes is
|
||||
// undefined clipping.
|
||||
Bool EmulatesClipDistance = false;
|
||||
// GL_OES_viewport_array is present: the driver knows gl_ViewportIndex in ESSL - and
|
||||
// only then. ESSL has no core spelling for it at ANY version, while SPIRV-Cross prints
|
||||
// the identifier bare and requests nothing for it (contrast gl_Layer, which it backs
|
||||
|
||||
@@ -2066,6 +2066,165 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
return measurement;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ===================== NATIVE CLIP DISTANCE =====================
|
||||
|
||||
constexpr const char* kClipDistanceProbeName = "clip distance";
|
||||
// Four pixels across: pixel 0 is centred at NDC x = -0.75, pixel 3 at +0.75, so the
|
||||
// distance gl_ClipDistance[0] = x is clearly negative on one and positive on the other.
|
||||
constexpr GLsizei kClipDistanceProbeWidth = 4;
|
||||
// GL_CLIP_DISTANCE0_EXT / 1_EXT: the desktop values, absent from the ES headers.
|
||||
constexpr GLenum kClipDistance0 = 0x3000;
|
||||
constexpr GLenum kClipDistance1 = 0x3001;
|
||||
|
||||
const char* const kClipDistanceProbeVertexSource =
|
||||
"#version 300 es\n"
|
||||
"#extension GL_EXT_clip_cull_distance : require\n"
|
||||
"precision highp float;\n"
|
||||
"void main() {\n"
|
||||
" vec2 mg_p = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);\n"
|
||||
" gl_Position = vec4(mg_p, 0.0, 1.0);\n"
|
||||
" gl_ClipDistance[0] = mg_p.x;\n"
|
||||
" gl_ClipDistance[1] = 1.0;\n"
|
||||
"}\n";
|
||||
// The control: the same stage with a distance that is positive everywhere.
|
||||
const char* const kClipDistanceProbeControlVertexSource =
|
||||
"#version 300 es\n"
|
||||
"#extension GL_EXT_clip_cull_distance : require\n"
|
||||
"precision highp float;\n"
|
||||
"void main() {\n"
|
||||
" vec2 mg_p = vec2((gl_VertexID == 1) ? 3.0 : -1.0, (gl_VertexID == 2) ? 3.0 : -1.0);\n"
|
||||
" gl_Position = vec4(mg_p, 0.0, 1.0);\n"
|
||||
" 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";
|
||||
|
||||
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).
|
||||
Bool DrawClipDistanceProbe(const GLESFunctionsTable& gl, Bool enableDistance0, Bool& leftPainted,
|
||||
Bool& rightPainted) {
|
||||
if (enableDistance0) {
|
||||
gl.glEnable(kClipDistance0);
|
||||
} else {
|
||||
gl.glDisable(kClipDistance0);
|
||||
}
|
||||
gl.glDisable(kClipDistance1);
|
||||
gl.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
gl.glClear(GL_COLOR_BUFFER_BIT);
|
||||
Drain(gl);
|
||||
gl.glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
if (gl.glGetError() != GL_NO_ERROR) return false;
|
||||
GLubyte row[kClipDistanceProbeWidth * 4] = {};
|
||||
gl.glReadPixels(0, 0, kClipDistanceProbeWidth, 1, GL_RGBA, GL_UNSIGNED_BYTE, row);
|
||||
if (gl.glGetError() != GL_NO_ERROR) return false;
|
||||
leftPainted = IsProbeGreen(row);
|
||||
rightPainted = IsProbeGreen(row + (kClipDistanceProbeWidth - 1) * 4);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
NativeClipDistanceMeasurement ProbeNativeClipDistance(const GLESFunctionsTable& gl) {
|
||||
NativeClipDistanceMeasurement measurement;
|
||||
if (!HasIoBlockProbeEntryPoints(gl)) {
|
||||
measurement.failure = "a probe entry point is missing";
|
||||
return measurement;
|
||||
}
|
||||
|
||||
SavedState saved;
|
||||
Save(gl, saved);
|
||||
GLboolean savedColorMask[4] = {GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE};
|
||||
const Bool canMaskColor = gl.glColorMask != nullptr && gl.glGetBooleanv != nullptr;
|
||||
if (canMaskColor) {
|
||||
gl.glGetBooleanv(GL_COLOR_WRITEMASK, savedColorMask);
|
||||
gl.glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
}
|
||||
GLuint vao = 0;
|
||||
gl.glGenVertexArrays(1, &vao);
|
||||
gl.glBindVertexArray(vao);
|
||||
PrepareForProbeDraw(gl);
|
||||
|
||||
Vector<StageSource> stages;
|
||||
stages.push_back({GL_VERTEX_SHADER, kClipDistanceProbeVertexSource, "vertex"});
|
||||
stages.push_back({GL_FRAGMENT_SHADER, kClipDistanceProbeFragmentSource, "fragment"});
|
||||
const ProgramBuild build = BuildProgram(gl, stages, kClipDistanceProbeName);
|
||||
Vector<StageSource> controlStages;
|
||||
controlStages.push_back({GL_VERTEX_SHADER, kClipDistanceProbeControlVertexSource, "control vertex"});
|
||||
controlStages.push_back({GL_FRAGMENT_SHADER, kClipDistanceProbeFragmentSource, "fragment"});
|
||||
const ProgramBuild control = BuildProgram(gl, controlStages, kClipDistanceProbeName);
|
||||
if (!build.compiled || !control.compiled) {
|
||||
measurement.failure = "the probe's gl_ClipDistance vertex stage did not compile";
|
||||
} else if (!build.linked || !control.linked) {
|
||||
measurement.failure =
|
||||
"the probe program did not link: " + FirstLine(!build.linked ? build.infoLog : control.infoLog);
|
||||
} else {
|
||||
GLuint renderbuffer = 0;
|
||||
GLuint framebuffer = 0;
|
||||
gl.glGenRenderbuffers(1, &renderbuffer);
|
||||
gl.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
gl.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, kClipDistanceProbeWidth, 1);
|
||||
gl.glGenFramebuffers(1, &framebuffer);
|
||||
gl.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
gl.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
if (gl.glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
measurement.failure = "the probe framebuffer is incomplete";
|
||||
} else {
|
||||
gl.glViewport(0, 0, kClipDistanceProbeWidth, 1);
|
||||
Bool left = false;
|
||||
Bool right = false;
|
||||
// THE CONTROL first: a positive distance, enabled, so both halves must paint.
|
||||
gl.glUseProgram(control.program);
|
||||
const Bool controlRan = DrawClipDistanceProbe(gl, true, left, right);
|
||||
gl.glUseProgram(build.program);
|
||||
if (!controlRan || !left || !right) {
|
||||
measurement.failure = "the control draw (a positive distance, enabled) did not paint the target";
|
||||
} else if (!DrawClipDistanceProbe(gl, true, left, right) || !right) {
|
||||
measurement.failure = "the clipped draw raised an error or lost its kept half";
|
||||
} else {
|
||||
measurement.clips = !left;
|
||||
Bool leftAfter = false;
|
||||
Bool rightAfter = false;
|
||||
if (DrawClipDistanceProbe(gl, false, leftAfter, rightAfter) && rightAfter) {
|
||||
measurement.ran = true;
|
||||
measurement.honorsEnables = leftAfter;
|
||||
} else {
|
||||
measurement.failure = "the re-disabled draw raised an error or lost its kept half";
|
||||
}
|
||||
}
|
||||
}
|
||||
gl.glDisable(kClipDistance0);
|
||||
gl.glDisable(kClipDistance1);
|
||||
if (framebuffer != 0) gl.glDeleteFramebuffers(1, &framebuffer);
|
||||
if (renderbuffer != 0) gl.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
}
|
||||
if (build.program != 0) gl.glDeleteProgram(build.program);
|
||||
if (control.program != 0) gl.glDeleteProgram(control.program);
|
||||
|
||||
if (vao != 0) {
|
||||
gl.glBindVertexArray(0);
|
||||
gl.glDeleteVertexArrays(1, &vao);
|
||||
}
|
||||
if (canMaskColor) {
|
||||
gl.glColorMask(savedColorMask[0], savedColorMask[1], savedColorMask[2], savedColorMask[3]);
|
||||
}
|
||||
Restore(gl, saved);
|
||||
Drain(gl);
|
||||
if (!measurement.failure.empty()) {
|
||||
MGLOG_I("[driver-bug] %s probe: %s", kClipDistanceProbeName, measurement.failure.c_str());
|
||||
}
|
||||
return measurement;
|
||||
}
|
||||
|
||||
const NativeClipDistanceMeasurement& NativeClipDistance(const GLESFunctionsTable& gl) {
|
||||
static const NativeClipDistanceMeasurement measurement = ProbeNativeClipDistance(gl);
|
||||
return measurement;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// ===================== PACKED16 COPY-IMAGE FIELD ORDER =====================
|
||||
|
||||
|
||||
@@ -95,6 +95,38 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
// ProbeLocatedIoBlocksLosePayload(), evaluated at most once per process.
|
||||
const LocatedIoBlockMeasurement& LocatedIoBlocksLosePayload(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// What the native clip-distance probe measured. Not a driver-bug finding by itself: the
|
||||
// loader turns it into the DirectGLES choice between the driver's gl_ClipDistance and
|
||||
// MobileGL's emulation (EmulateClipDistancePass), and the POST "Clip distances" row reports
|
||||
// that choice.
|
||||
struct NativeClipDistanceMeasurement {
|
||||
// The probe built its program and drew it; every field below is meaningless otherwise.
|
||||
Bool ran = false;
|
||||
// GL_CLIP_DISTANCE0_EXT enabled, distance negative on the left half: the left half was
|
||||
// removed and the right half painted.
|
||||
Bool clips = false;
|
||||
// GL_CLIP_DISTANCE0_EXT disabled, same distances: nothing was removed.
|
||||
Bool honorsEnables = false;
|
||||
// The driver's first info-log line (or a short reason) when the probe could not run.
|
||||
String failure;
|
||||
};
|
||||
|
||||
// Draws full-viewport triangles under GL_EXT_clip_cull_distance. THE CONTROL first: a
|
||||
// program whose gl_ClipDistance[0] is +1 everywhere, with distance 0 enabled - both halves
|
||||
// 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).
|
||||
//
|
||||
// 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
|
||||
// state DirectGLES's enable forwarding assumes a fresh context is in.
|
||||
NativeClipDistanceMeasurement ProbeNativeClipDistance(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// ProbeNativeClipDistance(), evaluated at most once per process.
|
||||
const NativeClipDistanceMeasurement& NativeClipDistance(const MG_External::GLESFunctionsTable& gl);
|
||||
|
||||
// Blits one layer of an RGBA8 2D array onto another array's layer 1 and reports whether the
|
||||
// copy landed where it was asked to. Returns true only when the destination layer is ignored
|
||||
// while the control lands correctly.
|
||||
|
||||
@@ -340,6 +340,65 @@ 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 EvaluateGlesChecklist(ReportBuilder& builder, const MG_External::GLESCapabilities& caps,
|
||||
const MG_External::GLESFunctionsTable& glesFuncs) {
|
||||
const Int major = caps.GLESVersion.Major;
|
||||
@@ -388,6 +447,7 @@ namespace MobileGL::MG_Util::SelfTest {
|
||||
"hard-fails, and a second fragment output cannot be produced any other "
|
||||
"way");
|
||||
}
|
||||
EvaluateGlesClipDistanceRows(builder, caps, glesFuncs);
|
||||
|
||||
if (es31) {
|
||||
GLint maxVertexSsboBlocks = 0;
|
||||
@@ -2776,6 +2836,38 @@ 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); GL_CLIP_DISTANCEi enables are not applied on this backend "
|
||||
"yet, so every distance a shader writes clips",
|
||||
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");
|
||||
}
|
||||
}
|
||||
// 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
|
||||
|
||||
@@ -75,6 +75,10 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// and expands gl_MaxClipDistances from the same number, so it decides both whether a
|
||||
// shader compiles at all and what a module that reads the constant generates.
|
||||
HashValue(state, env.params.MaxClipDistances);
|
||||
// Unlike the point-size demotion bits this IS frontend material: when it is set the
|
||||
// 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);
|
||||
// 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
|
||||
|
||||
@@ -165,6 +165,10 @@ namespace MobileGL::MG_Util::ShaderTranspiler {
|
||||
// fingerprint: glslang produces the same thing either way.
|
||||
Bool DemotesTessellationPointSize() const { return HasBackend() && !params.SupportsTessellationPointSize; }
|
||||
Bool DemotesGeometryPointSize() const { return HasBackend() && !params.SupportsGeometryPointSize; }
|
||||
// The backend cannot clip with gl_ClipDistance natively and emulates it
|
||||
// (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; }
|
||||
// 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 {
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#include "SpirvPasses/LegalizeResourceArrayIndexPass.h"
|
||||
#include "SpirvPasses/FlattenAtomicCounterBlockPass.h"
|
||||
#include "SpirvPasses/DemotePointSizePass.h"
|
||||
#include "SpirvPasses/EmulateClipDistancePass.h"
|
||||
#include "spirv-tools/libspirv.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
#include "source/opt/build_module.h"
|
||||
@@ -1215,6 +1216,127 @@ 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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
ClipDistanceModuleProbe probes[5];
|
||||
Bool anyAccess = false;
|
||||
Uint32 carrierLocation = 0;
|
||||
for (Int stage = 0; stage < 5; ++stage) {
|
||||
if (stageIndex[stage] < 0) continue;
|
||||
const Vector<Uint32>& module = modules[stageIndex[stage]];
|
||||
probes[stage] = EmulateClipDistancePass::ProbeModule(module);
|
||||
if (!probes[stage].parsed) return true; // broken for other reasons; owns its failure
|
||||
anyAccess |= probes[stage].declaresClipDistanceCapability;
|
||||
const PointSizeModuleProbe locations = ProbePointSizeModule(module);
|
||||
carrierLocation = std::max(carrierLocation, locations.locationSlotEnd);
|
||||
}
|
||||
if (!anyAccess) return true;
|
||||
if (captureRequestsClipDistance) {
|
||||
outcome.declineDetail = "transform feedback captures gl_ClipDistance";
|
||||
return true;
|
||||
}
|
||||
|
||||
// The producer: the last pre-rasterization stage present, and only when it
|
||||
// actually writes the built-in - an earlier stage's writes are dead in GL unless
|
||||
// a later stage reads gl_in[].gl_ClipDistance, which the pass declines.
|
||||
Int producer = -1;
|
||||
for (Int stage = 3; stage >= 0; --stage) {
|
||||
if (stage == 1) continue; // a control stage is never last
|
||||
if (stageIndex[stage] >= 0) {
|
||||
producer = stage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Uint32 distanceCount = 0;
|
||||
if (producer >= 0 && probes[producer].declaresClipDistanceCapability) {
|
||||
distanceCount = probes[producer].outputDistanceCount;
|
||||
}
|
||||
if (distanceCount > EmulateClipDistancePass::kMaxCarriers * 4) {
|
||||
outcome.declineDetail = std::format("{} clip distances, more than the eight the carriers hold",
|
||||
distanceCount);
|
||||
return true;
|
||||
}
|
||||
const Uint32 carrierCount = (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) "
|
||||
"under the {}-location budget",
|
||||
carrierLocation, carrierCount, maxVaryingLocations);
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector<Vector<Uint32>> rewritten(modules.size());
|
||||
Bool rewrote[5] = {false, false, false, false, false};
|
||||
Bool enablesFound = false;
|
||||
for (Int stage = 0; stage < 5; ++stage) {
|
||||
const Int moduleIndex = stageIndex[stage];
|
||||
if (moduleIndex < 0) continue;
|
||||
EmulateClipDistanceOptions options;
|
||||
options.location = carrierLocation;
|
||||
if (stage == 4) {
|
||||
options.role = EmulateClipDistanceOptions::Role::Consumer;
|
||||
options.distanceCount = distanceCount;
|
||||
if (distanceCount == 0 && !probes[stage].declaresClipDistanceCapability) continue;
|
||||
} else if (stage == producer && distanceCount > 0) {
|
||||
options.role = EmulateClipDistanceOptions::Role::Producer;
|
||||
options.enablesMemberName = CLIP_DISTANCE_ENABLES_UNIFORM_NAME;
|
||||
} else {
|
||||
options.role = EmulateClipDistanceOptions::Role::Redirect;
|
||||
if (!probes[stage].declaresClipDistanceCapability) continue;
|
||||
}
|
||||
|
||||
EmulateClipDistanceReport report;
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_1);
|
||||
optimizer.RegisterPass(EmulateClipDistancePass::CreateEmulateClipDistancePass(options, &report));
|
||||
if (!RunOptimizerChecked("EmulateClipDistanceForProgram", optimizer, modules[moduleIndex],
|
||||
rewritten[moduleIndex], validateOutput, enableSpirvValidation)) {
|
||||
return false; // modules untouched: nothing was committed
|
||||
}
|
||||
if (report.declined) {
|
||||
outcome.declineDetail = Move(report.declineReason);
|
||||
return true;
|
||||
}
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
enablesFound = report.enablesFound;
|
||||
}
|
||||
rewrote[stage] = report.rewrote;
|
||||
}
|
||||
|
||||
for (Int stage = 0; stage < 5; ++stage) {
|
||||
if (!rewrote[stage]) continue;
|
||||
modules[stageIndex[stage]] = Move(rewritten[stageIndex[stage]]);
|
||||
}
|
||||
outcome.emulated = true;
|
||||
outcome.distanceCount = distanceCount;
|
||||
outcome.enablesFound = enablesFound;
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ShaderCompiler::ModuleDeclaresFloat64(const Vector<Uint32>& spirv) {
|
||||
if (spirv.empty()) {
|
||||
// Same reasoning as ModuleDeclaresBufferTextureSampler: a stage that produced
|
||||
|
||||
@@ -650,6 +650,39 @@ namespace MobileGL {
|
||||
Bool demoteGeometry, Bool captureRequestsPointSize, PointSizeDemotionOutcome& outcome,
|
||||
bool validateOutput = true, bool enableSpirvValidation = false);
|
||||
|
||||
// What the program-scoped clip-distance emulation left behind. `emulated` false
|
||||
// with an empty detail means the program never touches gl_ClipDistance; false
|
||||
// WITH a detail is a shape the pass cannot express (modules byte-identical, so the
|
||||
// program keeps the built-in and the backend's own refusal of it).
|
||||
struct ClipDistanceEmulationOutcome {
|
||||
Bool emulated = false;
|
||||
// How many distances the producing stage carries to the fragment stage.
|
||||
Uint32 distanceCount = 0;
|
||||
// False: the producer had no enables uniform, so every distance clips.
|
||||
Bool enablesFound = false;
|
||||
String declineDetail;
|
||||
};
|
||||
|
||||
// Emulates gl_ClipDistance across a WHOLE program on a backend that cannot clip
|
||||
// natively (BackendCaps::EmulatesClipDistance): every stage's accesses move onto
|
||||
// a Private shadow, the last pre-rasterization stage carries
|
||||
// `enabled(i) ? d[i] : 1.0` to the fragment stage in ceil(N/4) vec4 varyings
|
||||
// placed past every location the program uses, and the fragment stage discards
|
||||
// where a carried distance is negative. See EmulateClipDistancePass for the
|
||||
// per-module rewrite. Runs BEFORE the sanitize chain (ProgramSpirvTask): the
|
||||
// reserved enables uniform is unread until this pass reads it, and ADCE would
|
||||
// otherwise delete a global UBO that holds nothing else. Atomic: every stage is
|
||||
// rewritten or none is.
|
||||
// `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.
|
||||
static Bool EmulateClipDistanceForProgram(Vector<Vector<Uint32>>& modules,
|
||||
const Vector<GLenum>& shaderTypes,
|
||||
Bool captureRequestsClipDistance, Uint32 maxVaryingLocations,
|
||||
ClipDistanceEmulationOutcome& outcome,
|
||||
bool validateOutput = true,
|
||||
bool enableSpirvValidation = false);
|
||||
|
||||
// True when the module still declares a 64-bit float type. After
|
||||
// SanitizeAndOptimizeBinary that can only mean DemoteFloat64Pass declined the
|
||||
// module (see its header for the two operations that make it decline), which is
|
||||
|
||||
@@ -1880,6 +1880,27 @@ namespace {
|
||||
source.insert(afterVersion.Get(source), shim);
|
||||
}
|
||||
|
||||
// On a backend that emulates gl_ClipDistance, the stage that ends up producing the clip
|
||||
// carriers needs GL's per-distance enables, and the only thing a module can read per draw
|
||||
// without a pipeline key is a uniform. Declared in every pre-rasterization stage that names
|
||||
// the built-in, because which of them is LAST is a link-time fact this per-shader pass cannot
|
||||
// know; the stages that are not the producer simply never read it, and glslang folds the one
|
||||
// name into the shared MGL_GLOBAL_UBO for every stage of the program alike.
|
||||
void InjectClipDistanceEnablesShim(MobileGL::ShaderStage stage, MobileGL::String& source,
|
||||
AfterVersionAnchor& afterVersion) {
|
||||
if (stage != MobileGL::ShaderStage::Vertex && stage != MobileGL::ShaderStage::TessControl &&
|
||||
stage != MobileGL::ShaderStage::TessEval && stage != MobileGL::ShaderStage::Geometry) {
|
||||
return;
|
||||
}
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
source.insert(afterVersion.Get(source), "uniform uint mg_ClipDistanceEnables;\n");
|
||||
}
|
||||
|
||||
// The uniforms `text` declares with the external sampler type, and (into `others`) the names it
|
||||
// declares with any other type - the same name can be both in a source whose #if branches pick
|
||||
// one (a compositor's shader generator does exactly that).
|
||||
@@ -2020,6 +2041,7 @@ namespace MobileGL {
|
||||
ModernizeLegacyGLSL(stage, source, afterVersion);
|
||||
InjectDepthRangeBuiltinShim(stage, source, afterVersion);
|
||||
InjectNumSamplesBuiltinShim(stage, originalLanguage, source, afterVersion);
|
||||
if (env.EmulatesClipDistance()) InjectClipDistanceEnablesShim(stage, source, afterVersion);
|
||||
}
|
||||
|
||||
Vector<String> ExternalSamplerUniformNames(const String& preprocessedSource) {
|
||||
|
||||
@@ -0,0 +1,699 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.cpp
|
||||
// Copyright (c) 2025-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 "EmulateClipDistancePass.h"
|
||||
|
||||
#include "spirv.hpp"
|
||||
#include "source/opt/basic_block.h"
|
||||
#include "source/opt/build_module.h"
|
||||
#include "source/opt/constants.h"
|
||||
#include "source/opt/def_use_manager.h"
|
||||
#include "source/opt/function.h"
|
||||
#include "source/opt/instruction.h"
|
||||
#include "source/opt/ir_builder.h"
|
||||
#include "source/opt/ir_context.h"
|
||||
#include "source/opt/module.h"
|
||||
#include "source/opt/type_manager.h"
|
||||
#include "source/opt/types.h"
|
||||
#include "source/util/make_unique.h"
|
||||
#include "source/util/string_utils.h"
|
||||
|
||||
#include <format>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
namespace {
|
||||
using spvtools::opt::BasicBlock;
|
||||
using spvtools::opt::Function;
|
||||
using spvtools::opt::Instruction;
|
||||
using spvtools::opt::InstructionBuilder;
|
||||
using spvtools::opt::IRContext;
|
||||
using spvtools::opt::Operand;
|
||||
namespace analysis = spvtools::opt::analysis;
|
||||
|
||||
const IRContext::Analysis kBuilderAnalyses = static_cast<IRContext::Analysis>(
|
||||
IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping);
|
||||
|
||||
Instruction* EntryPoint(IRContext* ctx) {
|
||||
for (Instruction& ep : ctx->module()->entry_points()) {
|
||||
return &ep;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Function* EntryFunction(IRContext* ctx, Instruction* entryPoint) {
|
||||
const uint32_t id = entryPoint->GetSingleWordInOperand(1);
|
||||
for (auto& function : *ctx->module()) {
|
||||
if (function.result_id() == id) return &function;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t PointeeOf(IRContext* ctx, uint32_t pointerTypeId) {
|
||||
Instruction* ptrType = ctx->get_def_use_mgr()->GetDef(pointerTypeId);
|
||||
if (ptrType == nullptr || ptrType->opcode() != spv::Op::OpTypePointer) return 0;
|
||||
return ptrType->GetSingleWordInOperand(1);
|
||||
}
|
||||
|
||||
bool PlainConstantValue(IRContext* ctx, uint32_t id, uint32_t& outValue) {
|
||||
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
|
||||
if (def == nullptr || def->opcode() != spv::Op::OpConstant) return false;
|
||||
if (def->NumInOperands() != 1) return false;
|
||||
outValue = def->GetSingleWordInOperand(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Length of an OpTypeArray of 32-bit float with a plain constant length, else 0.
|
||||
uint32_t FloatArrayLength(IRContext* ctx, uint32_t typeId) {
|
||||
Instruction* t = ctx->get_def_use_mgr()->GetDef(typeId);
|
||||
if (t == nullptr || t->opcode() != spv::Op::OpTypeArray) return 0;
|
||||
Instruction* element = ctx->get_def_use_mgr()->GetDef(t->GetSingleWordInOperand(0));
|
||||
if (element == nullptr || element->opcode() != spv::Op::OpTypeFloat ||
|
||||
element->GetSingleWordInOperand(0) != 32) {
|
||||
return 0;
|
||||
}
|
||||
uint32_t length = 0;
|
||||
if (!PlainConstantValue(ctx, t->GetSingleWordInOperand(1), length)) return 0;
|
||||
return length;
|
||||
}
|
||||
|
||||
void AddNameFor(IRContext* ctx, uint32_t id, const String& name) {
|
||||
std::vector<Operand> operands;
|
||||
operands.push_back({SPV_OPERAND_TYPE_ID, {id}});
|
||||
operands.push_back({SPV_OPERAND_TYPE_LITERAL_STRING, spvtools::utils::MakeVector(name)});
|
||||
ctx->AddDebug2Inst(spvtools::MakeUnique<Instruction>(ctx, spv::Op::OpName, 0, 0, operands));
|
||||
}
|
||||
|
||||
uint32_t AddGlobalVariable(IRContext* ctx, uint32_t pointeeTypeId, spv::StorageClass storage,
|
||||
const String& name) {
|
||||
const uint32_t ptrTypeId = ctx->get_type_mgr()->FindPointerToType(pointeeTypeId, storage);
|
||||
if (ptrTypeId == 0) return 0;
|
||||
const uint32_t varId = ctx->TakeNextId();
|
||||
auto var = spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpVariable, ptrTypeId, varId,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_STORAGE_CLASS, {static_cast<uint32_t>(storage)}}});
|
||||
Instruction* varInst = var.get();
|
||||
ctx->AddGlobalValue(std::move(var));
|
||||
ctx->get_def_use_mgr()->AnalyzeInstDefUse(varInst);
|
||||
AddNameFor(ctx, varId, name);
|
||||
return varId;
|
||||
}
|
||||
|
||||
void AddLocationDecoration(IRContext* ctx, uint32_t id, uint32_t location) {
|
||||
ctx->AddAnnotationInst(spvtools::MakeUnique<Instruction>(
|
||||
ctx, spv::Op::OpDecorate, 0, 0,
|
||||
std::initializer_list<Operand>{
|
||||
{SPV_OPERAND_TYPE_ID, {id}},
|
||||
{SPV_OPERAND_TYPE_DECORATION, {static_cast<uint32_t>(spv::Decoration::Location)}},
|
||||
{SPV_OPERAND_TYPE_LITERAL_INTEGER, {location}}}));
|
||||
}
|
||||
|
||||
void RemoveFromInterface(Instruction* entryPoint, uint32_t id) {
|
||||
Instruction::OperandList kept;
|
||||
for (uint32_t i = 0; i < entryPoint->NumInOperands(); ++i) {
|
||||
const Operand& operand = entryPoint->GetInOperand(i);
|
||||
if (i >= 3 && operand.type == SPV_OPERAND_TYPE_ID && operand.words.size() == 1 &&
|
||||
operand.words[0] == id) {
|
||||
continue;
|
||||
}
|
||||
kept.push_back(operand);
|
||||
}
|
||||
entryPoint->SetInOperands(Move(kept));
|
||||
}
|
||||
|
||||
// Whether every use of a pointer into the built-in is one the retype below can
|
||||
// follow: loads, stores (as the POINTER), copies, and further access chains.
|
||||
bool PointerUsesAreRetypable(IRContext* ctx, Instruction* pointer, String& reason) {
|
||||
bool ok = true;
|
||||
ctx->get_def_use_mgr()->ForEachUse(pointer, [&](Instruction* user, uint32_t operandIndex) {
|
||||
if (!ok) return;
|
||||
switch (user->opcode()) {
|
||||
case spv::Op::OpName:
|
||||
case spv::Op::OpDecorate:
|
||||
case spv::Op::OpEntryPoint:
|
||||
return;
|
||||
case spv::Op::OpLoad:
|
||||
case spv::Op::OpCopyMemory:
|
||||
return;
|
||||
case spv::Op::OpStore:
|
||||
// Operand 0 of OpStore is the pointer; anything else would be
|
||||
// storing the pointer itself, which logical addressing forbids.
|
||||
if (operandIndex != 0) {
|
||||
ok = false;
|
||||
reason = "a pointer into gl_ClipDistance stored as a value";
|
||||
}
|
||||
return;
|
||||
case spv::Op::OpAccessChain:
|
||||
case spv::Op::OpInBoundsAccessChain:
|
||||
// Base operand: the instruction's first id after result type/id.
|
||||
if (operandIndex != 2) {
|
||||
ok = false;
|
||||
reason = "a pointer into gl_ClipDistance used as an access-chain index";
|
||||
return;
|
||||
}
|
||||
if (!PointerUsesAreRetypable(ctx, user, reason)) ok = false;
|
||||
return;
|
||||
default:
|
||||
ok = false;
|
||||
reason = std::format("a pointer into gl_ClipDistance reaching SPIR-V opcode {}",
|
||||
static_cast<uint32_t>(user->opcode()));
|
||||
return;
|
||||
}
|
||||
});
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Re-points every access chain hanging off `pointer` (which now lives in Private
|
||||
// storage) at a Private pointer type of the same pointee.
|
||||
void RetypeChainUsers(IRContext* ctx, Instruction* pointer) {
|
||||
std::vector<Instruction*> chains;
|
||||
ctx->get_def_use_mgr()->ForEachUser(pointer, [&](Instruction* user) {
|
||||
if ((user->opcode() == spv::Op::OpAccessChain ||
|
||||
user->opcode() == spv::Op::OpInBoundsAccessChain) &&
|
||||
user->GetSingleWordInOperand(0) == pointer->result_id()) {
|
||||
chains.push_back(user);
|
||||
}
|
||||
});
|
||||
for (Instruction* chain : chains) {
|
||||
const uint32_t pointee = PointeeOf(ctx, chain->type_id());
|
||||
chain->SetResultType(
|
||||
ctx->get_type_mgr()->FindPointerToType(pointee, spv::StorageClass::Private));
|
||||
ctx->get_def_use_mgr()->AnalyzeInstUse(chain);
|
||||
RetypeChainUsers(ctx, chain);
|
||||
}
|
||||
}
|
||||
|
||||
// A scalar constant of the given 32-bit integer type.
|
||||
uint32_t IntConstantOfType(IRContext* ctx, uint32_t intTypeId, uint32_t value) {
|
||||
const analysis::Type* type = ctx->get_type_mgr()->GetType(intTypeId);
|
||||
const analysis::Integer* integer = type != nullptr ? type->AsInteger() : nullptr;
|
||||
if (integer != nullptr && integer->IsSigned()) {
|
||||
return ctx->get_constant_mgr()->GetSIntConstId(static_cast<int32_t>(value));
|
||||
}
|
||||
return ctx->get_constant_mgr()->GetUIntConstId(value);
|
||||
}
|
||||
|
||||
bool IsInt32Type(IRContext* ctx, uint32_t typeId) {
|
||||
const analysis::Type* type = ctx->get_type_mgr()->GetType(typeId);
|
||||
const analysis::Integer* integer = type != nullptr ? type->AsInteger() : nullptr;
|
||||
return integer != nullptr && integer->width() == 32;
|
||||
}
|
||||
|
||||
struct ClipSites {
|
||||
// struct type id -> member index of its BuiltIn ClipDistance member
|
||||
std::vector<std::pair<uint32_t, uint32_t>> members;
|
||||
std::unordered_set<uint32_t> standaloneVars;
|
||||
Instruction* capability = nullptr;
|
||||
};
|
||||
|
||||
ClipSites FindClipSites(IRContext* ctx) {
|
||||
ClipSites sites;
|
||||
for (Instruction& ann : ctx->annotations()) {
|
||||
if (ann.opcode() == spv::Op::OpMemberDecorate && ann.NumInOperands() >= 4 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(2)) == spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(3)) == spv::BuiltIn::ClipDistance) {
|
||||
sites.members.emplace_back(ann.GetSingleWordInOperand(0), ann.GetSingleWordInOperand(1));
|
||||
} else if (ann.opcode() == spv::Op::OpDecorate && ann.NumInOperands() >= 3 &&
|
||||
static_cast<spv::Decoration>(ann.GetSingleWordInOperand(1)) ==
|
||||
spv::Decoration::BuiltIn &&
|
||||
static_cast<spv::BuiltIn>(ann.GetSingleWordInOperand(2)) ==
|
||||
spv::BuiltIn::ClipDistance) {
|
||||
Instruction* var = ctx->get_def_use_mgr()->GetDef(ann.GetSingleWordInOperand(0));
|
||||
if (var != nullptr && var->opcode() == spv::Op::OpVariable) {
|
||||
sites.standaloneVars.insert(var->result_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Instruction& capability : ctx->module()->capabilities()) {
|
||||
if (capability.NumInOperands() >= 1 &&
|
||||
static_cast<spv::Capability>(capability.GetSingleWordInOperand(0)) ==
|
||||
spv::Capability::ClipDistance) {
|
||||
sites.capability = &capability;
|
||||
}
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
bool ClipMemberOf(const ClipSites& sites, uint32_t structId, uint32_t& member) {
|
||||
for (const auto& [id, index] : sites.members) {
|
||||
if (id == structId) {
|
||||
member = index;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ClipDistanceModuleProbe EmulateClipDistancePass::ProbeModule(const Vector<Uint32>& spirv) {
|
||||
ClipDistanceModuleProbe probe;
|
||||
if (spirv.empty()) {
|
||||
probe.parsed = true;
|
||||
return probe;
|
||||
}
|
||||
std::unique_ptr<IRContext> ctx = spvtools::BuildModule(
|
||||
SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {},
|
||||
spirv.data(), spirv.size());
|
||||
if (!ctx) return probe;
|
||||
probe.parsed = true;
|
||||
const ClipSites sites = FindClipSites(ctx.get());
|
||||
probe.declaresClipDistanceCapability = sites.capability != nullptr;
|
||||
for (Instruction& inst : ctx->module()->types_values()) {
|
||||
if (inst.opcode() != spv::Op::OpVariable) continue;
|
||||
if (static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0)) != spv::StorageClass::Output) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t pointee = PointeeOf(ctx.get(), inst.type_id());
|
||||
if (sites.standaloneVars.count(inst.result_id()) != 0) {
|
||||
probe.outputDistanceCount = FloatArrayLength(ctx.get(), pointee);
|
||||
break;
|
||||
}
|
||||
Instruction* type = ctx->get_def_use_mgr()->GetDef(pointee);
|
||||
uint32_t member = 0;
|
||||
if (type != nullptr && type->opcode() == spv::Op::OpTypeStruct &&
|
||||
ClipMemberOf(sites, type->result_id(), member)) {
|
||||
probe.outputDistanceCount = FloatArrayLength(ctx.get(), type->GetSingleWordInOperand(member));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return probe;
|
||||
}
|
||||
|
||||
spvtools::opt::Pass::Status EmulateClipDistancePass::Process() {
|
||||
IRContext* ctx = context();
|
||||
auto* defUse = ctx->get_def_use_mgr();
|
||||
auto* typeMgr = ctx->get_type_mgr();
|
||||
auto* constMgr = ctx->get_constant_mgr();
|
||||
using Role = EmulateClipDistanceOptions::Role;
|
||||
|
||||
Instruction* entryPoint = EntryPoint(ctx);
|
||||
if (entryPoint == nullptr) return Status::SuccessWithoutChange;
|
||||
const auto model = static_cast<spv::ExecutionModel>(entryPoint->GetSingleWordInOperand(0));
|
||||
const bool isFragment = model == spv::ExecutionModel::Fragment;
|
||||
const bool isGeometry = model == spv::ExecutionModel::Geometry;
|
||||
const bool isPreRaster = model == spv::ExecutionModel::Vertex ||
|
||||
model == spv::ExecutionModel::TessellationControl ||
|
||||
model == spv::ExecutionModel::TessellationEvaluation || isGeometry;
|
||||
|
||||
const auto decline = [&](String reason) {
|
||||
if (m_report != nullptr) {
|
||||
m_report->declined = true;
|
||||
m_report->declineReason = Move(reason);
|
||||
}
|
||||
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 && !isPreRaster) {
|
||||
return decline("producer/redirect role on a non-pre-rasterization stage");
|
||||
}
|
||||
if (m_options.role == Role::Producer && model == spv::ExecutionModel::TessellationControl) {
|
||||
return decline("a control stage cannot be the last pre-rasterization stage");
|
||||
}
|
||||
|
||||
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
|
||||
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;
|
||||
const auto storage = static_cast<spv::StorageClass>(inst.GetSingleWordInOperand(0));
|
||||
if (storage != spv::StorageClass::Input && storage != spv::StorageClass::Output) continue;
|
||||
const uint32_t pointeeId = PointeeOf(ctx, inst.type_id());
|
||||
|
||||
if (sites.standaloneVars.count(inst.result_id()) != 0) {
|
||||
if (FloatArrayLength(ctx, pointeeId) == 0) {
|
||||
return decline("a standalone gl_ClipDistance that is not a sized float array");
|
||||
}
|
||||
// A pre-rasterization stage's standalone INPUT would be gl_in data,
|
||||
// which a non-arrayed variable cannot be; only the fragment stage reads
|
||||
// gl_ClipDistance this way.
|
||||
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 (!PointerUsesAreRetypable(ctx, &inst, reason)) return decline(Move(reason));
|
||||
standaloneVars.push_back(&inst);
|
||||
continue;
|
||||
}
|
||||
|
||||
Instruction* pointee = defUse->GetDef(pointeeId);
|
||||
if (pointee == nullptr) continue;
|
||||
bool arrayed = false;
|
||||
if (pointee->opcode() == spv::Op::OpTypeArray) {
|
||||
arrayed = true;
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
const uint32_t memberTypeId = pointee->GetSingleWordInOperand(clipMember);
|
||||
const uint32_t memberOperand = arrayed ? 2u : 1u; // [vertex,] member, ...
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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");
|
||||
|
||||
Uint32 producedCount = 0;
|
||||
if (m_options.role == Role::Producer) {
|
||||
producedCount = std::min<Uint32>(shadowLength, kMaxCarriers * 4);
|
||||
if (shadowLength > kMaxCarriers * 4) return decline("more than eight clip distances");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccesses && sites.capability == nullptr && producedCount == 0 && consumedCount == 0) {
|
||||
return Status::SuccessWithoutChange;
|
||||
}
|
||||
|
||||
// ---- Phase 2: MUTATION. Nothing below may decline. ----
|
||||
const bool interfaceListsAllGlobals = ctx->module()->version() >= 0x00010400u;
|
||||
uint32_t shadowId = 0;
|
||||
if (shadowTypeId != 0 && (hasAccesses || producedCount > 0)) {
|
||||
shadowId = AddGlobalVariable(ctx, shadowTypeId, spv::StorageClass::Private, kShadowName);
|
||||
if (interfaceListsAllGlobals) entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {shadowId}});
|
||||
}
|
||||
|
||||
for (Instruction* chain : memberChains) {
|
||||
if (chain->NumInOperands() == 2) {
|
||||
// The chain IS the whole array: every use now addresses the shadow.
|
||||
ctx->ReplaceAllUsesWith(chain->result_id(), shadowId);
|
||||
ctx->KillInst(chain);
|
||||
continue;
|
||||
}
|
||||
Instruction::OperandList operands;
|
||||
operands.push_back({SPV_OPERAND_TYPE_ID, {shadowId}});
|
||||
for (uint32_t i = 2; i < chain->NumInOperands(); ++i) {
|
||||
operands.push_back(chain->GetInOperand(i));
|
||||
}
|
||||
chain->SetInOperands(Move(operands));
|
||||
chain->SetResultType(
|
||||
typeMgr->FindPointerToType(PointeeOf(ctx, chain->type_id()), spv::StorageClass::Private));
|
||||
defUse->AnalyzeInstUse(chain);
|
||||
RetypeChainUsers(ctx, chain);
|
||||
}
|
||||
for (Instruction* var : standaloneVars) {
|
||||
const uint32_t varId = var->result_id();
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
// ---- producer: write the carriers ----
|
||||
if (producedCount > 0) {
|
||||
const Uint32 carrierCount = (producedCount + 3) / 4;
|
||||
uint32_t carriers[kMaxCarriers] = {0, 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);
|
||||
entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {carriers[k]}});
|
||||
}
|
||||
uint32_t ptrUniformEnables = 0;
|
||||
uint32_t enablesMemberConst = 0;
|
||||
if (enablesVarId != 0) {
|
||||
ptrUniformEnables = typeMgr->FindPointerToType(enablesTypeId, spv::StorageClass::Uniform);
|
||||
enablesMemberConst = constMgr->GetSIntConstId(static_cast<int32_t>(enablesMemberIndex));
|
||||
if (interfaceListsAllGlobals) {
|
||||
bool listed = false;
|
||||
for (uint32_t i = 3; i < entryPoint->NumInOperands(); ++i) {
|
||||
if (entryPoint->GetSingleWordInOperand(i) == enablesVarId) listed = true;
|
||||
}
|
||||
if (!listed) entryPoint->AddOperand({SPV_OPERAND_TYPE_ID, {enablesVarId}});
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
InstructionBuilder b(ctx, site, kBuilderAnalyses);
|
||||
uint32_t maskId = 0;
|
||||
if (enablesVarId != 0) {
|
||||
Instruction* ptr = b.AddAccessChain(ptrUniformEnables, enablesVarId, {enablesMemberConst});
|
||||
maskId = b.AddLoad(enablesTypeId, ptr->result_id())->result_id();
|
||||
}
|
||||
std::vector<uint32_t> values;
|
||||
for (Uint32 i = 0; i < producedCount; ++i) {
|
||||
Instruction* ptr = b.AddAccessChain(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 on = b.AddBinaryOp(boolTypeId, spv::Op::OpINotEqual, bit,
|
||||
IntConstantOfType(ctx, enablesTypeId, 0u))
|
||||
->result_id();
|
||||
value = b.AddSelect(floatTypeId, on, value, oneId)->result_id();
|
||||
}
|
||||
values.push_back(value);
|
||||
}
|
||||
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]})
|
||||
->result_id();
|
||||
b.AddStore(carriers[k], composite);
|
||||
}
|
||||
}
|
||||
if (m_report != nullptr) {
|
||||
m_report->distanceCount = producedCount;
|
||||
m_report->enablesFound = enablesVarId != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_report != nullptr) m_report->rewrote = true;
|
||||
ctx->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone);
|
||||
return Status::SuccessWithChange;
|
||||
}
|
||||
|
||||
spvtools::Optimizer::PassToken EmulateClipDistancePass::CreateEmulateClipDistancePass(
|
||||
EmulateClipDistanceOptions options, EmulateClipDistanceReport* report) {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<EmulateClipDistancePass>(Move(options), report));
|
||||
}
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -0,0 +1,128 @@
|
||||
// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateClipDistancePass.h
|
||||
// Copyright (c) 2025-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
|
||||
|
||||
#pragma once
|
||||
#include "source/opt/pass.h"
|
||||
#include "spirv-tools/optimizer.hpp"
|
||||
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_Util {
|
||||
namespace ShaderTranspiler {
|
||||
// Emulates gl_ClipDistance on a backend that cannot host the built-in at all: an ES
|
||||
// driver without GL_EXT_clip_cull_distance, or a Vulkan device with
|
||||
// shaderClipDistance == VK_FALSE (every Mali GPU measured so far is both). GL 3.0+
|
||||
// requires GL_MAX_CLIP_DISTANCES >= 8, so a frontend that reports GL 4.6 cannot
|
||||
// honestly answer 0 there, and answering 8 without this pass makes every clipping
|
||||
// draw either fail to compile or render unclipped.
|
||||
//
|
||||
// WHAT THE EMULATION IS. Clip distance d is linear in clip space, so its
|
||||
// perspective-correct interpolant at a fragment is exactly the distance the
|
||||
// rasterizer's clipper would have evaluated there; discarding the fragments where it
|
||||
// is negative removes the same pixels hardware clipping removes. So:
|
||||
// - every access to the ClipDistance built-in, in every stage, is redirected onto
|
||||
// a Private float[N] shadow (mg_ClipDistanceShadow) and the ClipDistance
|
||||
// capability is stripped - the gl_PerVertex member stays declared but unaccessed,
|
||||
// which is the shape glslang emits for every program that never clips;
|
||||
// - the LAST pre-rasterization stage (the PRODUCER) writes
|
||||
// `enabled(i) ? shadow[i] : 1.0` into ceil(N/4) ordinary vec4 varyings
|
||||
// (mg_ClipDistanceEmu0/1) at every return of its entry function - before every
|
||||
// EmitVertex in a geometry stage, which is when one vertex's outputs latch.
|
||||
// enabled(i) is bit i of the reserved default-block uniform the source pipeline
|
||||
// declared (CLIP_DISTANCE_ENABLES_UNIFORM_NAME), which the draw path refreshes
|
||||
// from GL_CLIP_DISTANCE0..7 before every draw. Applying the enables HERE keeps
|
||||
// the varyings free of pipeline-key state and makes a disabled distance - even
|
||||
// one the shader writes negative - clip nothing, as GL requires;
|
||||
// - the fragment stage (the CONSUMER) reads the carriers before anything else and
|
||||
// executes OpKill when any carried distance is negative. Its own gl_ClipDistance
|
||||
// reads are served from the same carriers (GLSL leaves the disabled elements
|
||||
// undefined there, so the 1.0 they carry is a legal value).
|
||||
// - every other stage that touches the built-in (a vertex stage ahead of a
|
||||
// geometry stage, say) only has its accesses redirected: in GL those writes are
|
||||
// dead unless a later stage reads gl_in[].gl_ClipDistance.
|
||||
//
|
||||
// WHAT IT COSTS: two varying locations past every location the program uses, and a
|
||||
// discard in the fragment stage of every clipping program (early depth/stencil is
|
||||
// lost for those programs). Cull distance is NOT emulated: culling is per primitive,
|
||||
// which a per-fragment discard cannot reproduce.
|
||||
//
|
||||
// WHAT DECLINES (the module is left byte-identical and the report says why):
|
||||
// gl_in[].gl_ClipDistance reads and gl_out[].gl_ClipDistance writes (arrayed
|
||||
// per-vertex access in tessellation/geometry stages), a non-constant gl_PerVertex
|
||||
// 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).
|
||||
//
|
||||
// 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.
|
||||
struct EmulateClipDistanceOptions {
|
||||
enum class Role : Uint8 {
|
||||
// Redirect the accesses and strip the capability; no carriers.
|
||||
Redirect,
|
||||
// The last pre-rasterization stage: redirect, then write the carriers.
|
||||
Producer,
|
||||
// The fragment stage: read the carriers, discard, serve gl_ClipDistance reads.
|
||||
Consumer,
|
||||
};
|
||||
Role role = Role::Redirect;
|
||||
// The first carrier location; carrier k sits at location + k.
|
||||
Uint32 location = 0;
|
||||
// Consumer only: how many distances the producer carries (0 = none, in which
|
||||
// case the stage only has its own reads redirected).
|
||||
Uint32 distanceCount = 0;
|
||||
// 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;
|
||||
};
|
||||
|
||||
struct EmulateClipDistanceReport {
|
||||
Bool declined = false;
|
||||
String declineReason;
|
||||
Bool rewrote = false;
|
||||
// Producer: how many distances it now carries.
|
||||
Uint32 distanceCount = 0;
|
||||
// Producer: whether the enables uniform was found (false = all-enabled fallback).
|
||||
Bool enablesFound = false;
|
||||
};
|
||||
|
||||
// What one module looks like to the program-wide driver, from a single parse.
|
||||
struct ClipDistanceModuleProbe {
|
||||
Bool parsed = false;
|
||||
// glslang declares the capability on first ACCESS, so this is the trigger.
|
||||
Bool declaresClipDistanceCapability = false;
|
||||
// The length of the module's non-arrayed ClipDistance OUTPUT (block member or
|
||||
// standalone variable), or 0 when it declares none.
|
||||
Uint32 outputDistanceCount = 0;
|
||||
};
|
||||
|
||||
class EmulateClipDistancePass : public spvtools::opt::Pass {
|
||||
public:
|
||||
static constexpr const char* kShadowName = "mg_ClipDistanceShadow";
|
||||
static constexpr Uint32 kMaxCarriers = 2;
|
||||
static const char* CarrierName(Uint32 index) {
|
||||
return index == 0 ? "mg_ClipDistanceEmu0" : "mg_ClipDistanceEmu1";
|
||||
}
|
||||
|
||||
EmulateClipDistancePass(EmulateClipDistanceOptions options, EmulateClipDistanceReport* report)
|
||||
: m_options(Move(options)), m_report(report) {}
|
||||
const char* name() const override { return "mobilegl-emulate-clip-distance"; }
|
||||
Status Process() override;
|
||||
|
||||
static ClipDistanceModuleProbe ProbeModule(const Vector<Uint32>& spirv);
|
||||
|
||||
static spvtools::Optimizer::PassToken CreateEmulateClipDistancePass(
|
||||
EmulateClipDistanceOptions options, EmulateClipDistanceReport* report);
|
||||
|
||||
private:
|
||||
EmulateClipDistanceOptions m_options;
|
||||
EmulateClipDistanceReport* m_report;
|
||||
};
|
||||
} // namespace ShaderTranspiler
|
||||
} // namespace MG_Util
|
||||
} // namespace MobileGL
|
||||
@@ -28,6 +28,14 @@ namespace MobileGL {
|
||||
// uses gl_NumSamples the link task will still hide its uniform. That is the same
|
||||
// bargain every mg_-prefixed rewrite in this pipeline strikes.
|
||||
inline const char* NUM_SAMPLES_UNIFORM_NAME = "mg_NumSamples";
|
||||
// The default-block uniform InjectClipDistanceEnablesShim declares, on a backend that
|
||||
// emulates gl_ClipDistance (BackendCaps::EmulatesClipDistance), in every
|
||||
// pre-rasterization stage that names the built-in: bit i is GL_CLIP_DISTANCE0 + i's
|
||||
// enable. EmulateClipDistancePass reads it where the producing stage writes its
|
||||
// carriers; the draw path refreshes it from the render state before every draw, and
|
||||
// the link task hides it from the GL uniform surface exactly as it hides
|
||||
// NUM_SAMPLES_UNIFORM_NAME. Reserved on the same terms.
|
||||
inline const char* CLIP_DISTANCE_ENABLES_UNIFORM_NAME = "mg_ClipDistanceEnables";
|
||||
// glslang's Vulkan-relaxed parse rewrites every atomic_uint into a member of a
|
||||
// synthesized storage block named "<this>_<GL atomic-counter binding>"
|
||||
// (ParseContextBase::growAtomicCounterBlock). That block IS the GL atomic counter
|
||||
|
||||
Reference in New Issue
Block a user