diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp index c5aa3566..83da4946 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -131,25 +131,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { } 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. + // MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass + // (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing + // depth for its equality chain). MIN/MAX ignore blend factors 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. + // Deliberately the ONLY shape stripped. A quirk should touch as little unrelated + // content as possible, and a trace sweep of every fixture showed the wider + // alternatives all cost more than they fix: + // - 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) { - 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; + return attachment.colorBlendOp == VK_BLEND_OP_MIN || + attachment.colorBlendOp == VK_BLEND_OP_MAX; } } // namespace diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h index 5da32176..9f101d7f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -70,11 +70,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { // 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. + // passes. Only MIN/MAX extremum blends are stripped - the signature of such a + // chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the + // only depth-writing shape the chain actually uses - so every other blend + // (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps + // its depth writes. 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 3c92a249..d6967c74 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -1547,6 +1547,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { "ProgramFactory::ReflectFragmentOutputs: failed to create reflection module (result=%d)", static_cast(createResult)); 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; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 8aa0e029..38fd2167 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3552,6 +3552,17 @@ void main() { (bufferMask.a() ? VK_COLOR_COMPONENT_A_BIT : 0u)); Bool effectiveBlendEnabled = blendEnabled; 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()) { const auto drawBuffer = drawBuffers[i]; colorAttachmentTexture = resolveCompleteColorAttachmentTexture(i); diff --git a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp index ca908e5f..8b5962bd 100644 --- a/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp +++ b/MobileGL/MG_Test/Pipeline/PipelineQuirkTest.cpp @@ -258,11 +258,14 @@ TEST(PipelineQuirkStripDecision, MinBlendIsStripped) { EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); } -TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsStripped) { - // MC 26.3 OIT transmittance/accumulate: ONE+ONE additive accumulation writing depth. +TEST(PipelineQuirkStripDecision, AdditiveOnePlusOneIsNotStripped) { + // 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( 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) { @@ -285,9 +288,10 @@ TEST(PipelineQuirkStripDecision, EffectivelyOpaqueBlendIsNotStripped) { 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. + // 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( - 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)); } @@ -314,8 +318,8 @@ TEST(PipelineQuirkStripDecision, FragDepthWriterIsExempt) { } TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) { - // The hazard is not limited to attachment 0: the 26.3 transmittance pass accumulates - // into a 2-target MRT. + // The scan is not limited to attachment 0: an extremum accumulation on any live + // attachment marks the pipeline. PipelineFactory::PipelineCreatePayload payload{}; payload.colorAttachmentCount = 2; payload.depthTestEnable = true; @@ -323,21 +327,20 @@ TEST(PipelineQuirkStripDecision, AccumulationOnSecondaryAttachmentIsStripped) { 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); + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, 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. + // SRC_ALPHA,ONE additive: the classic *sorted* particle/glow blend. Kept exempt like + // every other ADD-op shape now that the strip is extremum-only. 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 + // Deliberate narrowing: only the MIN/MAX extremum ops carry the depth-bounds // 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, @@ -348,7 +351,7 @@ TEST(PipelineQuirkStripDecision, ReverseSubtractIsNotStripped) { 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)); + true, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_MAX, VK_COLOR_COMPONENT_A_BIT)); EXPECT_TRUE(PipelineFactory::ShouldSuppressDepthWrite(payload)); }