From f3def150e7f74e6da4ccada8293a9de72bc34102 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 19 Jul 2026 05:45:54 -0400 Subject: [PATCH] [Fix] (DirectVulkan): suppress blended depth writes on Qualcomm and mark gl_Position invariant to fix MC 26.3 OIT cloud flicker --- .../DirectVulkan/Renderer/PipelineFactory.cpp | 17 +++++ .../DirectVulkan/Renderer/PipelineFactory.h | 9 +++ .../DirectVulkan/Renderer/ProgramFactory.cpp | 16 +++++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 26 +++++++ .../ShaderTranspiler/ShaderCompiler.cpp | 70 +++++++++++++++++++ .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 7 ++ 6 files changed, 145 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index 75df6ee4..235a3677 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -8,6 +8,7 @@ #include "PipelineFactory.h" + namespace MobileGL::MG_Backend::DirectVulkan { static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) { switch (topology) { @@ -108,6 +109,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { "vkCreatePipelineCache"); } + void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) { + s_suppressBlendedDepthWrite = enabled; + } + PipelineFactory::~PipelineFactory() { DestroyAll(); if (m_pipelineCache != VK_NULL_HANDLE) { @@ -258,6 +263,18 @@ 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; + } + } + } VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE; blend.logicOp = payload.logicOp; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index cec5ec07..7b16fff4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -62,6 +62,14 @@ 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. + static void SetSuppressBlendedDepthWrite(Bool enabled); + private: VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const; @@ -70,5 +78,6 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipelineCache m_pipelineCache = VK_NULL_HANDLE; UnorderedMap m_cache; static inline XXH64_state_t* m_hashState = XXH64_createState(); + static inline Bool s_suppressBlendedDepthWrite = false; }; } // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index e4348e64..cef9b576 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -1697,6 +1697,22 @@ namespace MobileGL::MG_Backend::DirectVulkan { moduleSpirvs[i] = spv; } + // GL apps depend on cross-program position invariance for multi-pass equality + // depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the + // depth its own first pass wrote); decorate Position outputs Invariant so + // per-pipeline compilers cannot vary the position math between passes. + { + Vector invariantSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::DecoratePositionInvariantForVulkan( + moduleSpirvs[i], invariantSpirv)) { + moduleSpirvs[i] = std::move(invariantSpirv); + } else { + MGLOG_W("ProgramFactory: position-invariant decoration failed for program %u; " + "keeping the original module", + program.GetExternalIndex()); + } + } + // glslang's relaxed-Vulkan mode aliases GL's zero-based gl_InstanceID to Vulkan's // gl_InstanceIndex, which wrongly includes the draw's baseInstance. Rebase vertex-stage // loads to (InstanceIndex - BaseInstance) so shaders observe GL semantics. Reflection diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 976c3a68..e34fbc38 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -25,6 +25,9 @@ #include #include #include +#ifdef __ANDROID__ +#include +#endif #if defined(__APPLE__) #include @@ -947,6 +950,7 @@ void main() { } )"; + static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) { Int maxDimension = std::max( baseTexelSize.x(), @@ -1884,6 +1888,28 @@ void main() { m_pipelineFactory = MakeUnique(m_device, m_config); MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory creation failed."); + { + // Qualcomm's pipeline compiler does not keep vertex positions invariant across + // 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; the env variable + // forces the quirk on ("0") or off ("1") on any driver. + static constexpr Uint32 kVendorIdQualcomm = 0x5143; + Bool suppressBlendedDepthWrite = m_physicalDevice.properties.vendorID == kVendorIdQualcomm; + if (const char* env = getenv("MOBILEGL_MAGMA_BLENDED_DEPTH_WRITE")) { + if (env[0] == '0') { + suppressBlendedDepthWrite = true; + } else if (env[0] == '1') { + suppressBlendedDepthWrite = false; + } + } + if (suppressBlendedDepthWrite) { + MGLOG_I("DirectVulkan: suppressing depth writes on blended pipelines " + "(driver lacks cross-pipeline position invariance)"); + } + PipelineFactory::SetSuppressBlendedDepthWrite(suppressBlendedDepthWrite); + } m_programFactory = MakeUnique(m_device, m_config, maxProgramBindings, m_shaderDrawParametersFeatureEnabled); MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed."); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index bd60797b..fe9fa2c0 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -21,6 +21,7 @@ #include "ShaderSourceProcessor.h" #include #include +#include namespace MobileGL { namespace MG_Util { @@ -322,6 +323,75 @@ namespace MobileGL { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } + bool ShaderCompiler::DecoratePositionInvariantForVulkan(const Vector& inputBinary, + Vector& outputBinary) { + static constexpr Uint32 kHeaderWords = 5; + static constexpr Uint32 kOpDecorate = 71; + static constexpr Uint32 kOpMemberDecorate = 72; + static constexpr Uint32 kDecorationInvariant = 18; + static constexpr Uint32 kDecorationBuiltIn = 11; + static constexpr Uint32 kBuiltInPosition = 0; + if (inputBinary.size() < kHeaderWords) { + return false; + } + + // First pass: find targets that already carry Invariant so we never duplicate. + struct MemberKey { + Uint32 id; + Uint32 member; + bool operator==(const MemberKey& o) const { return id == o.id && member == o.member; } + }; + Vector invariantIds; + Vector invariantMembers; + for (SizeT i = kHeaderWords; i < inputBinary.size();) { + const Uint32 word0 = inputBinary[i]; + const Uint32 opcode = word0 & 0xFFFFu; + const Uint32 length = word0 >> 16; + if (length == 0 || i + length > inputBinary.size()) { + return false; + } + if (opcode == kOpDecorate && length >= 3 && inputBinary[i + 2] == kDecorationInvariant) { + invariantIds.push_back(inputBinary[i + 1]); + } else if (opcode == kOpMemberDecorate && length >= 4 && + inputBinary[i + 3] == kDecorationInvariant) { + invariantMembers.push_back({inputBinary[i + 1], inputBinary[i + 2]}); + } + i += length; + } + + outputBinary.clear(); + outputBinary.reserve(inputBinary.size() + 8); + outputBinary.insert(outputBinary.end(), inputBinary.begin(), inputBinary.begin() + kHeaderWords); + for (SizeT i = kHeaderWords; i < inputBinary.size();) { + const Uint32 word0 = inputBinary[i]; + const Uint32 opcode = word0 & 0xFFFFu; + const Uint32 length = word0 >> 16; + outputBinary.insert(outputBinary.end(), inputBinary.begin() + i, + inputBinary.begin() + i + length); + if (opcode == kOpDecorate && length == 4 && + inputBinary[i + 2] == kDecorationBuiltIn && inputBinary[i + 3] == kBuiltInPosition) { + const Uint32 target = inputBinary[i + 1]; + if (std::find(invariantIds.begin(), invariantIds.end(), target) == invariantIds.end()) { + outputBinary.push_back((3u << 16) | kOpDecorate); + outputBinary.push_back(target); + outputBinary.push_back(kDecorationInvariant); + } + } else if (opcode == kOpMemberDecorate && length == 5 && + inputBinary[i + 3] == kDecorationBuiltIn && inputBinary[i + 4] == kBuiltInPosition) { + const MemberKey key{inputBinary[i + 1], inputBinary[i + 2]}; + if (std::find(invariantMembers.begin(), invariantMembers.end(), key) == + invariantMembers.end()) { + outputBinary.push_back((4u << 16) | kOpMemberDecorate); + outputBinary.push_back(key.id); + outputBinary.push_back(key.member); + outputBinary.push_back(kDecorationInvariant); + } + } + i += length; + } + return true; + } + Result ShaderCompiler::DecompileShader(SpvcSession& session) { spvc_compiler_options options; session.CreateOptions(&options); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index 6fb1abbf..d9dc1542 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -39,6 +39,13 @@ namespace MobileGL { // which wrongly includes baseInstance). static bool RebaseInstanceIndexForVulkan(const Vector& inputBinary, Vector& outputBinary); + // Adds the Invariant decoration to every Position builtin output. GL apps + // routinely rely on cross-program position invariance for multi-pass + // equality depth tests (e.g. GEQUAL re-draws of the same geometry), and + // mobile drivers that optimize per-pipeline break that without the + // decoration. DirectVulkan only. + static bool DecoratePositionInvariantForVulkan(const Vector& inputBinary, + Vector& outputBinary); static Result DecompileShader(SpvcSession& session); }; } // namespace ShaderTranspiler