From c158bfa584addc2108d8a4480300cdd8f971bc47 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 20 Jul 2026 07:39:25 -0400 Subject: [PATCH] [Fix] (DirectVulkan): narrow the blended depth-write quirk to order-independent accumulation blends, exempting sorted-transparency, gl_FragDepth writers and fully masked attachments --- MobileGL/Config.h | 8 +- .../DirectVulkan/Renderer/PipelineFactory.cpp | 94 ++++- .../DirectVulkan/Renderer/PipelineFactory.h | 28 +- .../DirectVulkan/Renderer/ProgramFactory.cpp | 19 + .../DirectVulkan/Renderer/ProgramFactory.h | 13 + .../DirectVulkan/Renderer/VulkanRenderer.cpp | 23 +- MobileGL/MG_Test/CMakeLists.txt | 1 + MobileGL/MG_Test/Pipeline/CMakeLists.txt | 27 ++ .../MG_Test/Pipeline/PipelineQuirkTest.cpp | 341 ++++++++++++++++++ 9 files changed, 527 insertions(+), 27 deletions(-) create mode 100644 MobileGL/MG_Test/Pipeline/CMakeLists.txt create mode 100644 MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 3d1435a8..7bd5b213 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -81,9 +81,11 @@ namespace MobileGL::MG_Config { // subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry). QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto; // MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that - // strips depth writes from blended pipelines on drivers without cross-pipeline - // vertex position invariance (see VulkanRenderer's PipelineFactory setup). Auto - // detects Qualcomm. + // strips depth writes from accumulation-blended pipelines (MIN/MAX or additive + // ONE+ONE - the multi-pass depth-equality signature) on drivers without + // cross-pipeline vertex position invariance. Sorted-transparency "over" blends, + // gl_FragDepth writers, and fully color-masked attachments are exempt (see + // PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm. QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto; // MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device // feature off. It is enabled by default to match GL's defined out-of-range fetch diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index 235a3677..c5aa3566 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -109,10 +109,81 @@ namespace MobileGL::MG_Backend::DirectVulkan { "vkCreatePipelineCache"); } + // Must be called once, before any pipeline is created: the flag is not part of the + // pipeline hash, so flipping it mid-life would serve cached pipelines built under the + // old value. void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) { s_suppressBlendedDepthWrite = enabled; } + Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride, + Uint32 vendorId) { + static constexpr Uint32 kVendorIdQualcomm = 0x5143; + switch (quirkOverride) { + case MG_Config::QuirkOverride::ForceOn: + return true; + case MG_Config::QuirkOverride::ForceOff: + return false; + case MG_Config::QuirkOverride::Auto: + default: + return vendorId == kVendorIdQualcomm; + } + } + + namespace { + // Order-independent accumulation blending: the write order of overlapping fragments + // does not change the result, which is what lets multi-pass chains re-rasterize the + // same geometry and combine per-pass contributions (MC 26.3 OIT: GL_MAX depth + // bounds, additive ONE+ONE transmittance/accumulate). Sorted-transparency "over" + // compositing (SRC_ALPHA-style factors) is order-dependent, drawn once per surface, + // and relies on its depth writes for occlusion - it must not be treated as hazardous. + // MIN/MAX ignore blend factors entirely per the Vulkan spec. + // + // Deliberately color-channel only. A separate-alpha accumulation + // (glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX)) whose color channel is an ordinary + // over-blend is not treated as hazardous: no known content pairs that shape with a + // depth-equality chain, and widening the test would re-capture sorted transparency. + Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) { + if (attachment.colorBlendOp == VK_BLEND_OP_MIN || attachment.colorBlendOp == VK_BLEND_OP_MAX) { + return true; + } + return attachment.colorBlendOp == VK_BLEND_OP_ADD && + attachment.srcColorBlendFactor == VK_BLEND_FACTOR_ONE && + attachment.dstColorBlendFactor == VK_BLEND_FACTOR_ONE; + } + } // namespace + + Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) { + if (!payload.depthWriteEnable) { + return false; + } + // A shader that assigns gl_FragDepth supplies depth itself rather than taking the + // pipeline's interpolated Z, so a driver that varies the vertex position math + // between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z + // passthrough is the exception that stays exposed; no known content pairs one with + // an equality chain, and 26.3's composite is a genuine computed-depth writer.) + if (payload.fragmentReplacesDepth) { + return false; + } + for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { + const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i]; + if (attachment.blendEnable != VK_TRUE) { + continue; + } + // All color writes masked: blending is moot (depth-prepass pattern that left + // GL_BLEND enabled); stripping the depth write would delete the whole prepass. + if (attachment.colorWriteMask == 0) { + continue; + } + // Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass + // accumulates into a 2-target MRT and must stay stripped. + if (IsAccumulationBlend(attachment)) { + return true; + } + } + return false; + } + PipelineFactory::~PipelineFactory() { DestroyAll(); if (m_pipelineCache != VK_NULL_HANDLE) { @@ -157,6 +228,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp))); XXHASH_VERIFY( XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp))); + XXHASH_VERIFY( + XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth))); if (payload.colorAttachmentCount > 0) { XXHASH_VERIFY(XXH64_update( m_hashState, @@ -263,17 +336,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { colorAttachments[i] = payload.colorBlendAttachments[i]; } - // Suppress depth writes on blended pipelines when the active driver cannot keep - // vertex positions invariant across the pipelines of a multi-pass depth-equality - // chain (see SetSuppressBlendedDepthWrite). Blended draws that write depth are rare - // and the equality-dependent prepass pattern is exactly the case that breaks. - if (s_suppressBlendedDepthWrite && depthStencil.depthWriteEnable == VK_TRUE) { - for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) { - if (colorAttachments[i].blendEnable == VK_TRUE) { - depthStencil.depthWriteEnable = VK_FALSE; - break; - } - } + // Suppress depth writes on accumulation-blended pipelines when the active driver + // cannot keep vertex positions invariant across the pipelines of a multi-pass + // depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed + // in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water), + // gl_FragDepth writers, and masked-out attachments keep their depth writes. + // This bakes the decision into the pipeline, which only works because depth write is + // static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates + // would let the record-time value override it and silently disable the quirk. + if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) { + depthStencil.depthWriteEnable = VK_FALSE; } VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index 7b16fff4..5da32176 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -49,6 +49,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP; VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP; VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS; + // The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the + // pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite). + Bool fragmentReplacesDepth = false; Array colorBlendAttachments{}; const Vector* stages = nullptr; const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr; @@ -62,13 +65,26 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload); void DestroyAll(); - // Driver quirk: suppress depth writes on blended pipelines. Multi-pass depth-equality - // rendering (a blended prepass writes depth that later passes re-test with an - // equality-inclusive compare on the re-rasterized geometry) requires cross-pipeline - // position invariance that some mobile compilers do not provide, even with the - // SPIR-V Invariant decoration; whole primitives then drop out of the later passes. - // Set at renderer initialization based on the active driver. + // Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass + // depth-equality rendering (a blended prepass writes depth that later passes re-test + // with an equality-inclusive compare on the re-rasterized geometry) requires + // cross-pipeline position invariance that some mobile compilers do not provide, even + // with the SPIR-V Invariant decoration; whole primitives then drop out of the later + // passes. Only order-independent accumulation blends (MIN/MAX, additive ONE+ONE) are + // stripped - that is the signature of such equality chains (MC 26.3 OIT) - while + // sorted-transparency "over" compositing (e.g. vanilla MC water, SRC_ALPHA factors), + // which draws each surface once and depends on its depth writes to occlude later + // passes, keeps them. Set at renderer initialization based on the active driver. static void SetSuppressBlendedDepthWrite(Bool enabled); + static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; } + // Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on + // the known-affected vendor (Qualcomm). + static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride, + Uint32 vendorId); + // Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and + // non-accumulation blends); combined with the device flag in CreatePipeline. Static + // and payload-only so tests can pin the contract without a VkDevice. + static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload); private: VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 5cce9401..cc09bbfe 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -1218,6 +1218,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } // namespace + // A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself + // instead of taking the pipeline's interpolated Z, so a driver that varies the vertex + // position math between pipelines cannot desynchronize it; the blended depth-write + // quirk therefore leaves it alone (see PipelineFactory::ShouldSuppressDepthWrite). + Bool ProgramFactory::ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule) { + for (Uint32 entryIndex = 0; entryIndex < reflectModule.entry_point_count; ++entryIndex) { + const SpvReflectEntryPoint& entryPoint = reflectModule.entry_points[entryIndex]; + for (Uint32 modeIndex = 0; modeIndex < entryPoint.execution_mode_count; ++modeIndex) { + if (entryPoint.execution_modes[modeIndex] == SpvExecutionModeDepthReplacing) { + return true; + } + } + } + return false; + } + VkShaderStageFlagBits ProgramFactory::ToVkStage(ShaderStage stage) { switch (stage) { case ShaderStage::Vertex: @@ -1512,6 +1528,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkProgramObject& entry) const { entry.activeFragmentOutputLocationMask = 0; entry.fragmentOutputTypes.fill(0); + entry.fragmentReplacesDepth = false; for (SizeT moduleIndex = 0; moduleIndex < shaders.size() && moduleIndex < spirv.size(); ++moduleIndex) { if (!shaders[moduleIndex] || shaders[moduleIndex]->GetShaderStage() != ShaderStage::Fragment) { @@ -1533,6 +1550,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { continue; } + entry.fragmentReplacesDepth = ReflectedFragmentReplacesDepth(reflectModule); + uint32_t outputCount = 0; SpvReflectResult reflectResult = spvReflectEnumerateOutputVariables(&reflectModule, &outputCount, nullptr); MOBILEGL_ASSERT(reflectResult == SPV_REFLECT_RESULT_SUCCESS, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 2021c81e..5df2b52b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -78,6 +78,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { ShaderStage rasterizationProducerStage = ShaderStage::Unknown; Uint32 producerOutputComponentCount = 0; Uint32 fragmentInputComponentCount = 0; + // The fragment module declares the DepthReplacing execution mode (writes + // gl_FragDepth); shader-computed depth is immune to the cross-pipeline + // position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite). + Bool fragmentReplacesDepth = false; static inline VkDevice s_device = VK_NULL_HANDLE; @@ -111,6 +115,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { rasterizationProducerStage = other.rasterizationProducerStage; producerOutputComponentCount = other.producerOutputComponentCount; fragmentInputComponentCount = other.fragmentInputComponentCount; + fragmentReplacesDepth = other.fragmentReplacesDepth; other.hash = 0; other.descriptorSetLayout = VK_NULL_HANDLE; other.pipelineLayout = VK_NULL_HANDLE; @@ -121,6 +126,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { other.rasterizationProducerStage = ShaderStage::Unknown; other.producerOutputComponentCount = 0; other.fragmentInputComponentCount = 0; + other.fragmentReplacesDepth = false; } VkProgramObject& operator=(VkProgramObject&& other) noexcept { if (this == &other) { @@ -153,6 +159,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { rasterizationProducerStage = other.rasterizationProducerStage; producerOutputComponentCount = other.producerOutputComponentCount; fragmentInputComponentCount = other.fragmentInputComponentCount; + fragmentReplacesDepth = other.fragmentReplacesDepth; other.hash = 0; other.descriptorSetLayout = VK_NULL_HANDLE; other.pipelineLayout = VK_NULL_HANDLE; @@ -163,6 +170,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { other.rasterizationProducerStage = ShaderStage::Unknown; other.producerOutputComponentCount = 0; other.fragmentInputComponentCount = 0; + other.fragmentReplacesDepth = false; return *this; } @@ -210,6 +218,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { static VkShaderStageFlagBits ToVkStage(ShaderStage stage); static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format); static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType); + // True when any entry point declares the DepthReplacing execution mode, i.e. the + // shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption + // can be pinned by tests. A false negative loses the exemption, so such a shader is + // stripped conservatively and forfeits its depth write. + static Bool ReflectedFragmentReplacesDepth(const SpvReflectShaderModule& reflectModule); private: struct ProgramLookupCache { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index ceca31c3..8aa0e029 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2112,18 +2112,16 @@ void main() { // the pipelines of a multi-pass depth-equality chain (even with the SPIR-V // Invariant decoration), so a blended depth-writing prepass makes later // equality-compare passes drop whole primitives (MC 26.3 improved-transparency - // clouds flicker black). Suppress blended depth writes there; + // clouds flicker black). Suppress depth writes on accumulation-blended pipelines + // there (see PipelineFactory::ShouldSuppressDepthWrite for the exact scope); // MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE forces the quirk on or off on any // driver. - static constexpr Uint32 kVendorIdQualcomm = 0x5143; const MG_Config::QuirkOverride quirkOverride = MG_Config::Features.MagmaDisableBlendedDepthWriteQuirk; - const Bool suppressBlendedDepthWrite = - quirkOverride == MG_Config::QuirkOverride::ForceOn || - (quirkOverride == MG_Config::QuirkOverride::Auto && - m_physicalDevice.properties.vendorID == kVendorIdQualcomm); + const Bool suppressBlendedDepthWrite = PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice( + quirkOverride, m_physicalDevice.properties.vendorID); if (suppressBlendedDepthWrite) { - MGLOG_I("DirectVulkan: suppressing depth writes on blended pipelines " + MGLOG_I("DirectVulkan: suppressing depth writes on accumulation-blended pipelines " "(driver lacks cross-pipeline position invariance)%s", quirkOverride == MG_Config::QuirkOverride::ForceOn ? " (forced on)" : ""); } @@ -3466,6 +3464,7 @@ void main() { .backStencilPassOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthPassOp), .backStencilDepthFailOp = MG_Util::ConvertStencilOperationToVkEnum(backStencil.PassDepthFailOp), .backStencilCompareOp = MG_Util::ConvertDepthTestFuncToVkEnum(backStencil.Func), + .fragmentReplacesDepth = programObj.fragmentReplacesDepth, .stages = &programObj.stages, .vertexInputState = pipelineVertexInputState }; @@ -3670,6 +3669,16 @@ void main() { "disabling blending on attachments with this format (first hit: attachment %u textureId=%d program=%u)", static_cast(colorAttachmentFormat), i, textureExternalIndex, program.GetExternalIndex()); + if (PipelineFactory::IsSuppressBlendedDepthWriteEnabled()) { + // With blending force-disabled the blended depth-write quirk can + // never fire for pipelines on this format, so a depth-equality + // chain that accumulates into it (MC 26.3 OIT depth_bounds on + // RGBA32F) keeps its depth writes and may flicker on this driver. + MGLOG_W("GetOrCreatePipeline: format=%d is not blendable, so the blended " + "depth-write quirk cannot apply to it; depth-equality chains " + "accumulating into this format may flicker", + static_cast(colorAttachmentFormat)); + } } } if (!blendSupportIt->second) { diff --git a/MobileGL/MG_Test/CMakeLists.txt b/MobileGL/MG_Test/CMakeLists.txt index f7b98c9e..041f4eea 100644 --- a/MobileGL/MG_Test/CMakeLists.txt +++ b/MobileGL/MG_Test/CMakeLists.txt @@ -72,6 +72,7 @@ add_subdirectory(Texture) add_subdirectory(VertexArray) add_subdirectory(Program) add_subdirectory(Query) +add_subdirectory(Pipeline) if (ENABLE_INTEGRATION_TESTS) add_subdirectory(Backend/DirectVulkan) endif() diff --git a/MobileGL/MG_Test/Pipeline/CMakeLists.txt b/MobileGL/MG_Test/Pipeline/CMakeLists.txt new file mode 100644 index 00000000..f3fe1ff9 --- /dev/null +++ b/MobileGL/MG_Test/Pipeline/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.14) + +add_executable( + PipelineQuirkTest + PipelineQuirkTest.cpp +) + +target_include_directories(PipelineQuirkTest PRIVATE + ${MGL_ROOT}/include + ${MGL_ROOT}/MobileGL + ${MGL_ROOT}/3rdparty/xxHash + ${MGL_ROOT}/3rdparty/Vulkan-Headers/include + ${MGL_ROOT}/3rdparty/SPIRV-Reflect +) + +target_link_libraries( + PipelineQuirkTest PRIVATE + GTest::gtest_main + ${LINK_LIBRARIES} +) + +if (MSVC) + target_compile_options(PipelineQuirkTest PRIVATE /Zc:preprocessor) +endif() + +include(GoogleTest) +gtest_discover_tests(PipelineQuirkTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit) diff --git a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp new file mode 100644 index 00000000..750af2a1 --- /dev/null +++ b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp @@ -0,0 +1,341 @@ +// MobileGL - MobileGL/MG_Test/Pipeline/PipelineQuirkTest.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 + +#include +#include +#include + +using namespace MobileGL; +using MobileGL::MG_Backend::DirectVulkan::PipelineFactory; +using MobileGL::MG_Backend::DirectVulkan::ProgramFactory; +using MobileGL::MG_Config::QuirkOverride; + +namespace { + constexpr Uint32 kVendorIdQualcomm = 0x5143; + constexpr Uint32 kVendorIdArm = 0x13B5; + + constexpr VkColorComponentFlags kFullColorWriteMask = + VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + + // Builds non-separate blend state: the alpha channel repeats the color factors/op, which + // is what glBlendFunc/glBlendEquation (as opposed to their *Separate forms) produce. + // ShouldSuppressDepthWrite deliberately decides on the color channel alone, so these + // cases cover its whole input space; SeparateAlphaAccumulationIsNotStripped below pins + // the separate-alpha contract. + VkPipelineColorBlendAttachmentState MakeBlendAttachment(Bool blendEnable, + VkBlendFactor srcColor, + VkBlendFactor dstColor, + VkBlendOp colorOp, + VkColorComponentFlags colorWriteMask) { + VkPipelineColorBlendAttachmentState attachment{}; + attachment.blendEnable = blendEnable ? VK_TRUE : VK_FALSE; + attachment.srcColorBlendFactor = srcColor; + attachment.dstColorBlendFactor = dstColor; + attachment.colorBlendOp = colorOp; + attachment.srcAlphaBlendFactor = srcColor; + attachment.dstAlphaBlendFactor = dstColor; + attachment.alphaBlendOp = colorOp; + attachment.colorWriteMask = colorWriteMask; + return attachment; + } + + // glslangValidator -V output for: + // #version 450 + // layout(location = 0) out vec4 outColor; + // void main() { outColor = vec4(1.0); gl_FragDepth = 0.5; } + // Assigning gl_FragDepth makes glslang emit OpExecutionMode ... DepthReplacing. + constexpr Uint32 kFragDepthWriterSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x0000000fu, 0x00000000u, 0x00020011u, + 0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, + 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0007000fu, 0x00000004u, + 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000009u, 0x0000000du, 0x00030010u, + 0x00000004u, 0x00000007u, 0x00030010u, 0x00000004u, 0x0000000cu, 0x00030003u, + 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, 0x6e69616du, 0x00000000u, + 0x00050005u, 0x00000009u, 0x4374756fu, 0x726f6c6fu, 0x00000000u, 0x00060005u, + 0x0000000du, 0x465f6c67u, 0x44676172u, 0x68747065u, 0x00000000u, 0x00040047u, + 0x00000009u, 0x0000001eu, 0x00000000u, 0x00040047u, 0x0000000du, 0x0000000bu, + 0x00000016u, 0x00020013u, 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, + 0x00030016u, 0x00000006u, 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, + 0x00000004u, 0x00040020u, 0x00000008u, 0x00000003u, 0x00000007u, 0x0004003bu, + 0x00000008u, 0x00000009u, 0x00000003u, 0x0004002bu, 0x00000006u, 0x0000000au, + 0x3f800000u, 0x0007002cu, 0x00000007u, 0x0000000bu, 0x0000000au, 0x0000000au, + 0x0000000au, 0x0000000au, 0x00040020u, 0x0000000cu, 0x00000003u, 0x00000006u, + 0x0004003bu, 0x0000000cu, 0x0000000du, 0x00000003u, 0x0004002bu, 0x00000006u, + 0x0000000eu, 0x3f000000u, 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, + 0x00000003u, 0x000200f8u, 0x00000005u, 0x0003003eu, 0x00000009u, 0x0000000bu, + 0x0003003eu, 0x0000000du, 0x0000000eu, 0x000100fdu, 0x00010038u, + }; + + // Same shader without the gl_FragDepth assignment. + constexpr Uint32 kPlainFragmentSpirv[] = { + 0x07230203u, 0x00010000u, 0x0008000bu, 0x0000000cu, 0x00000000u, 0x00020011u, + 0x00000001u, 0x0006000bu, 0x00000001u, 0x4c534c47u, 0x6474732eu, 0x3035342eu, + 0x00000000u, 0x0003000eu, 0x00000000u, 0x00000001u, 0x0006000fu, 0x00000004u, + 0x00000004u, 0x6e69616du, 0x00000000u, 0x00000009u, 0x00030010u, 0x00000004u, + 0x00000007u, 0x00030003u, 0x00000002u, 0x000001c2u, 0x00040005u, 0x00000004u, + 0x6e69616du, 0x00000000u, 0x00050005u, 0x00000009u, 0x4374756fu, 0x726f6c6fu, + 0x00000000u, 0x00040047u, 0x00000009u, 0x0000001eu, 0x00000000u, 0x00020013u, + 0x00000002u, 0x00030021u, 0x00000003u, 0x00000002u, 0x00030016u, 0x00000006u, + 0x00000020u, 0x00040017u, 0x00000007u, 0x00000006u, 0x00000004u, 0x00040020u, + 0x00000008u, 0x00000003u, 0x00000007u, 0x0004003bu, 0x00000008u, 0x00000009u, + 0x00000003u, 0x0004002bu, 0x00000006u, 0x0000000au, 0x3f800000u, 0x0007002cu, + 0x00000007u, 0x0000000bu, 0x0000000au, 0x0000000au, 0x0000000au, 0x0000000au, + 0x00050036u, 0x00000002u, 0x00000004u, 0x00000000u, 0x00000003u, 0x000200f8u, + 0x00000005u, 0x0003003eu, 0x00000009u, 0x0000000bu, 0x000100fdu, 0x00010038u, + }; + + // Owns the reflection module so each test case cleans up after itself. + class ReflectModule { + public: + template + explicit ReflectModule(const Uint32 (&spirv)[WordCount]) { + m_created = spvReflectCreateShaderModule(sizeof(spirv), spirv, &m_module) == + SPV_REFLECT_RESULT_SUCCESS; + } + ~ReflectModule() { + if (m_created) { + spvReflectDestroyShaderModule(&m_module); + } + } + ReflectModule(const ReflectModule&) = delete; + ReflectModule& operator=(const ReflectModule&) = delete; + + Bool Created() const { return m_created; } + const SpvReflectShaderModule& Get() const { return m_module; } + + private: + SpvReflectShaderModule m_module{}; + Bool m_created = false; + }; + + PipelineFactory::PipelineCreatePayload MakeDepthWritingPayload( + const VkPipelineColorBlendAttachmentState& attachment0) { + PipelineFactory::PipelineCreatePayload payload{}; + payload.colorAttachmentCount = 1; + payload.depthTestEnable = true; + payload.depthWriteEnable = true; + payload.colorBlendAttachments[0] = attachment0; + return payload; + } +} // namespace + +// --- Device gate: MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE tri-state --- + +TEST(PipelineQuirkDeviceGate, ForceOnEnablesOnAnyVendor) { + EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn, + kVendorIdArm)); + EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn, + kVendorIdQualcomm)); +} + +TEST(PipelineQuirkDeviceGate, ForceOffDisablesEvenOnQualcomm) { + EXPECT_FALSE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOff, + kVendorIdQualcomm)); +} + +TEST(PipelineQuirkDeviceGate, AutoDetectsQualcommOnly) { + EXPECT_TRUE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::Auto, + kVendorIdQualcomm)); + EXPECT_FALSE(PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::Auto, + kVendorIdArm)); +} + +TEST(PipelineQuirkDeviceGate, ForceOnRoundTripsThroughTheFactoryFlag) { + const Bool previous = PipelineFactory::IsSuppressBlendedDepthWriteEnabled(); + PipelineFactory::SetSuppressBlendedDepthWrite( + PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(QuirkOverride::ForceOn, kVendorIdArm)); + EXPECT_TRUE(PipelineFactory::IsSuppressBlendedDepthWriteEnabled()); + PipelineFactory::SetSuppressBlendedDepthWrite(previous); +} + +// --- Per-pipeline strip decision against the pipeline create-info payload --- + +TEST(PipelineQuirkStripDecision, MaxBlendIsStripped) { + // MC 26.3 OIT depth_bounds: GL_MAX accumulation writing depth - the case the quirk fixes. + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_MAX, kFullColorWriteMask)); + EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, MinBlendIsStripped) { + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_MIN, kFullColorWriteMask)); + EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsStripped) { + // MC 26.3 OIT transmittance/accumulate: ONE+ONE additive accumulation writing depth. + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask)); + EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, SortedTransparencyOverBlendIsNotStripped) { + // Vanilla MC translucent layer (water, stained glass): SRC_ALPHA "over" compositing + // draws each surface once and depends on its depth writes to occlude particles, rain, + // and clouds drawn later - it must keep them. + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, + kFullColorWriteMask)); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, EffectivelyOpaqueBlendIsNotStripped) { + // GL_BLEND left enabled with ONE/ZERO+ADD factors is opaque in effect; stripping its + // depth write would break occlusion for plainly opaque geometry. + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask)); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, FullyMaskedAccumulationBlendIsNotStripped) { + // Depth-prepass pattern: colorMask(0,0,0,0) with blending left enabled - blending is + // moot, and stripping would delete the entire prepass. + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 0)); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, DisabledBlendIsNotStripped) { + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask)); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, NoDepthWriteMeansNoStrip) { + auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask)); + payload.depthWriteEnable = false; + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, FragDepthWriterIsExempt) { + // gl_FragDepth output does not go through per-pipeline vertex position math, so the + // cross-pipeline invariance hazard cannot affect it (e.g. the 26.3 OIT composite). + auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask)); + payload.fragmentReplacesDepth = true; + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) { + // The hazard is not limited to attachment 0: the 26.3 transmittance pass accumulates + // into a 2-target MRT. + PipelineFactory::PipelineCreatePayload payload{}; + payload.colorAttachmentCount = 2; + payload.depthTestEnable = true; + payload.depthWriteEnable = true; + payload.colorBlendAttachments[0] = MakeBlendAttachment( + false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask); + payload.colorBlendAttachments[1] = MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask); + EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, AlphaWeightedAdditiveIsNotStripped) { + // SRC_ALPHA,ONE additive is order-independent in the color channel but is the classic + // *sorted* particle/glow blend, not an OIT accumulation pass. Pins the src==ONE clause: + // without it this state would be stripped. + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask)); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) { + // Deliberate narrowing: only MIN/MAX and ONE+ONE ADD carry the equality-chain + // signature. SUBTRACT-class ops stay outside the quirk until content demands them. + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_REVERSE_SUBTRACT, + kFullColorWriteMask)); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, PartiallyMaskedAccumulationIsStripped) { + // Only a fully masked attachment is exempt; a live alpha channel still accumulates. + const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_COLOR_COMPONENT_A_BIT)); + EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, NoColorAttachmentsMeansNoStrip) { + // Depth-only FBO: the loop must not read the (stale) attachment array at all. + PipelineFactory::PipelineCreatePayload payload{}; + payload.colorAttachmentCount = 0; + payload.depthTestEnable = true; + payload.depthWriteEnable = true; + payload.colorBlendAttachments[0] = MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +TEST(PipelineQuirkStripDecision, SeparateAlphaAccumulationIsNotStripped) { + // glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX) over an ordinary color over-blend: the + // alpha channel accumulates but the color channel does not. Pins that the decision is + // color-channel only - widening it to alpha would re-capture sorted transparency. + auto attachment = MakeBlendAttachment(true, VK_BLEND_FACTOR_SRC_ALPHA, + VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, + kFullColorWriteMask); + attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + attachment.alphaBlendOp = VK_BLEND_OP_MAX; + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(MakeDepthWritingPayload(attachment))); +} + +TEST(PipelineQuirkStripDecision, MixedOverAndMaskedAttachmentsAreNotStripped) { + PipelineFactory::PipelineCreatePayload payload{}; + payload.colorAttachmentCount = 2; + payload.depthTestEnable = true; + payload.depthWriteEnable = true; + payload.colorBlendAttachments[0] = MakeBlendAttachment( + true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, + kFullColorWriteMask); + payload.colorBlendAttachments[1] = MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, 0); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +} + +// --- DepthReplacing reflection feeding the gl_FragDepth exemption --- + +TEST(ReflectedFragmentReplacesDepth, TrueForAShaderThatAssignsFragDepth) { + const ReflectModule module(kFragDepthWriterSpirv); + ASSERT_TRUE(module.Created()); + EXPECT_TRUE(ProgramFactory::ReflectedFragmentReplacesDepth(module.Get())); +} + +TEST(ReflectedFragmentReplacesDepth, FalseForAPlainFragmentShader) { + const ReflectModule module(kPlainFragmentSpirv); + ASSERT_TRUE(module.Created()); + EXPECT_FALSE(ProgramFactory::ReflectedFragmentReplacesDepth(module.Get())); +} + +TEST(ReflectedFragmentReplacesDepth, FalseForAnEmptyModule) { + // A default-constructed module has no entry points; the scan must not dereference. + SpvReflectShaderModule emptyModule{}; + EXPECT_FALSE(ProgramFactory::ReflectedFragmentReplacesDepth(emptyModule)); +} + +TEST(ReflectedFragmentReplacesDepth, ReflectedFlagFlipsTheStripDecision) { + // The two fixtures differ only by the gl_FragDepth assignment, so they pin that the + // reflected flag is what flips the strip decision for an otherwise identical pipeline. + const ReflectModule depthWriter(kFragDepthWriterSpirv); + const ReflectModule plain(kPlainFragmentSpirv); + ASSERT_TRUE(depthWriter.Created()); + ASSERT_TRUE(plain.Created()); + + auto payload = MakeDepthWritingPayload(MakeBlendAttachment( + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask)); + + payload.fragmentReplacesDepth = ProgramFactory::ReflectedFragmentReplacesDepth(plain.Get()); + EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); + + payload.fragmentReplacesDepth = ProgramFactory::ReflectedFragmentReplacesDepth(depthWriter.Get()); + EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); +}