fix(agc,video): CMASK/DCC metadata state machine , remove heavy heavy motion trails in DeadCell (#836)

* fix(agc): add support for CMASK fast clear in rendering process

* fix(agc): prevent stale cross-frame label writes from bypassing WAIT_REG_MEM

Add frame-ID tracking to GpuWaitRegistry so that WAIT_REG_MEM in frame N+1
is not satisfied by a label written in frame N. Previously, a label that
persisted in guest memory from the previous frame could satisfy a new
WAIT_REG_MEM immediately, causing the waiting DCB to bypass its fence and
execute out of order.

Changes:
- GpuWaitRegistry: add _labelFrameIds, _currentFrameId, AdvanceFrame(),
  IsLabelFresh(); RecordProduced now stamps the frame ID on each write
- AgcExports: call AdvanceFrame() at RFlip (frame boundary);
  HandleSubmittedWaitRegMem checks IsLabelFresh before bypassing

This is one piece of the Dead Cells character visibility fix (issue #833).
The cross-frame label reuse could cause composite passes to execute before
character-layer passes, resulting in invisible characters.

* fix(video): simulate CMASK 'all clear' at frame boundary to prevent trails

On real PS5, CMASK is reset to 'all clear' at each frame boundary,
so render targets are effectively cleared. SharpEmu did not implement
this behavior, causing GBuffer targets to LOAD stale data from the
previous frame, resulting in visible trails/ghosting.

Reset Initialized=false for all offscreen guest images at the present
boundary (when _currentFrameSlot changes). This ensures every render
target starts each frame with LoadOp.Clear, matching the hardware's
CMASK behavior.

This is the trail fix for Dead Cells (issue #833), complementing the
cross-frame label staleness fix in GpuWaitRegistry.

* fix(video): only reset render targets (not textures) for CMASK simulation

Previous commit 89b4610 reset Initialized for ALL guest images, including
textures and storage images, causing a completely black screen. Only images
with a RenderPass (actual color render targets) should be reset, as CMASK
'all clear' only applies to color render targets, not sampled textures.

* Revert "fix(video): simulate CMASK 'all clear' at frame boundary to prevent trails"

This reverts commit 89b4610a015619848e4000372822dca1c08051ff.

* fix(agc): implement CMASK state machine for render target clearing

Implement CMASK (Color Mask) hardware behavior based on shadPS4's approach:

1. Track CMASK addresses from CB_COLORn_CMASK registers
2. Detect compute shaders that write to CMASK addresses (IsComputeMetaClear
   heuristic: no bitwise XOR in shader = clear shader)
3. Mark CMASK as 'all clear' when compute shader writes to it
4. EliminateFastClear mode now checks CMASK state before clearing

This addresses the trail/ghosting issue in Dead Cells (issue #833) where
642x362 GBuffer targets were never cleared after first use, causing stale
data from previous frames to persist.

Reference: shadPS4's IsComputeMetaClear + EliminateFastClear implementation.

* fix(agc): implement CMASK state machine for render target clearing

Based on shadPS4's PM4 implementation, add CMASK (Color Mask) hardware
behavior tracking:

1. TrackCmaskAddresses: Read CB_COLORn_CMASK registers to get CMASK addresses
2. CheckCmaskWrite: When DMA fill targets CMASK address with value 0,
   mark CMASK as 'all clear' (shadPS4's FillBuffer logic)
3. EliminateFastClear: Check CMASK state before clearing - only clear
   if CMASK is 'all clear', then mark as 'dirty'
4. DMA fill: Call TrackCmaskAddresses before processing to ensure
   CMASK addresses are registered when DMA fills happen

This addresses the trail/ghosting issue in Dead Cells (issue #833) where
642x362 GBuffer targets were never cleared after first use.

Reference: shadPS4's IsComputeMetaClear + EliminateFastClear + FillBuffer.

* refactor(agc): remove diagnostic CMASK traces from hot path

Remove 4 diagnostic TraceAgcShader calls that were added during CMASK
investigation:
- agc.cb_regs=[...] - allocated List<string> + string.Join on every draw
- agc.cmask_track - per-slot interpolated string on every draw
- agc.cmask_write - per-DMA-fill interpolated string
- agc.eliminate_fast_clear - per-draw interpolated string

These were debugging probes and are no longer needed.

* fix(agc): sync constant-fill zero writes to Vulkan render targets

Dead Cells GBuffer trails: the constant-fill compute kernel
(TrySubmitConstantFillKernel) wrote zeros to guest memory but never
invalided the host Vulkan GuestImageResource that backs the render
target, so the GBuffer pass kept LoadOp.Load stale previous-frame pixels.

After the guest write, request a guest color clear尷 ... clear for the
destination address (RequestAgentColorClear projection (clear)) so the next render pass
uses LoadOp.Clear. The pending clearandray mechanism already exists; the
fill kernel was simply bypassing it.

* Remove AGC lifecycle debug probes

Remove transient render-target lifecycle and indirect draw debug instrumentation from AgcExports. The change strips the noisy console probing while keeping the active render-target tracking logic intact for draw sequencing and validation.

* fix(video): clear frame-stale MRT groups at guest flip boundary

On hardware a colour surface whose fast-clear metadata is in reset
state reads back as the CB clear value instead of stale memory, so
games can leave per-frame MRT groups uncleaned and rely on that
implicit initialization. With no metadata layer, such a target keeps
whatever touched it last; when the group shares addresses with the
compositor's output, the entire finished previous frame bleeds
through every region the new frame does not repaint (Dead Cells
dungeon trails / ghosted characters).

Arm a reset at the guest's own flip command - the authoritative
frame boundary inside the submission stream - and let the first
multi-attachment colour group of the fresh frame consume it,
starting from LoadOp.Clear. One arm per flip yields exactly one
reset per guest frame regardless of CPU/GPU pipelining; later
groups keep load semantics so intra-frame pass chaining is
untouched. CPU-backed images are never touched.

Mirrors shadPS4's meta-state-driven attachment.is_clear at
render-pass begin (vk_rasterizer.cpp BeginRendering).

* fix(agc): correct CbColor0Cmask register offset and add EXT support

CbColor0Cmask was 0x320 (CMASK_SLICE, tile_max:14) instead of 0x31F
(CMASK_BASE_ADDRESS). TrackCmaskAddresses read the slice count instead
of the base address, so CMASK registration always produced garbage or
zero — the entire meta-state chain starved for every game.

Fix: 0x320 → 0x31F. Add CbColor0CmaskBaseExt (0x398) and decode the
full 48-bit address: ((ext & 0xFF) << 40) | ((low & 0x1FFFFFFF) << 8).

Phase 0 probe confirmed Dead Cells programs neither CMASK nor DCC
registers (all zeros) — it relies on unified memory semantics where
unwritten surfaces read as zero. The register offset fix is still
needed for games that do use CMASK metadata.

* feat(agc,video): CMASK/DCC meta-state machine with CLEAR WORD support

Replace the flat _cmaskClearedState dictionary with a proper meta-state
ledger that separates registration, clearing, and dirtying:

- MetaSurfaceInfo keyed by colour-buffer address (not meta address)
- Reverse map _cmaskToColorBuffer for fill/compute write detection
- TrackCmaskAddresses reads both CMASK (0x31F) and DCC (0x325) with
  EXT high-bit decoding; prefers CMASK, falls back to DCC
- CLEAR_WORD captured at registration time, passed through to
  BeginTranslatedRenderPass as VkClearValue
- EFC checks specific surface (not 'any cleared → clear slot0'),
  dirties only that surface (not entire table)
- CheckCmaskWrite uses reverse map for O(1) lookup
- Presenter bind loop queries IsMetaClearedForSurface → LoadOp.Clear
- _metaStateOverridesFlipArm flag gates flip-arm for gradual retirement

Phase 0 probe confirmed Dead Cells programs neither CMASK nor DCC
(all registers zero) — flip-arm remains the correct fix for that game.
The meta-state machine serves games that do use CMASK/DCC metadata.

* fix(agc,video): implement surface clearing at guest flip boundary

* refactor(video): remove flip-arm heuristic, meta-state machine fully replaces it

The flip-arm (_frameColorResetArmed) was a heuristic that cleared the
first multi-attachment colour group at guest flip boundary.  The
CMASK/DCC meta-state machine now handles all clearing: at flip time
MarkAllSurfacesCleared() marks every registered surface as cleared;
at bind time IsMetaClearedForSurface() triggers LoadOp.Clear and
consumes the state.  This replaces the flip-arm with a per-surface
state machine that correctly handles both explicit clears (DMA fill,
compute, EFC) and implicit frame-boundary resets.

Also fixed a bug where TrackCmaskAddresses overwrote IsCleared to
false on every call, defeating the frame-boundary reset.  Now
preserves existing IsCleared state when re-registering.

* Thread-safe meta-surface state & meta-clear decode

Add synchronization for metadata state: introduce _metaSurfaceGate and guard accesses to _metaSurfaces and _cmaskToColorBuffer in AgcExports to avoid concurrent-dictionary corruption and ensure small critical sections. Preserve cleared state only when metadata binding matches during re-registration. Make Gen5 texture format constants internal. Update logic to set/consume IsCleared under lock and check CMASK nearby windows safely.

Decode meta clear values in VulkanVideoPresenter: skip CPU-backed targets for meta clears, add UnpackMetaClearValue and HalfToFloat to convert CLEAR_WORD0/1 into proper ClearColorValue for R8G8B8A8_UNORM and R16G16B16A16_FLOAT formats (with an 8_8_8_8 fallback). This ensures correct clear colours and thread-safe metadata handling.
This commit is contained in:
angleyanalbedo
2026-08-24 18:56:44 +08:00
committed by GitHub
parent 4a7a45d1b3
commit f8a826ec1b
3 changed files with 414 additions and 9 deletions
+300 -6
View File
@@ -118,6 +118,29 @@ public static partial class AgcExports
// Multiple producers can share one target label; last-writer-wins would
// starve waits on the others.
private static readonly Dictionary<ulong, List<ulong>> _cbReleaseMemTargets = new();
// CMASK meta-state tracking: maps colour-buffer addresses to their
// compression metadata. Keyed by colour-buffer base address so the
// consumption path (which only knows the surface address) can query
// directly without a reverse lookup.
private record struct MetaSurfaceInfo(
ulong CmaskAddress,
uint ClearWord0,
uint ClearWord1,
bool IsCleared);
private static readonly Dictionary<ulong, MetaSurfaceInfo> _metaSurfaces = new();
// Reverse map: CMASK address → colour-buffer address. Needed so
// CheckCmaskWrite (which only sees the write target address) can
// find the owning surface.
private static readonly Dictionary<ulong, ulong> _cmaskToColorBuffer = new();
// Guards _metaSurfaces and _cmaskToColorBuffer. Two threads touch them:
// the parse thread (registration in TrackCmaskAddresses, CheckCmaskWrite
// from DMA/compute writes, EFC consumption) and the render thread
// (MarkAllSurfacesCleared at guest flip, IsMetaClearedForSurface /
// ConsumeMetaClear / GetMetaClearValue at pass-record time). Plain
// Dictionaries corrupt under concurrent write, so every access below
// holds this gate. Keep the critical sections tiny and never block on
// anything external while holding it.
private static readonly object _metaSurfaceGate = new();
// header -> {ring base, write cursor} of the last submitted slice.
// Submissions stay cursor-bounded since rings aren't zeroed. Lap
// distinguishes a stale cursor from a previous pass over the same base.
@@ -1095,15 +1118,20 @@ public static partial class AgcExports
private const uint CbColor0Base = 0x318;
private const uint CbColorRegisterStride = 15;
private const uint CbColor0Info = 0x31C;
private const uint CbColor0Cmask = 0x31F;
private const uint CbColor0ClearWord0 = 0x323;
private const uint CbColor0ClearWord1 = 0x324;
private const uint CbColor0DccBase = 0x325;
private const uint CbColor0BaseExt = 0x390;
private const uint CbColor0CmaskBaseExt = 0x398;
private const uint CbColor0DccBaseExt = 0x3A8;
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 CbColorInfoFastClearEnableMask = 1u << 12;
private const uint CbBlend0Control = 0x1E0;
private const uint PaScModeCntl0 = 0x292;
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
@@ -1123,8 +1151,8 @@ public static partial class AgcExports
private const uint EsUserDataRegister = 0xCC;
private const uint ComputeUserDataRegister = 0x240;
private const uint NggUserDataScalarRegisterBase = 8;
private const uint Gen5TextureFormatR8G8B8A8Unorm = 10;
private const uint Gen5TextureFormatR16G16B16A16Float = 12;
internal const uint Gen5TextureFormatR8G8B8A8Unorm = 10;
internal const uint Gen5TextureFormatR16G16B16A16Float = 12;
private const uint Gen5TextureType1D = 8;
private const uint Gen5TextureType2D = 9;
private const uint Gen5TextureType3D = 10;
@@ -5083,6 +5111,10 @@ public static partial class AgcExports
if (op == ItNop && register == RDmaData && length >= 7)
{
// Ensure CMASK addresses are tracked before DMA fills
var tempTargets = GetRenderTargets(state.CxRegisters);
TrackCmaskAddresses(state.CxRegisters, tempTargets);
ApplySubmittedDmaData(
ctx,
gpuState,
@@ -5243,6 +5275,7 @@ public static partial class AgcExports
{
TraceFramePacketSummary(state);
SyncCpuWrittenGuestImages(ctx);
GpuWaitRegistry.AdvanceFrame();
if (!TryReadUInt32(ctx, currentAddress + 4, out var videoOutHandle) ||
!TryReadUInt32(ctx, currentAddress + 8, out var displayBufferIndexRaw) ||
!TryReadUInt32(ctx, currentAddress + 12, out var flipMode) ||
@@ -6264,6 +6297,13 @@ public static partial class AgcExports
ulong byteCount,
uint? fillValue)
{
// Check if this DMA write targets a CMASK address (shadPS4's FillBuffer
// logic: when a buffer fill targets CMASK metadata, mark it as "all clear")
if (fillValue is { } fillVal && fillVal == 0)
{
CheckCmaskWrite(destinationAddress, null);
}
var hasImage = GuestGpu.Current.TryGetGuestImageExtent(
destinationAddress,
out var width,
@@ -7072,7 +7112,13 @@ public static partial class AgcExports
if (hasCurrent && GpuWaitRegistry.Compare(waiter, currentValue))
{
return false; // already satisfied — keep parsing
// Value satisfies the condition, but only bypass if the label was
// written in the current frame. A stale label from a previous frame
// means the producer hasn't written yet this frame — must wait.
if (GpuWaitRegistry.IsLabelFresh(ctx.Memory, waitAddress))
{
return false; // satisfied by current-frame write — keep parsing
}
}
if (!_gpuWaitSuspendEnabled)
@@ -8063,7 +8109,8 @@ public static partial class AgcExports
var hasPsInputEna = state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna);
var hasPsInputAddr = state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr);
state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType);
var renderTargets = GetRenderTargets(state.CxRegisters);
var renderTargets = GetRenderTargets(state.CxRegisters);
TrackCmaskAddresses(state.CxRegisters, renderTargets);
var drawSequence = ++gpuState.WorkSequence;
if (state.PendingTargetlessDraw is { } stalePendingDraw)
{
@@ -8083,6 +8130,31 @@ public static partial class AgcExports
if (TryGetCbColorControlMode(state.CxRegisters, out var cbMode) &&
IsCbMetadataColorMode(cbMode))
{
// EliminateFastClear: the game explicitly asks the CB to clear
// the fast-clear metadata and the colour buffer.
if (cbMode == (uint)CbColorMode.EliminateFastClear &&
renderTargets.Count > 0 &&
renderTargets[0].Address != 0)
{
var targetAddr = renderTargets[0].Address;
bool requestClear;
lock (_metaSurfaceGate)
{
requestClear =
_metaSurfaces.TryGetValue(targetAddr, out var meta) &&
meta.IsCleared;
if (requestClear)
{
_metaSurfaces[targetAddr] = meta with { IsCleared = false };
}
}
if (requestClear)
{
VulkanVideoPresenter.RequestGuestColorClear(targetAddr);
}
}
if (_traceAgcShader || ShouldTraceHotPath(ref _cbMetadataSkipTraceCount))
{
TraceAgcShader(
@@ -8297,6 +8369,34 @@ public static partial class AgcExports
return;
}
// DbRenderControl CLEARON (bit0): when set, the CB clears color
// targets on first draw. Handle color targets (depth is already
// handled by DecodeDepthState).
if (state.CxRegisters.TryGetValue(DbRenderControl, out var rc) && (rc & 0x1u) != 0)
{
foreach (var rt in translatedDraw.RenderTargets)
{
if (rt.Address != 0)
{
VulkanVideoPresenter.RequestGuestColorClear(rt.Address);
}
}
}
// CMASK fast clear: CB_COLORn_INFO.FAST_CLEAR (bit12) set on
// one or more targets. The CB clears via CMASK before the draw
// writes; mark targets for clear-on-first-use.
if (IsCmaskFastClearDraw(state.CxRegisters, translatedDraw.RenderTargets))
{
foreach (var rt in translatedDraw.RenderTargets)
{
if (rt.Address != 0)
{
VulkanVideoPresenter.RequestGuestColorClear(rt.Address);
}
}
}
var firstTarget = translatedDraw.RenderTargets.FirstOrDefault();
if (firstTarget.Address != 0)
{
@@ -9589,6 +9689,193 @@ public static partial class AgcExports
CoversClipSpace(vertexInputs, vertexCount);
}
/// <summary>
/// GFX10 CMASK fast clear: CB_COLORn_INFO.FAST_CLEAR (bit 12) set on
/// one or more targets. The CB clears via CMASK before the draw writes;
/// mark targets for clear-on-first-use. Unlike DCC, the draw content
/// IS written (not dropped). Dead Cells uses DbRenderControl CLEARON
/// instead (bit0), not this mechanism.
/// </summary>
private static bool IsCmaskFastClearDraw(
IReadOnlyDictionary<uint, uint> registers,
IReadOnlyList<RenderTargetDescriptor> renderTargets)
{
foreach (var rt in renderTargets)
{
var stride = rt.Slot * CbColorRegisterStride;
if (registers.TryGetValue(CbColor0Info + stride, out var info) &&
(info & CbColorInfoFastClearEnableMask) != 0)
{
return true;
}
}
return false;
}
/// <summary>
/// Registers the CMASK metadata mapping for each colour buffer.
/// Does NOT mark as cleared — clearing only happens on actual clear
/// events (DMA fill, compute write, EFC draw).
/// </summary>
private static void TrackCmaskAddresses(
IReadOnlyDictionary<uint, uint> registers,
IReadOnlyList<RenderTargetDescriptor> renderTargets)
{
foreach (var rt in renderTargets)
{
var stride = rt.Slot * CbColorRegisterStride;
// CMASK metadata address (legacy GCN path).
var cmaskRegAddr = CbColor0Cmask + stride;
registers.TryGetValue(cmaskRegAddr, out var cmaskLow);
var cmaskExtAddr = CbColor0CmaskBaseExt + rt.Slot;
registers.TryGetValue(cmaskExtAddr, out var cmaskExt);
var cmaskAddress = ((ulong)(cmaskExt & 0xFFu) << 40) |
((ulong)(cmaskLow & 0x1FFFFFFFu) << 8);
// DCC metadata address (GFX10+ primary path).
var dccRegAddr = CbColor0DccBase + stride;
registers.TryGetValue(dccRegAddr, out var dccLow);
var dccExtAddr = CbColor0DccBaseExt + rt.Slot;
registers.TryGetValue(dccExtAddr, out var dccExt);
var dccAddress = ((ulong)(dccExt & 0xFFu) << 40) |
((ulong)(dccLow & 0x1FFFFFFFu) << 8);
// Prefer CMASK if present; fall back to DCC.
var metaAddress = cmaskAddress != 0 ? cmaskAddress : dccAddress;
var cw0Addr = CbColor0ClearWord0 + stride;
var cw1Addr = CbColor0ClearWord1 + stride;
registers.TryGetValue(cw0Addr, out var cw0);
registers.TryGetValue(cw1Addr, out var cw1);
lock (_metaSurfaceGate)
{
_metaSurfaces[rt.Address] = new MetaSurfaceInfo(
metaAddress, cw0, cw1,
// Re-registration runs on every draw; keep the cleared state
// so a mark-clear event survives until the pass consumes it.
// If the metadata binding changed, the old state refers to
// the old metadata and must be reset.
IsCleared: _metaSurfaces.TryGetValue(rt.Address, out var prev) &&
prev.IsCleared &&
prev.CmaskAddress == metaAddress);
if (metaAddress != 0)
{
_cmaskToColorBuffer[metaAddress] = rt.Address;
}
}
}
}
/// <summary>
/// Checks if a write targets a registered CMASK address. If so,
/// marks the owning colour buffer's metadata as "all clear".
/// </summary>
private static void CheckCmaskWrite(
ulong writeAddress,
SubmittedGpuState? gpuState)
{
if (writeAddress == 0)
{
return;
}
lock (_metaSurfaceGate)
{
// Exact match: write directly to a registered CMASK address.
if (_cmaskToColorBuffer.TryGetValue(writeAddress, out var cbAddr))
{
if (_metaSurfaces.TryGetValue(cbAddr, out var meta))
{
_metaSurfaces[cbAddr] = meta with { IsCleared = true };
}
return;
}
// CMASK surfaces are small (typically ≤ 4 KiB). Check the ±1024
// window around each registered address to catch partial writes.
foreach (var (cmaskAddr, colorBufAddr) in _cmaskToColorBuffer)
{
if (writeAddress >= cmaskAddr && writeAddress < cmaskAddr + 1024)
{
if (_metaSurfaces.TryGetValue(colorBufAddr, out var meta))
{
_metaSurfaces[colorBufAddr] = meta with { IsCleared = true };
}
return;
}
}
}
}
/// <summary>
/// Returns true if the colour buffer at <paramref name="colorBufferAddress"/>
/// has pending CMASK "all clear" metadata — i.e. the surface was fast-cleared
/// but not yet rendered into.
/// </summary>
internal static bool IsMetaClearedForSurface(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
return _metaSurfaces.TryGetValue(colorBufferAddress, out var meta) &&
meta.IsCleared;
}
}
/// <summary>
/// Consumes the "all clear" state for the given surface, marking it dirty.
/// Called after the first render pass uses LoadOp.Clear.
/// </summary>
internal static void ConsumeMetaClear(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
if (_metaSurfaces.TryGetValue(colorBufferAddress, out var meta))
{
_metaSurfaces[colorBufferAddress] = meta with { IsCleared = false };
}
}
}
/// <summary>
/// Returns the CB clear word values for the given colour buffer.
/// </summary>
internal static (uint Cw0, uint Cw1) GetMetaClearValue(ulong colorBufferAddress)
{
lock (_metaSurfaceGate)
{
if (_metaSurfaces.TryGetValue(colorBufferAddress, out var meta))
{
return (meta.ClearWord0, meta.ClearWord1);
}
}
return (0, 0);
}
/// <summary>
/// Marks all registered surfaces as "all clear". Called at guest flip
/// (frame boundary). Real hardware applies a fast clear / load-clear to
/// its per-frame surfaces every frame; the emulator restores that
/// per-frame clear here, per surface, at flip time. This is the
/// per-surface successor of the removed flip-arm heuristic (which reset
/// only the first multi-attachment group).
/// </summary>
internal static void MarkAllSurfacesCleared()
{
lock (_metaSurfaceGate)
{
foreach (var (addr, meta) in _metaSurfaces)
{
_metaSurfaces[addr] = meta with { IsCleared = true };
}
}
}
/// <summary>
/// True when the draw's float32x3 position stream spans the full clip
/// rectangle, i.e. x and y both reach -1 and +1.
@@ -10001,7 +10288,7 @@ public static partial class AgcExports
private static readonly HashSet<ulong> _sampledRenderTargets = new();
private static readonly object _renderTargetProbeGate = new();
private static long _renderTargetSampleTraceCount;
private static long _indirectDrawProbeCount;
private static long _indirectDrawProbeCount;
private static long _indirectDrawEmitCount;
private static long _indirectDrawEmitRejectCount;
private static long _indirectMultiProbeCount;
@@ -12487,6 +12774,10 @@ public static partial class AgcExports
shaderAddress,
binding.Opcode);
// Check if this compute shader writes to a CMASK address
// (shadPS4's IsComputeMetaClear logic)
CheckCmaskWrite(texture.Address, gpuState);
TraceAgcShader(
$"agc.compute_writer addr=0x{texture.Address:X16} " +
$"fmt={texture.Format} num={texture.NumberType} tile={texture.TileMode} " +
@@ -13082,11 +13373,14 @@ public static partial class AgcExports
return;
}
GuestImageWriteTracker.Track(
GuestImageWriteTracker.Track(
destinationAddress,
(ulong)output.Length,
VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics,
"agc.constant-fill");
VulkanVideoPresenter.RequestGuestColorClear(destinationAddress);
},
$"constant_fill dst=0x{destinationAddress:X16} bytes={output.Length}");
description =
+33
View File
@@ -59,6 +59,10 @@ internal static class GpuWaitRegistry
// cycle forever even though a real producer did signal it. Keyed by (memory,
// address) so distinct guest processes never alias.
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
// Frame-staleness guard: tracks the frame ID of each label write so that
// WAIT_REG_MEM in frame N+1 is not satisfied by a stale write from frame N.
private static readonly Dictionary<(object, ulong), long> _labelFrameIds = new();
private static long _currentFrameId;
private static object? Canonicalize(object? memory)
@@ -71,6 +75,34 @@ internal static class GpuWaitRegistry
return memory;
}
/// <summary>
/// Advances the frame counter. Called at each frame boundary (flip) so that
/// stale label writes from previous frames cannot satisfy WAIT_REG_MEM.
/// </summary>
public static void AdvanceFrame()
{
System.Threading.Interlocked.Increment(ref _currentFrameId);
}
/// <summary>
/// Returns true if the label at (memory, address) was written in the
/// current frame, or has never been written (uninitialized).
/// Only labels written in a PREVIOUS frame are considered stale.
/// </summary>
public static bool IsLabelFresh(object memory, ulong address)
{
memory = Canonicalize(memory)!;
lock (_gate)
{
if (!_labelFrameIds.TryGetValue((memory, address), out var frameId))
{
return true; // never written — treat as fresh (not stale)
}
return frameId >= System.Threading.Volatile.Read(ref _currentFrameId);
}
}
public static int Count
{
get
@@ -576,6 +608,7 @@ internal static class GpuWaitRegistry
}
_lastProduced[(memory, address)] = value;
_labelFrameIds[(memory, address)] = System.Threading.Volatile.Read(ref _currentFrameId);
}
return LatchSatisfiedByValue(memory, address, value);
@@ -5884,6 +5884,7 @@ internal static unsafe class VulkanVideoPresenter
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
{
Agc.AgcExports.MarkAllSurfacesCleared();
FlushBatchedGuestCommands();
_guestImages.TryGetValue(work.Address, out var source);
if (_deviceLost ||
@@ -12770,6 +12771,18 @@ internal static unsafe class VulkanVideoPresenter
targets[index].Initialized = false;
}
// CMASK meta-state: if the surface's metadata says "all clear",
// start this pass from LoadOp.Clear and consume the state.
// CPU-backed targets are skipped (their guest memory contents
// are uploaded, not cleared) — same rule the flip-arm used.
if (work.Targets[index].Address != 0 &&
!targets[index].IsCpuBacked &&
Agc.AgcExports.IsMetaClearedForSurface(work.Targets[index].Address))
{
targets[index].Initialized = false;
Agc.AgcExports.ConsumeMetaClear(work.Targets[index].Address);
}
if (work.Targets[index].Address != 0 &&
TakeGuestImageInitialData(work.Targets[index].Address) is { } initialData &&
!targets[index].Initialized &&
@@ -13031,13 +13044,31 @@ internal static unsafe class VulkanVideoPresenter
&toDepthAttachment);
}
ClearColorValue[]? metaClearValues = null;
for (var ci = 0; ci < targets.Length; ci++)
{
if (!targets[ci].Initialized &&
work.Targets[ci].Address != 0)
{
var (cw0, cw1) = Agc.AgcExports.GetMetaClearValue(
work.Targets[ci].Address);
if (cw0 != 0 || cw1 != 0)
{
metaClearValues ??= new ClearColorValue[targets.Length];
metaClearValues[ci] = UnpackMetaClearValue(
work.Targets[ci].Format, cw0, cw1);
}
}
}
BeginTranslatedRenderPass(
renderPass,
framebuffer,
extent,
colorAttachmentCount: targets.Length,
hasDepthAttachment: depth is not null && !clearDepthSeparately,
clearDepth: depth?.ClearDepth ?? 1f);
clearDepth: depth?.ClearDepth ?? 1f,
colorClearValues: metaClearValues);
RecordTranslatedDrawInPass(resources, extent);
_vk.CmdEndRenderPass(_commandBuffer);
@@ -17688,20 +17719,67 @@ internal static unsafe class VulkanVideoPresenter
_vk.CmdEndRenderPass(_commandBuffer);
}
/// <summary>
/// Decodes the CB CLEAR_WORD0/1 pair into a float RGBA clear value
/// according to the surface pixel format. CLEAR_WORD holds the clear
/// colour packed in the surface's native layout, so the two 32-bit
/// words must be unpacked channel-by-channel; passing the raw word as
/// a single float channel clears to a garbage colour.
/// </summary>
private static ClearColorValue UnpackMetaClearValue(
uint format, uint cw0, uint cw1)
{
switch (format)
{
// Gen5 8_8_8_8 (R8G8B8A8): four UNORM bytes packed in WORD0,
// little-endian channel order R,G,B,A.
case Agc.AgcExports.Gen5TextureFormatR8G8B8A8Unorm:
return new ClearColorValue(
float32_0: ((cw0 >> 0) & 0xFF) / 255f,
float32_1: ((cw0 >> 8) & 0xFF) / 255f,
float32_2: ((cw0 >> 16) & 0xFF) / 255f,
float32_3: ((cw0 >> 24) & 0xFF) / 255f);
// Gen5 16_16_16_16 float (R16G16B16A16F): R,G as halfs in
// WORD0 and B,A as halfs in WORD1.
case Agc.AgcExports.Gen5TextureFormatR16G16B16A16Float:
return new ClearColorValue(
float32_0: HalfToFloat((ushort)(cw0 >> 0)),
float32_1: HalfToFloat((ushort)(cw0 >> 16)),
float32_2: HalfToFloat((ushort)(cw1 >> 0)),
float32_3: HalfToFloat((ushort)(cw1 >> 16)));
default:
// Unknown format: fall back to the common 8_8_8_8 layout.
return new ClearColorValue(
float32_0: ((cw0 >> 0) & 0xFF) / 255f,
float32_1: ((cw0 >> 8) & 0xFF) / 255f,
float32_2: ((cw0 >> 16) & 0xFF) / 255f,
float32_3: ((cw0 >> 24) & 0xFF) / 255f);
}
}
private static float HalfToFloat(ushort halfBits) =>
(float)BitConverter.UInt16BitsToHalf(halfBits);
private void BeginTranslatedRenderPass(
RenderPass renderPass,
Framebuffer framebuffer,
Extent2D extent,
int colorAttachmentCount = 1,
bool hasDepthAttachment = false,
float clearDepth = 1f)
float clearDepth = 1f,
ClearColorValue[]? colorClearValues = null)
{
colorAttachmentCount = Math.Max(colorAttachmentCount, 1);
var clearValueCount = colorAttachmentCount + (hasDepthAttachment ? 1 : 0);
var clearValues = stackalloc ClearValue[clearValueCount];
for (var index = 0; index < colorAttachmentCount; index++)
{
clearValues[index] = default;
clearValues[index] = colorClearValues is not null &&
index < colorClearValues.Length
? new ClearValue { Color = colorClearValues[index] }
: default;
}
// Reverse-Z is not assumed; clear depth to 1.0 (far) so a standard
// LessOrEqual/Less test keeps the nearest fragment.