[Fix] (DirectVulkan): shrink the blended depth-write quirk to MIN/MAX extremum blends only - a fixture-wide trace sweep showed the additive ONE+ONE arm never fires on the 26.3 OIT chain (its accumulation passes disable depth writes themselves) and only hit unrelated additive glow content; also fail reflection toward the gl_FragDepth exemption and zero phantom default-FBO blend slots so stale indexed state cannot trigger the strip

This commit is contained in:
2026-07-20 23:39:28 -04:00
parent bce9c48c8e
commit 202037b5a3
5 changed files with 53 additions and 35 deletions
@@ -131,25 +131,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} }
namespace { namespace {
// Order-independent accumulation blending: the write order of overlapping fragments // MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
// does not change the result, which is what lets multi-pass chains re-rasterize the // (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
// same geometry and combine per-pass contributions (MC 26.3 OIT: GL_MAX depth // depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
// 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 // Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
// (glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX)) whose color channel is an ordinary // content as possible, and a trace sweep of every fixture showed the wider
// over-blend is not treated as hazardous: no known content pairs that shape with a // alternatives all cost more than they fix:
// depth-equality chain, and widening the test would re-capture sorted transparency. // - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
// (its transmittance/accumulate passes disable depth writes themselves) - the
// only real content it caught was harmless additive glow effects (Create);
// - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
// drawn once per surface, and rely on their depth writes for occlusion;
// - separate-alpha accumulation over an over-blending color channel has no
// known pairing with a depth-equality chain (color channel only, see tests).
// If a future workload pairs another blend shape with an equality chain, widen
// this with that evidence in hand rather than pre-emptively.
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) { Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
if (attachment.colorBlendOp == VK_BLEND_OP_MIN || attachment.colorBlendOp == VK_BLEND_OP_MAX) { return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
return true; attachment.colorBlendOp == VK_BLEND_OP_MAX;
}
return attachment.colorBlendOp == VK_BLEND_OP_ADD &&
attachment.srcColorBlendFactor == VK_BLEND_FACTOR_ONE &&
attachment.dstColorBlendFactor == VK_BLEND_FACTOR_ONE;
} }
} // namespace } // namespace
@@ -70,11 +70,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// with an equality-inclusive compare on the re-rasterized geometry) requires // with an equality-inclusive compare on the re-rasterized geometry) requires
// cross-pipeline position invariance that some mobile compilers do not provide, even // 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 // 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 // passes. Only MIN/MAX extremum blends are stripped - the signature of such a
// stripped - that is the signature of such equality chains (MC 26.3 OIT) - while // chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
// sorted-transparency "over" compositing (e.g. vanilla MC water, SRC_ALPHA factors), // only depth-writing shape the chain actually uses - so every other blend
// which draws each surface once and depends on its depth writes to occlude later // (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
// passes, keeps them. Set at renderer initialization based on the active driver. // its depth writes. Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled); static void SetSuppressBlendedDepthWrite(Bool enabled);
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; } static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on // Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
@@ -1547,6 +1547,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)", "ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)",
static_cast<Int>(createResult)); static_cast<Int>(createResult));
if (createResult != SPV_REFLECT_RESULT_SUCCESS) { if (createResult != SPV_REFLECT_RESULT_SUCCESS) {
// Fail toward the exemption: stripping a genuine gl_FragDepth writer would
// corrupt its depth output outright, while wrongly exempting an accumulation
// pass merely reverts that one program to the pre-quirk behavior.
entry.fragmentReplacesDepth = true;
continue; continue;
} }
@@ -3552,6 +3552,17 @@ void main() {
(bufferMask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u)); (bufferMask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u));
Bool effectiveBlendEnabled = blendEnabled; Bool effectiveBlendEnabled = blendEnabled;
MG_State::GLState::ITextureObject* colorAttachmentTexture = nullptr; MG_State::GLState::ITextureObject* colorAttachmentTexture = nullptr;
if (isDefaultDrawFbo && i < drawBuffers.size() &&
drawBuffers[i] == FramebufferAttachmentType::None) {
// The default framebuffer spans the same MAX_DRAW_BUFFERS slots as an FBO
// (slot 0 is the back buffer, or None after glDrawBuffer(GL_NONE); slots
// 1+ are always None). Discard writes and blend state for the None slots
// like the FBO path below does, so stale indexed blend state on phantom
// slots cannot leak into the pipeline - most notably into the blended
// depth-write quirk's accumulation scan.
attachmentColorWriteMask = 0;
effectiveBlendEnabled = false;
}
if (!isDefaultDrawFbo && i < drawBuffers.size()) { if (!isDefaultDrawFbo && i < drawBuffers.size()) {
const auto drawBuffer = drawBuffers[i]; const auto drawBuffer = drawBuffers[i];
colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i); colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i);
+16 -13
View File
@@ -258,11 +258,14 @@ TEST(PipelineQuirkStripDecision, MinBlendIsStripped) {
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
} }
TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsStripped) { TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsNotStripped) {
// MC 26.3 OIT transmittance/accumulate: ONE+ONE additive accumulation writing depth. // ONE+ONE additive with a depth write matched zero draws of the 26.3 chain in the
// fixture sweep (transmittance/accumulate disable depth writes themselves); the only
// real content with this shape was harmless additive glow effects (Create). A quirk
// touches as little unrelated content as possible, so the shape stays exempt.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask)); true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
} }
TEST(PipelineQuirkStripDecision, SortedTransparencyOverBlendIsNotStripped) { TEST(PipelineQuirkStripDecision, SortedTransparencyOverBlendIsNotStripped) {
@@ -285,9 +288,10 @@ TEST(PipelineQuirkStripDecision, EffectivelyOpaqueBlendIsNotStripped) {
TEST(PipelineQuirkStripDecision, FullyMaskedAccumulationBlendIsNotStripped) { TEST(PipelineQuirkStripDecision, FullyMaskedAccumulationBlendIsNotStripped) {
// Depth-prepass pattern: colorMask(0,0,0,0) with blending left enabled - blending is // Depth-prepass pattern: colorMask(0,0,0,0) with blending left enabled - blending is
// moot, and stripping would delete the entire prepass. // moot, and stripping would delete the entire prepass. MAX so the exemption, not the
// blend-op filter, is what keeps the depth write.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 0)); true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, 0));
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
} }
@@ -314,8 +318,8 @@ TEST(PipelineQuirkStripDecision, FragDepthWriterIsExempt) {
} }
TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) { TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) {
// The hazard is not limited to attachment 0: the 26.3 transmittance pass accumulates // The scan is not limited to attachment 0: an extremum accumulation on any live
// into a 2-target MRT. // attachment marks the pipeline.
PipelineFactory::PipelineCreatePayload payload{}; PipelineFactory::PipelineCreatePayload payload{};
payload.colorAttachmentCount = 2; payload.colorAttachmentCount = 2;
payload.depthTestEnable = true; payload.depthTestEnable = true;
@@ -323,21 +327,20 @@ TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) {
payload.colorBlendAttachments[0] = MakeBlendAttachment( payload.colorBlendAttachments[0] = MakeBlendAttachment(
false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask); false, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ZERO, VK_BLEND_OP_ADD, kFullColorWriteMask);
payload.colorBlendAttachments[1] = MakeBlendAttachment( payload.colorBlendAttachments[1] = MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask); true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, kFullColorWriteMask);
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
} }
TEST(PipelineQuirkStripDecision, AlphaWeightedAdditiveIsNotStripped) { TEST(PipelineQuirkStripDecision, AlphaWeightedAdditiveIsNotStripped) {
// SRC_ALPHA,ONE additive is order-independent in the color channel but is the classic // SRC_ALPHA,ONE additive: the classic *sorted* particle/glow blend. Kept exempt like
// *sorted* particle/glow blend, not an OIT accumulation pass. Pins the src==ONE clause: // every other ADD-op shape now that the strip is extremum-only.
// without it this state would be stripped.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask)); true, VK_BLEND_FACTOR_SRC_ALPHA, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, kFullColorWriteMask));
EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload)); EXPECT_FALSE(PipelineFactory::ShouldSuppressDepthWrite(payload));
} }
TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) { TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) {
// Deliberate narrowing: only MIN/MAX and ONE+ONE ADD carry the equality-chain // Deliberate narrowing: only the MIN/MAX extremum ops carry the depth-bounds
// signature. SUBTRACT-class ops stay outside the quirk until content demands them. // signature. SUBTRACT-class ops stay outside the quirk until content demands them.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_REVERSE_SUBTRACT, true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_REVERSE_SUBTRACT,
@@ -348,7 +351,7 @@ TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) {
TEST(PipelineQuirkStripDecision, PartiallyMaskedAccumulationIsStripped) { TEST(PipelineQuirkStripDecision, PartiallyMaskedAccumulationIsStripped) {
// Only a fully masked attachment is exempt; a live alpha channel still accumulates. // Only a fully masked attachment is exempt; a live alpha channel still accumulates.
const auto payload = MakeDepthWritingPayload(MakeBlendAttachment( const auto payload = MakeDepthWritingPayload(MakeBlendAttachment(
true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_COLOR_COMPONENT_A_BIT)); true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, VK_COLOR_COMPONENT_A_BIT));
EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload));
} }