mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-30 13:24:19 +08:00
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:
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user