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 &&