From c4da98e889a59793b00b89e15b697c6e6be17f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 9 Oct 2026 02:23:40 +0200 Subject: [PATCH 1/2] GE dump recording: Read back framebuffers drawn before the recording With a hardware renderer, a framebuffer only exists on the GPU, so a dump copied whatever stale data RAM held when a game sampled a buffer it drew in an earlier frame. Silent Hill: Origins shows the previous frame by copying it into its 565 display buffer at the start of the next one, so its dumps displayed old scratch data. The recorder now reads such buffers back once before copying them out (not ones drawn during the recording, which replay draws again). It asks for the copy without creating a framebuffer: with BlockTransferAllowCreateFB (which Silent Hill has) a plain readback makes a new framebuffer at the destination and never writes RAM. Co-Authored-By: Claude Opus 5.5 --- GPU/Debugger/Record.cpp | 30 ++++++++++++++++++++++++++++++ GPU/Debugger/Record.h | 2 ++ 2 files changed, 32 insertions(+) diff --git a/GPU/Debugger/Record.cpp b/GPU/Debugger/Record.cpp index f2b07cdaac..7768c0ff51 100644 --- a/GPU/Debugger/Record.cpp +++ b/GPU/Debugger/Record.cpp @@ -163,6 +163,7 @@ bool Recorder::BeginRecording() { guard.unlock(); lastTextures.clear(); lastRenderTargets.clear(); + readbackAddrs.clear(); flipLastAction = gpuStats.totals.numFlips; flipFinishAt = -1; @@ -421,6 +422,30 @@ u32 Recorder::GetTargetFlags(u32 addr, u32 sizeInRAM) { return flags; } +// A hardware renderer keeps framebuffers on the GPU, so RAM can hold something older than what a game +// samples from a buffer it drew before the recording started (Silent Hill: Origins shows the previous +// frame by copying it into its display buffer). Read such buffers back once before copying them out. +// Anything drawn during the recording is drawn again on replay, so that doesn't need it. +void Recorder::ReadbackUndrawnVRAM(u32 addr, u32 bytes) { + if (!gpu || bytes == 0 || !Memory::IsVRAMAddress(addr)) { + return; + } + if (!readbackAddrs.insert(addr & 0x041FFFFF).second) { + return; + } + + uint32_t start = (addr >> DIRTY_VRAM_SHIFT) & DIRTY_VRAM_MASK; + uint32_t blocks = (bytes + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT; + for (uint32_t i = 0; i < blocks && start + i < DIRTY_VRAM_SIZE; ++i) { + if (dirtyVRAM[start + i] == DirtyVRAMFlag::DRAWN) { + return; + } + } + // Not PerformReadbackToMemory: with BlockTransferAllowCreateFB (Silent Hill: Origins) that would create a + // framebuffer at the destination and copy into it instead of writing RAM. + gpu->PerformMemoryCopy(addr, addr, bytes, GPUCopyFlag::FORCE_DST_MATCH_MEM | GPUCopyFlag::DISALLOW_CREATE_VFB); +} + void Recorder::EmitTextureData(int level, u32 texaddr) { GETextureFormat format = gstate.getTextureFormat(); int w = gstate.getTextureWidth(level); @@ -435,6 +460,8 @@ void Recorder::EmitTextureData(int level, u32 texaddr) { std::vector framebufData; if (Memory::IsVRAMAddress(texaddr)) { + ReadbackUndrawnVRAM(texaddr, bytes); + struct FramebufData { u32 addr; int bufw; @@ -543,6 +570,7 @@ void Recorder::EmitTransfer(u32 op) { dstBytes = Memory::ClampValidSizeAt(dstBasePtr, dstBytes); if (srcBytes != 0) { + ReadbackUndrawnVRAM(srcBasePtr, srcBytes); EmitCommandWithRAM(CommandType::TRANSFERSRC, Memory::GetPointerUnchecked(srcBasePtr), srcBytes, 16); DirtyVRAM(dstBasePtr, dstBytes, DirtyVRAMFlag::DIRTY); } @@ -566,6 +594,8 @@ void Recorder::EmitClut(u32 op) { if (bytes != 0) { // Send the original address so VRAM can be reasoned about. if (Memory::IsVRAMAddress(addr)) { + ReadbackUndrawnVRAM(addr, bytes); + struct ClutAddrData { u32 addr; u32 flags; diff --git a/GPU/Debugger/Record.h b/GPU/Debugger/Record.h index 3c291057af..f9fe1344f5 100644 --- a/GPU/Debugger/Record.h +++ b/GPU/Debugger/Record.h @@ -82,6 +82,7 @@ private: int CompareLastVRAM(u32 addr, u32 bytes) const; u32 GetTargetFlags(u32 addr, u32 sizeInRAM); + void ReadbackUndrawnVRAM(u32 addr, u32 bytes); void FlushPrimState(int vcount); void EmitTextureData(int level, u32 texaddr); @@ -104,6 +105,7 @@ private: std::vector lastRegisters; std::vector lastTextures; std::set lastRenderTargets; + std::set readbackAddrs; std::vector lastVRAM; DirtyVRAMFlag dirtyVRAM[DIRTY_VRAM_SIZE]; From 275fc530614111051a75896ad5d6c7ac3a95aafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 9 Oct 2026 03:36:37 +0200 Subject: [PATCH 2/2] GE dump recording: Record one more frame when the display shows the previous one Silent Hill: Origins shows each frame one late: the first draw of a frame copies the previous one into its 565 display buffer. A dump of one frame then displays a copy of the frame before it, taken from memory, and none of what it draws. When the display buffer's last write came before most of the frame's drawing, the recorder now records the next frame too, whose copy puts the recorded frame on the display. Co-Authored-By: Claude Opus 5.5 --- GPU/Debugger/Record.cpp | 72 ++++++++++++++++++++++++++++++++++++++--- GPU/Debugger/Record.h | 15 +++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/GPU/Debugger/Record.cpp b/GPU/Debugger/Record.cpp index 7768c0ff51..b0ab2efd1e 100644 --- a/GPU/Debugger/Record.cpp +++ b/GPU/Debugger/Record.cpp @@ -164,6 +164,9 @@ bool Recorder::BeginRecording() { lastTextures.clear(); lastRenderTargets.clear(); readbackAddrs.clear(); + targetWrites_.clear(); + writeCount_ = 0; + waitForDisplayCopy_ = false; flipLastAction = gpuStats.totals.numFlips; flipFinishAt = -1; @@ -568,6 +571,7 @@ void Recorder::EmitTransfer(u32 op) { u32 dstBytes = ((dstY + height - 1) * dstStride + (dstX + width)) * bpp; dstBytes = Memory::ClampValidSizeAt(dstBasePtr, dstBytes); + NoteTargetWrite(dstBasePtr, dstBytes); if (srcBytes != 0) { ReadbackUndrawnVRAM(srcBasePtr, srcBytes); @@ -616,7 +620,46 @@ void Recorder::EmitClut(u32 op) { lastRegisters.push_back(op); } +// Where the current draw writes color, as an offset in VRAM and a size. +static void CurrentTarget(u32 *addr, u32 *bytes) { + const int h = std::min(gstate.getScissorY2(), gstate.getRegionY2()) + 1; + const int w = std::min(gstate.getScissorX2(), gstate.getRegionX2()) + 1; + const int bpp = gstate.FrameBufFormat() == GE_FORMAT_8888 ? 4 : 2; + *addr = gstate.getFrameBufRawAddress() & 0x001FFFFF; + *bytes = bpp * (gstate.FrameBufStride() * (h - 1) + w); +} + +void Recorder::NoteTargetWrite(u32 addr, u32 bytes) { + addr &= 0x001FFFFF; + writeCount_++; + for (TargetWrite &t : targetWrites_) { + if (t.start == addr) { + t.end = std::max(t.end, addr + bytes); + t.last = writeCount_; + return; + } + } + targetWrites_.push_back(TargetWrite{ addr, addr + bytes, writeCount_ }); +} + +// Whether the last write to the display buffer came before most of the frame's drawing. Silent Hill: Origins +// shows each frame one late, by copying it into its 565 display buffer with the first draw of the next frame. +bool Recorder::DisplayDrawnEarly(u32 addr, u32 bytes) const { + addr &= 0x001FFFFF; + int last = -1; + for (const TargetWrite &t : targetWrites_) { + if (t.start < addr + bytes && addr < t.end) { + last = std::max(last, t.last); + } + } + return last >= 0 && writeCount_ - last > writeCount_ / 2; +} + void Recorder::EmitPrim(u32 op) { + u32 target, targetBytes; + CurrentTarget(&target, &targetBytes); + NoteTargetWrite(target, targetBytes); + FlushPrimState(op & 0x0000FFFF); lastRegisters.push_back(op); @@ -624,6 +667,10 @@ void Recorder::EmitPrim(u32 op) { } void Recorder::EmitBezierSpline(u32 op) { + u32 target, targetBytes; + CurrentTarget(&target, &targetBytes); + NoteTargetWrite(target, targetBytes); + int ucount = op & 0xFF; int vcount = (op >> 8) & 0xFF; FlushPrimState(ucount * vcount); @@ -831,6 +878,26 @@ void Recorder::NotifyDisplay(u32 framebuf, int stride, int fmt) { return; } + PushDisplay(framebuf, stride, fmt); + + if (writePending && !waitForDisplayCopy_) { + const int bpp = fmt == GE_FORMAT_8888 ? 4 : 2; + if (DisplayDrawnEarly(framebuf, bpp * stride * 272)) { + // What's displayed is a copy of the previous frame. Record the next frame too, which copies this one to + // the display (into the other half, in Silent Hill), so the replay shows what it drew. + NOTICE_LOG(Log::System, "Display buffer drawn early in the frame, recording one more frame"); + waitForDisplayCopy_ = true; + return; + } + } + + if (writePending) { + NOTICE_LOG(Log::System, "Recording complete on display"); + FinishRecording(); + } +} + +void Recorder::PushDisplay(u32 framebuf, int stride, int fmt) { CheckEdramTrans(); struct DisplayBufData { PSPPointer topaddr; @@ -844,11 +911,6 @@ void Recorder::NotifyDisplay(u32 framebuf, int stride, int fmt) { u32 ptr = AppendToPushbuf(pushbuf, &disp, sz); commands.push_back({ CommandType::DISPLAY, sz, ptr }); - - if (writePending) { - NOTICE_LOG(Log::System, "Recording complete on display"); - FinishRecording(); - } } void Recorder::NotifyBeginFrame() { diff --git a/GPU/Debugger/Record.h b/GPU/Debugger/Record.h index f9fe1344f5..78964859b0 100644 --- a/GPU/Debugger/Record.h +++ b/GPU/Debugger/Record.h @@ -83,6 +83,9 @@ private: u32 GetTargetFlags(u32 addr, u32 sizeInRAM); void ReadbackUndrawnVRAM(u32 addr, u32 bytes); + void NoteTargetWrite(u32 addr, u32 bytes); + bool DisplayDrawnEarly(u32 addr, u32 bytes) const; + void PushDisplay(u32 framebuf, int stride, int fmt); void FlushPrimState(int vcount); void EmitTextureData(int level, u32 texaddr); @@ -106,6 +109,18 @@ private: std::vector lastTextures; std::set lastRenderTargets; std::set readbackAddrs; + + // The VRAM each draw or transfer wrote (offsets in VRAM), and when, to see whether the display buffer only got + // an early copy (see NotifyDisplay). + struct TargetWrite { + u32 start; + u32 end; + int last; + }; + std::vector targetWrites_; + int writeCount_ = 0; + // Recording one more frame, whose start copies the recorded one to the display. + bool waitForDisplayCopy_ = false; std::vector lastVRAM; DirtyVRAMFlag dirtyVRAM[DIRTY_VRAM_SIZE];