mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-11 05:38:31 +09:00
[Fix] (DirectVulkan): narrow the blended depth-write quirk to order-independent accumulation blends, exempting sorted-transparency, gl_FragDepth writers and fully masked attachments
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Int>(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<Int>(colorAttachmentFormat));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!blendSupportIt->second) {
|
||||
|
||||
Reference in New Issue
Block a user