diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs
index 8efb8feb..f535c649 100644
--- a/src/SharpEmu.Libs/Agc/AgcExports.cs
+++ b/src/SharpEmu.Libs/Agc/AgcExports.cs
@@ -142,9 +142,15 @@ public static partial class AgcExports
private const uint CbColor0Base = 0x318;
private const uint CbColorRegisterStride = 15;
private const uint CbColor0Info = 0x31C;
+ private const uint CbColor0ClearWord0 = 0x323;
+ private const uint CbColor0ClearWord1 = 0x324;
private const uint CbColor0BaseExt = 0x390;
private const uint CbColor0Attrib2 = 0x3B0;
private const uint CbColor0Attrib3 = 0x3B8;
+ // CB_COLORn_INFO.DCC_ENABLE (gc_10_1_0_sh_mask.h). On GFX10 the legacy
+ // FAST_CLEAR and COMPRESSION bits stay clear because DCC, not CMASK,
+ // carries the compression.
+ private const uint CbColorInfoDccEnableMask = 1u << 28;
private const uint CbBlend0Control = 0x1E0;
private const uint PaScModeCntl0 = 0x292;
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
@@ -504,7 +510,8 @@ public static partial class AgcExports
float ClearRed = 0f,
float ClearGreen = 0f,
float ClearBlue = 0f,
- float ClearAlpha = 1f);
+ float ClearAlpha = 1f,
+ bool IsDccFastClear = false);
private sealed record TranslatedImageBinding(
TextureDescriptor Descriptor,
@@ -6865,6 +6872,29 @@ public static partial class AgcExports
$"dst=0x{resolveDestination.Address:X16}");
}
+ // A DCC fast clear writes metadata only; the colour block discards
+ // the quad's shaded output. Reset the attachment and drop the draw,
+ // which reproduces the observable effect of a clear to zero without
+ // modelling DCC block state.
+ if (translatedDraw.IsDccFastClear)
+ {
+ foreach (var target in translatedDraw.GuestTargets)
+ {
+ if (target.Address != 0)
+ {
+ VulkanVideoPresenter.RequestGuestColorClear(target.Address);
+ }
+ }
+
+ ReturnPooledDrawArrays(
+ translatedDraw,
+ globals: true,
+ vertex: true,
+ index: true);
+ state.TranslatedDraw = null;
+ return;
+ }
+
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
if (firstTarget.Address != 0)
{
@@ -7781,6 +7811,12 @@ public static partial class AgcExports
pixelUserData[index] = pixelEvaluation.InitialScalarRegisters[index];
}
+ var renderState = ApplyTransparentPremultipliedFillClear(
+ CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
+ textures,
+ vertexInputs,
+ pixelEvaluation.InitialScalarRegisters);
+
draw = new TranslatedGuestDraw(
exportShaderAddress,
pixelShaderAddress,
@@ -7798,11 +7834,7 @@ public static partial class AgcExports
renderTargets,
DecodeDepthTarget(state.CxRegisters),
guestTargets,
- ApplyTransparentPremultipliedFillClear(
- CreateRenderState(state.CxRegisters, renderTargets, pixelColorExportMasks),
- textures,
- vertexInputs,
- pixelEvaluation.InitialScalarRegisters),
+ renderState,
pixelUserData,
state.CxRegisters.TryGetValue(CbBlend0Control, out var rawBlend) ? rawBlend : 0,
state.CxRegisters.TryGetValue(
@@ -7816,7 +7848,15 @@ public static partial class AgcExports
fullscreenClearColor.Red,
fullscreenClearColor.Green,
fullscreenClearColor.Blue,
- fullscreenClearColor.Alpha);
+ fullscreenClearColor.Alpha,
+ IsDccFastClearDraw(
+ state.CxRegisters,
+ renderTargets,
+ textures,
+ vertexInputs,
+ renderState,
+ primitiveType,
+ vertexCount));
return true;
}
@@ -8093,6 +8133,113 @@ public static partial class AgcExports
};
}
+ ///
+ /// Recognises the covering quad a GFX10 driver issues to clear a
+ /// DCC-compressed colour target. There is no clear packet: the driver
+ /// programs CB_COLORn_CLEAR_WORD0/1 and draws a quad that the colour block
+ /// turns into DCC clear codes, discarding whatever the pixel shader
+ /// exported. Executing it as an ordinary draw writes the shaded output
+ /// instead, and because the blend it uses computes
+ /// a <- a_src + a_dst * (1 - a_src) - fixed point 1 - the target's
+ /// alpha then climbs every frame and saturates.
+ ///
+ /// Restricted to clear-to-zero. The reset performed for a match clears the
+ /// attachment to zero, so a nonzero CLEAR_WORD would be cleared to the
+ /// wrong colour; those fall through and are drawn. Zero is zero under every
+ /// encoding the register can carry, so the pair needs no format handling.
+ ///
+ /// The clip-space test is load-bearing rather than belt-and-braces: fills
+ /// sharing the vertex count, topology and blend outnumber the clears by two
+ /// orders of magnitude and sit at coordinates well outside the frame.
+ ///
+ private const uint TriangleStripPrimitive = 6;
+
+ // A float32x3 vertex position stream (BUF_DATA_FORMAT_32_32_32 / FLOAT).
+ private const uint PositionDataFormat = 13;
+ private const uint PositionNumberFormat = 7;
+
+ private static bool IsDccFastClearDraw(
+ IReadOnlyDictionary registers,
+ IReadOnlyList renderTargets,
+ IReadOnlyList textures,
+ IReadOnlyList vertexInputs,
+ GuestRenderState renderState,
+ uint primitiveType,
+ uint vertexCount)
+ {
+ if (textures.Count != 0 ||
+ vertexCount != 4 ||
+ primitiveType != TriangleStripPrimitive ||
+ renderTargets.Count == 0 ||
+ renderState.Blends.Count == 0 ||
+ !renderState.Blends.All(IsTransparentPremultipliedFillBlend))
+ {
+ return false;
+ }
+
+ var slotStride = renderTargets[0].Slot * CbColorRegisterStride;
+ return registers.TryGetValue(CbColor0Info + slotStride, out var info) &&
+ (info & CbColorInfoDccEnableMask) != 0 &&
+ registers.TryGetValue(CbColor0ClearWord0 + slotStride, out var clearWord0) &&
+ registers.TryGetValue(CbColor0ClearWord1 + slotStride, out var clearWord1) &&
+ clearWord0 == 0 &&
+ clearWord1 == 0 &&
+ CoversClipSpace(vertexInputs, vertexCount);
+ }
+
+ ///
+ /// True when the draw's float32x3 position stream spans the full clip
+ /// rectangle, i.e. x and y both reach -1 and +1.
+ ///
+ private static bool CoversClipSpace(
+ IReadOnlyList vertexInputs,
+ uint vertexCount)
+ {
+ const float Tolerance = 0.001f;
+ foreach (var input in vertexInputs)
+ {
+ if (input.DataFormat != PositionDataFormat ||
+ input.NumberFormat != PositionNumberFormat)
+ {
+ continue;
+ }
+
+ var stride = input.Stride == 0 ? 12u : input.Stride;
+ var available = Math.Min(input.DataLength, input.Data.Length);
+ float minX = float.MaxValue, maxX = float.MinValue;
+ float minY = float.MaxValue, maxY = float.MinValue;
+ var seen = 0;
+ for (var vertex = 0u; vertex < vertexCount; vertex++)
+ {
+ var at = (int)(input.OffsetBytes + (vertex * stride));
+ if (at + 12 > available)
+ {
+ break;
+ }
+
+ var position = input.Data.AsSpan(at);
+ var x = BitConverter.ToSingle(position);
+ var y = BitConverter.ToSingle(position[4..]);
+ if (!float.IsFinite(x) || !float.IsFinite(y))
+ {
+ return false;
+ }
+
+ minX = Math.Min(minX, x);
+ maxX = Math.Max(maxX, x);
+ minY = Math.Min(minY, y);
+ maxY = Math.Max(maxY, y);
+ seen++;
+ }
+
+ return seen >= 3 &&
+ minX <= -1f + Tolerance && maxX >= 1f - Tolerance &&
+ minY <= -1f + Tolerance && maxY >= 1f - Tolerance;
+ }
+
+ return false;
+ }
+
private static bool IsTransparentPremultipliedFillBlend(GuestBlendState blend) =>
blend is
{
diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
index 52f473d0..7678cbd6 100644
--- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
+++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs
@@ -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 _pendingGuestColorClears = new();
+
+ ///
+ /// Clear a guest colour target to zero at its next render pass.
+ ///
+ /// Deliberately not : 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 Initialized makes the render pass
+ /// itself clear via .
+ ///
+ internal static void RequestGuestColorClear(ulong address)
+ {
+ if (address != 0)
+ {
+ _pendingGuestColorClears[address] = 0;
+ }
+ }
+
///
/// 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 &&