[Fix] (DirectVulkan): suppress blended depth writes on Qualcomm and mark gl_Position invariant to fix MC 26.3 OIT cloud flicker

This commit is contained in:
2026-07-19 05:47:23 -04:00
parent fc0688c223
commit f3def150e7
6 changed files with 145 additions and 0 deletions
@@ -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;
@@ -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<HashType, VkPipeline> m_cache;
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -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<Uint> 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
@@ -25,6 +25,9 @@
#include <cstdlib>
#include <cstring>
#include <vulkan/vulkan_core.h>
#ifdef __ANDROID__
#include <sys/system_properties.h>
#endif
#if defined(__APPLE__)
#include <CoreGraphics/CoreGraphics.h>
@@ -947,6 +950,7 @@ void main() {
}
)";
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
Int maxDimension = std::max<Int>(
baseTexelSize.x(),
@@ -1884,6 +1888,28 @@ void main() {
m_pipelineFactory = MakeUnique<PipelineFactory>(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<ProgramFactory>(m_device, m_config, maxProgramBindings,
m_shaderDrawParametersFeatureEnabled);
MOBILEGL_ASSERT(m_programFactory != nullptr, "ProgramFactory creation failed.");
@@ -21,6 +21,7 @@
#include "ShaderSourceProcessor.h"
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToGlslang/ProgramEnumConverter.h>
#include <cstdlib>
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<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32> invariantIds;
Vector<MemberKey> 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<String> ShaderCompiler::DecompileShader(SpvcSession& session) {
spvc_compiler_options options;
session.CreateOptions(&options);
@@ -39,6 +39,13 @@ namespace MobileGL {
// which wrongly includes baseInstance).
static bool RebaseInstanceIndexForVulkan(const Vector<Uint32>& inputBinary,
Vector<uint32_t>& 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<Uint32>& inputBinary,
Vector<uint32_t>& outputBinary);
static Result<String> DecompileShader(SpvcSession& session);
};
} // namespace ShaderTranspiler