AGC: honour DCC fast clears instead of drawing the clear quad (#738)

On GFX10 a colour clear is not a packet. The driver programs
CB_COLORn_CLEAR_WORD0/1 and draws a covering quad which the colour block
turns into DCC clear codes, discarding whatever the pixel shader
exported. We executed that quad as an ordinary draw, so the shaded output
landed in the surface instead of a clear.

That alone would be a wrong-pixels bug, but the blend those quads use
makes it compound. Every draw into the target blends src=ONE,
dst=ONE_MINUS_SRC_ALPHA, so alpha follows a <- a_src + a_dst*(1 - a_src),
whose fixed point is 1. A guest colour attachment is cleared once on
first use and loaded on every pass after, so nothing ever resets it and
the channel climbs until it saturates. Where such a surface is a
compositing layer, the final image is ui.rgb + scene.rgb*(1 - ui.a) and a
saturated alpha multiplies the scene away entirely - the scene renders
correctly the whole time and is then masked to black.

Recognise the clear and perform it: the attachment is reset and the quad
is dropped, which reproduces the observable effect without modelling DCC
block state. The reset drops the image's Initialized flag so the next
render pass clears via AttachmentLoadOp.Clear, rather than enqueuing a
CmdClearColorImage - the latter lands outside the following render pass,
and a target cleared that way was still observed reading back its
previous contents.

Restricted to clear-to-zero: the reset clears to zero, so a nonzero
CLEAR_WORD would be cleared to the wrong colour and is left to be drawn.
Zero is zero under every encoding the register pair can carry, so the
test needs no format handling.

The clip-space span test is load-bearing rather than defensive. Fills
sharing the vertex count, topology and blend outnumber the clears by two
orders of magnitude and sit well outside the frame; treating those as
clears erases the UI and blanks video surfaces.
This commit is contained in:
Arseny Yankovsky
2026-08-02 15:39:25 +02:00
committed by GitHub
parent cf3bd0b4f2
commit 4b5ea6a793
2 changed files with 185 additions and 7 deletions
@@ -3,6 +3,7 @@
using Silk.NET.Core;
using Silk.NET.Core.Native;
using System.Collections.Concurrent;
using SharpEmu.HLE;
using SharpEmu.Libs.Agc;
using SharpEmu.Libs.Media;
@@ -1259,6 +1260,25 @@ internal static unsafe class VulkanVideoPresenter
}
}
private static readonly ConcurrentDictionary<ulong, byte> _pendingGuestColorClears = new();
/// <summary>
/// Clear a guest colour target to zero at its next render pass.
///
/// Deliberately not <see cref="SubmitOffscreenColorClear"/>: that enqueues
/// a CmdClearColorImage which lands outside the render pass that follows
/// it, so a target cleared this way was still observed reading back its
/// previous contents. Dropping <c>Initialized</c> makes the render pass
/// itself clear via <see cref="AttachmentLoadOp.Clear"/>.
/// </summary>
internal static void RequestGuestColorClear(ulong address)
{
if (address != 0)
{
_pendingGuestColorClears[address] = 0;
}
}
/// <summary>
/// Apply a solid color clear to offscreen guest render targets without a
/// graphics pipeline. Used for empty-SRT procedural clear draws that
@@ -12412,6 +12432,17 @@ internal static unsafe class VulkanVideoPresenter
// format could later be replayed inside a render pass of the
// other identity.
formats[index] = targets[index].Format;
// Guest colour attachments load their previous contents on
// every pass after the first, so nothing resets one until the
// guest clears it. Consume a pending clear here, before the
// render pass is built, so the pass uses LoadOp.Clear.
if (work.Targets[index].Address != 0 &&
_pendingGuestColorClears.TryRemove(work.Targets[index].Address, out _))
{
targets[index].Initialized = false;
}
if (work.Targets[index].Address != 0 &&
TakeGuestImageInitialData(work.Targets[index].Address) is { } initialData &&
!targets[index].Initialized &&