Files
ppsspp/GPU/Debugger/Record.cpp
T
Henrik RydgårdandClaude Opus 5.5 275fc53061 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 <noreply@anthropic.com>
2026-10-12 02:04:25 +02:00

957 lines
27 KiB
C++

// Copyright (c) 2017- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <atomic>
#include <cstring>
#include <functional>
#include <set>
#include <vector>
#include <mutex>
#include <zstd.h>
#include "Common/CommonTypes.h"
#include "Common/File/FileUtil.h"
#include "Common/Thread/ThreadManager.h"
#include "Common/Thread/ParallelLoop.h"
#include "Common/Log.h"
#include "Common/StringUtils.h"
#include "Common/System/System.h"
#include "Core/Core.h"
#include "Core/ELF/ParamSFO.h"
#include "Core/HLE/sceDisplay.h"
#include "Core/MemMap.h"
#include "Core/System.h"
#include "GPU/GPUCommon.h"
#include "GPU/GPUCommon.h"
#include "GPU/GPUState.h"
#include "GPU/ge_constants.h"
#include "GPU/Common/TextureDecoder.h"
#include "GPU/Common/VertexDecoderCommon.h"
#include "GPU/Debugger/Record.h"
#include "GPU/Debugger/RecordFormat.h"
namespace GPURecord {
// Appends data (or zeroes, for a null p) at a 4-byte aligned offset, zero padded: players read register blocks and headers as
// words, which a PSP can't do unaligned.
static u32 AppendToPushbuf(std::vector<u8> &pushbuf, const void *p, u32 sz) {
const u32 ptr = ((u32)pushbuf.size() + 3) & ~3;
pushbuf.resize(ptr + sz, 0);
if (p && sz)
memcpy(pushbuf.data() + ptr, p, sz);
return ptr;
}
void Recorder::FlushRegisters() {
if (!lastRegisters.empty()) {
Command last{ CommandType::REGISTERS };
last.sz = (u32)(lastRegisters.size() * sizeof(u32));
last.ptr = AppendToPushbuf(pushbuf, lastRegisters.data(), last.sz);
lastRegisters.clear();
commands.push_back(last);
}
}
static Path GenRecordingFilename() {
const Path dumpDir = GetSysDirectory(DIRECTORY_DUMP);
File::CreateFullPath(dumpDir);
const std::string prefix = g_paramSFO.GetDiscID();
for (int n = 1; n < 10000; ++n) {
std::string filename = StringFromFormat("%s_%04d.ppdmp", prefix.c_str(), n);
const Path path = dumpDir / filename;
if (!File::Exists(path)) {
return path;
}
}
return dumpDir / StringFromFormat("%s_%04d.ppdmp", prefix.c_str(), 9999);
}
void Recorder::DirtyAllVRAM(DirtyVRAMFlag flag) {
if (flag == DirtyVRAMFlag::UNKNOWN) {
for (uint32_t i = 0; i < DIRTY_VRAM_SIZE; ++i) {
if (dirtyVRAM[i] == DirtyVRAMFlag::CLEAN)
dirtyVRAM[i] = DirtyVRAMFlag::UNKNOWN;
}
} else {
for (uint32_t i = 0; i < DIRTY_VRAM_SIZE; ++i)
dirtyVRAM[i] = flag;
}
}
void Recorder::DirtyVRAM(u32 start, u32 sz, DirtyVRAMFlag flag) {
if (sz == 0)
return;
// Every block the range touches, also when it starts partway into one. Only CLEAN (the dump has the data)
// leaves out partly covered blocks at the ends, since the rest of those isn't in the dump.
u32 first = (start >> DIRTY_VRAM_SHIFT) & DIRTY_VRAM_MASK;
u32 count = ((start & DIRTY_VRAM_ROUND) + sz + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT;
if (flag == DirtyVRAMFlag::CLEAN) {
if ((start & DIRTY_VRAM_ROUND) != 0) {
first++;
count--;
}
if (count > 0 && ((start + sz) & DIRTY_VRAM_ROUND) != 0)
count--;
// Past the end of VRAM, only what's inside is in the dump.
if (first + count > DIRTY_VRAM_SIZE)
count = first < DIRTY_VRAM_SIZE ? DIRTY_VRAM_SIZE - first : 0;
if (count == 0)
return;
}
if (first + count > DIRTY_VRAM_SIZE) {
DirtyAllVRAM(flag);
return;
}
for (u32 i = 0; i < count; ++i)
dirtyVRAM[first + i] = flag;
}
void Recorder::DirtyDrawnVRAM() {
int w = std::min(gstate.getScissorX2(), gstate.getRegionX2()) + 1;
int h = std::min(gstate.getScissorY2(), gstate.getRegionY2()) + 1;
bool drawZ = !gstate.isModeClear() && gstate.isDepthWriteEnabled() && gstate.isDepthTestEnabled();
bool clearZ = gstate.isModeClear() && gstate.isClearModeDepthMask();
if (drawZ || clearZ) {
int bytes = 2 * gstate.DepthBufStride() * h;
if (w > gstate.DepthBufStride())
bytes += 2 * (w - gstate.DepthBufStride());
DirtyVRAM(gstate.getDepthBufAddress(), bytes, DirtyVRAMFlag::DRAWN);
}
int bpp = gstate.FrameBufFormat() == GE_FORMAT_8888 ? 4 : 2;
int bytes = bpp * gstate.FrameBufStride() * h;
if (w > gstate.FrameBufStride())
bytes += bpp * (w - gstate.FrameBufStride());
DirtyVRAM(gstate.getFrameBufAddress(), bytes, DirtyVRAMFlag::DRAWN);
}
bool Recorder::BeginRecording() {
std::unique_lock<std::mutex> guard(callbackLock_);
nextFrame = false;
if (PSP_CoreParameter().fileType == IdentifiedFileType::PPSSPP_GE_DUMP) {
// Can't record a GE dump. RecordNextFrame refuses this too.
writeCallback = nullptr;
return false;
}
active = true;
guard.unlock();
lastTextures.clear();
lastRenderTargets.clear();
readbackAddrs.clear();
targetWrites_.clear();
writeCount_ = 0;
waitForDisplayCopy_ = false;
flipLastAction = gpuStats.totals.numFlips;
flipFinishAt = -1;
u32 sz = 512 * 4;
u32 ptr = AppendToPushbuf(pushbuf, nullptr, sz);
gstate.Save((u32_le *)(pushbuf.data() + ptr));
commands.push_back({ CommandType::INIT, sz, ptr });
lastVRAM.resize(2 * 1024 * 1024);
// Also save the initial CLUT.
GPUDebugBuffer clut;
if (gpu->GetCurrentClut(clut)) {
sz = clut.GetStride() * clut.PixelSize();
_assert_msg_(sz == 1024, "CLUT should be 1024 bytes");
ptr = AppendToPushbuf(pushbuf, clut.GetData(), sz);
commands.push_back({ CommandType::CLUT, sz, ptr });
}
DirtyAllVRAM(DirtyVRAMFlag::DIRTY);
return true;
}
static void WriteCompressed(FILE *fp, const void *p, size_t sz) {
size_t compressed_size = ZSTD_compressBound(sz);
u8 *compressed = new u8[compressed_size];
compressed_size = ZSTD_compress(compressed, compressed_size, p, sz, 6);
u32 write_size = (u32)compressed_size;
fwrite(&write_size, sizeof(write_size), 1, fp);
fwrite(compressed, compressed_size, 1, fp);
delete[] compressed;
}
Path Recorder::WriteRecording() {
FlushRegisters();
const Path filename = GenRecordingFilename();
NOTICE_LOG(Log::G3D, "Recording filename: %s", filename.c_str());
FILE *fp = File::OpenCFile(filename, "wb");
if (!fp) {
ERROR_LOG(Log::G3D, "Failed to open '%s' for writing the recording", filename.c_str());
return Path();
}
Header header{};
memcpy(header.magic, HEADER_MAGIC, sizeof(header.magic));
header.version = VERSION;
strncpy(header.gameID, g_paramSFO.GetDiscID().c_str(), sizeof(header.gameID));
fwrite(&header, sizeof(header), 1, fp);
u32 sz = (u32)commands.size();
fwrite(&sz, sizeof(sz), 1, fp);
u32 bufsz = (u32)pushbuf.size();
fwrite(&bufsz, sizeof(bufsz), 1, fp);
WriteCompressed(fp, commands.data(), commands.size() * sizeof(Command));
WriteCompressed(fp, pushbuf.data(), bufsz);
fclose(fp);
return filename;
}
static void GetVertDataSizes(int vcount, const void *indices, u32 &vbytes, u32 &ibytes) {
VertexDecoder vdec;
VertexDecoderOptions opts{};
vdec.SetVertexType(gstate.vertType, opts);
if (indices) {
u16 lower = 0;
u16 upper = 0;
GetIndexBounds(indices, vcount, gstate.vertType, &lower, &upper);
vbytes = (upper + 1) * vdec.VertexSize();
u32 idx = gstate.vertType & GE_VTYPE_IDX_MASK;
if (idx == GE_VTYPE_IDX_8BIT) {
ibytes = vcount * sizeof(u8);
} else if (idx == GE_VTYPE_IDX_16BIT) {
ibytes = vcount * sizeof(u16);
} else if (idx == GE_VTYPE_IDX_32BIT) {
ibytes = vcount * sizeof(u32);
}
} else {
vbytes = vcount * vdec.VertexSize();
}
}
static const u8 *mymemmem(const u8 *haystack, size_t off, size_t hlen, const u8 *needle, size_t nlen, uintptr_t align) {
if (!nlen) {
return nullptr;
}
const u8 *last_possible = haystack + hlen - nlen;
const u8 *first_possible = haystack + off;
int first = *needle;
const u8 *result = nullptr;
std::mutex resultLock;
int range = (int)(last_possible - first_possible);
ParallelRangeLoop(&g_threadManager, [&](int l, int h) {
const u8 *p = haystack + off + l;
const u8 *pend = haystack + off + h;
const uintptr_t align_mask = align - 1;
auto poffset = [&]() {
return ((uintptr_t)(p - haystack) & align_mask);
};
auto alignp = [&]() {
uintptr_t offset = poffset();
if (offset != 0)
p += align - offset;
};
alignp();
while (p <= pend) {
p = (const u8 *)memchr(p, first, pend - p + 1);
if (!p) {
return;
}
if (poffset() == 0 && !memcmp(p, needle, nlen)) {
std::lock_guard<std::mutex> guard(resultLock);
// Take the lowest result so we get the same file for any # of threads.
if (!result || p < result)
result = p;
return;
}
p++;
alignp();
}
}, 0, range, 128 * 1024, 8, TaskPriority::LOW);
return result;
}
Command Recorder::EmitCommandWithRAM(CommandType t, const void *p, u32 sz, u32 align) {
FlushRegisters();
Command cmd{ t, sz, 0 };
align = std::max(align, 4U);
if (sz) {
// If at all possible, try to find it already in the buffer.
const u8 *prev = nullptr;
const size_t NEAR_WINDOW = std::max((int)sz * 2, 1024 * 10);
// Let's try nearby first... it will often be nearby.
if (pushbuf.size() > NEAR_WINDOW) {
prev = mymemmem(pushbuf.data(), pushbuf.size() - NEAR_WINDOW, pushbuf.size(), (const u8 *)p, sz, align);
}
if (!prev) {
prev = mymemmem(pushbuf.data(), 0, pushbuf.size(), (const u8 *)p, sz, align);
}
if (prev) {
cmd.ptr = (u32)(prev - pushbuf.data());
} else {
cmd.ptr = (u32)pushbuf.size();
int pad = 0;
if (cmd.ptr & (align - 1)) {
pad = align - (cmd.ptr & (align - 1));
cmd.ptr += pad;
}
pushbuf.resize(pushbuf.size() + sz + pad);
if (pad) {
memset(pushbuf.data() + cmd.ptr - pad, 0, pad);
}
memcpy(pushbuf.data() + cmd.ptr, p, sz);
}
}
commands.push_back(cmd);
return cmd;
}
void Recorder::UpdateLastVRAM(u32 addr, u32 bytes) {
u32 base = addr & 0x001FFFFF;
if (base + bytes > 0x00200000) {
memcpy(&lastVRAM[base], Memory::GetPointerUnchecked(0x04000000 | base), 0x00200000 - base);
bytes = base + bytes - 0x00200000;
base = 0;
}
memcpy(&lastVRAM[base], Memory::GetPointerUnchecked(0x04000000 | base), bytes);
}
void Recorder::ClearLastVRAM(u32 addr, u8 c, u32 bytes) {
u32 base = addr & 0x001FFFFF;
if (base + bytes > 0x00200000) {
memset(&lastVRAM[base], c, 0x00200000 - base);
bytes = base + bytes - 0x00200000;
base = 0;
}
memset(&lastVRAM[base], c, bytes);
}
int Recorder::CompareLastVRAM(u32 addr, u32 bytes) const {
u32 base = addr & 0x001FFFFF;
if (base + bytes > 0x00200000) {
int result = memcmp(&lastVRAM[base], Memory::GetPointerUnchecked(0x04000000 | base), 0x00200000 - base);
if (result != 0)
return result;
bytes = base + bytes - 0x00200000;
base = 0;
}
return memcmp(&lastVRAM[base], Memory::GetPointerUnchecked(0x04000000 | base), bytes);
}
u32 Recorder::GetTargetFlags(u32 addr, u32 sizeInRAM) {
addr &= 0x041FFFFF;
const bool isTarget = lastRenderTargets.find(addr) != lastRenderTargets.end();
bool isUnknownVRAM = false;
bool isDirtyVRAM = false;
bool isDrawnVRAM = false;
uint32_t start = (addr >> DIRTY_VRAM_SHIFT) & DIRTY_VRAM_MASK;
// Every block the range touches, also when it starts partway into one.
uint32_t blocks = sizeInRAM == 0 ? 0 : ((addr & DIRTY_VRAM_ROUND) + sizeInRAM + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT;
if (start + blocks >= DIRTY_VRAM_SIZE)
return 0;
bool startEven = (addr & DIRTY_VRAM_ROUND) == 0;
bool endEven = ((addr + sizeInRAM) & DIRTY_VRAM_ROUND) == 0;
for (uint32_t i = 0; i < blocks; ++i) {
DirtyVRAMFlag flag = dirtyVRAM[start + i];
isUnknownVRAM = (isUnknownVRAM || flag == DirtyVRAMFlag::UNKNOWN) && flag != DirtyVRAMFlag::DIRTY && flag != DirtyVRAMFlag::DRAWN;
isDirtyVRAM = isDirtyVRAM || flag != DirtyVRAMFlag::CLEAN;
isDrawnVRAM = isDrawnVRAM || flag == DirtyVRAMFlag::DRAWN;
// Mark the VRAM clean now that it's been copied to the dump, except partly covered blocks at the ends.
if (flag == DirtyVRAMFlag::UNKNOWN || flag == DirtyVRAMFlag::DIRTY) {
if ((i > 0 || startEven) && (i + 1 < blocks || endEven))
dirtyVRAM[start + i] = DirtyVRAMFlag::CLEAN;
}
}
if (isUnknownVRAM && isDirtyVRAM) {
// This means it's only UNKNOWN/CLEAN and not known to be actually dirty.
// Let's check our shadow copy of what we last sent for this VRAM.
int diff = CompareLastVRAM(addr, sizeInRAM);
if (diff == 0)
isDirtyVRAM = false;
}
// The isTarget flag is mostly used for replay of dumps on a PSP.
u32 flags = isTarget ? 1 : 0;
// The unchangedVRAM flag tells us we can skip recopying.
if (!isDirtyVRAM)
flags |= 2;
// And the drawn flag tells us this data was potentially drawn to.
if (isDrawnVRAM)
flags |= 4;
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);
int h = gstate.getTextureHeight(level);
int bufw = GetTextureBufw(level, texaddr, format);
int extraw = w > bufw ? w - bufw : 0;
u32 sizeInRAM = (textureBitsPerPixel[format] * (bufw * h + extraw)) / 8;
CommandType type = CommandType((int)CommandType::TEXTURE0 + level);
const u8 *p = Memory::GetPointerUnchecked(texaddr);
u32 bytes = Memory::ClampValidSizeAt(texaddr, sizeInRAM);
std::vector<u8> framebufData;
if (Memory::IsVRAMAddress(texaddr)) {
ReadbackUndrawnVRAM(texaddr, bytes);
struct FramebufData {
u32 addr;
int bufw;
u32 flags;
u32 pad;
};
u32 flags = GetTargetFlags(texaddr, bytes);
FramebufData framebuf{ texaddr, bufw, flags };
framebufData.resize(sizeof(framebuf) + bytes);
memcpy(&framebufData[0], &framebuf, sizeof(framebuf));
memcpy(&framebufData[sizeof(framebuf)], p, bytes);
p = &framebufData[0];
if ((flags & 2) == 0)
UpdateLastVRAM(texaddr, bytes);
// Okay, now we'll just emit this instead.
type = CommandType((int)CommandType::FRAMEBUF0 + level);
bytes += (u32)sizeof(framebuf);
}
if (bytes > 0) {
FlushRegisters();
// Dumps are huge - let's try to find this already emitted.
for (u32 prevptr : lastTextures) {
if (pushbuf.size() < prevptr + bytes) {
continue;
}
if (memcmp(pushbuf.data() + prevptr, p, bytes) == 0) {
commands.push_back({ type, bytes, prevptr });
// Okay, that was easy. Bail out.
return;
}
}
// Not there, gotta emit anew.
Command cmd = EmitCommandWithRAM(type, p, bytes, 16);
lastTextures.push_back(cmd.ptr);
}
}
void Recorder::FlushPrimState(int vcount) {
// TODO: Eventually, how do we handle texturing from framebuf/zbuf?
// TODO: Do we need to preload color/depth/stencil (in case from last frame)?
lastRenderTargets.insert(PSP_GetVidMemBase() | gstate.getFrameBufRawAddress());
lastRenderTargets.insert(PSP_GetVidMemBase() | gstate.getDepthBufRawAddress());
// We re-flush textures always in case the game changed them... kinda expensive.
bool textureEnabled = gstate.isTextureMapEnabled() || gstate.isAntiAliasEnabled();
// Play it safe and allow texture coords to emit data too.
bool textureCoords = (gstate.vertType & GE_VTYPE_TC_MASK) != 0;
for (int level = 0; level < 8; ++level) {
u32 texaddr = gstate.getTextureAddress(level);
if (texaddr && (textureEnabled || textureCoords)) {
EmitTextureData(level, texaddr);
}
}
const void *verts = Memory::GetPointerOrException(gstate_c.vertexAddr);
const void *indices = nullptr;
if ((gstate.vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) {
indices = Memory::GetPointerOrException(gstate_c.indexAddr);
}
u32 ibytes = 0;
u32 vbytes = 0;
GetVertDataSizes(vcount, indices, vbytes, ibytes);
if (indices && ibytes > 0) {
EmitCommandWithRAM(CommandType::INDICES, indices, ibytes, 4);
}
if (verts && vbytes > 0) {
EmitCommandWithRAM(CommandType::VERTICES, verts, vbytes, 4);
}
}
void Recorder::EmitTransfer(u32 op) {
FlushRegisters();
// This may not make a lot of sense right now, unless it's to a framebuf...
u32 dstBasePtr = gstate.getTransferDstAddress();
if (!Memory::IsVRAMAddress(dstBasePtr)) {
// Skip, not VRAM, so can't affect drawing (we flush textures each prim.)
return;
}
u32 srcBasePtr = gstate.getTransferSrcAddress();
u32 srcStride = gstate.getTransferSrcStride();
int srcX = gstate.getTransferSrcX();
int srcY = gstate.getTransferSrcY();
u32 dstStride = gstate.getTransferDstStride();
int dstX = gstate.getTransferDstX();
int dstY = gstate.getTransferDstY();
int width = gstate.getTransferWidth();
int height = gstate.getTransferHeight();
int bpp = gstate.getTransferBpp();
u32 srcBytes = ((srcY + height - 1) * srcStride + (srcX + width)) * bpp;
srcBytes = Memory::ClampValidSizeAt(srcBasePtr, srcBytes);
u32 dstBytes = ((dstY + height - 1) * dstStride + (dstX + width)) * bpp;
dstBytes = Memory::ClampValidSizeAt(dstBasePtr, dstBytes);
NoteTargetWrite(dstBasePtr, dstBytes);
if (srcBytes != 0) {
ReadbackUndrawnVRAM(srcBasePtr, srcBytes);
EmitCommandWithRAM(CommandType::TRANSFERSRC, Memory::GetPointerUnchecked(srcBasePtr), srcBytes, 16);
DirtyVRAM(dstBasePtr, dstBytes, DirtyVRAMFlag::DIRTY);
}
lastRegisters.push_back(op);
}
void Recorder::EmitClut(u32 op) {
u32 addr = gstate.getClutAddress();
// Hardware rendering may be using a framebuffer as CLUT.
// To get at this, we first run the command (normally we're called right before it has run.)
if (Memory::IsVRAMAddress(addr))
gpu->SetCmdValue(op);
// Actually should only be 0x3F, but we allow enhanced CLUTs. See #15727.
u32 blocks = (op & 0x7F) == 0x40 ? 0x40 : (op & 0x3F);
u32 bytes = blocks * 32;
bytes = Memory::ClampValidSizeAt(addr, bytes);
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;
};
u32 flags = GetTargetFlags(addr, bytes);
ClutAddrData data{ addr, flags };
FlushRegisters();
Command cmd{ CommandType::CLUTADDR, sizeof(data), AppendToPushbuf(pushbuf, &data, sizeof(data)) };
commands.push_back(cmd);
if ((flags & 2) == 0)
UpdateLastVRAM(addr, bytes);
}
EmitCommandWithRAM(CommandType::CLUT, Memory::GetPointerUnchecked(addr), bytes, 16);
}
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);
DirtyDrawnVRAM();
}
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);
lastRegisters.push_back(op);
DirtyDrawnVRAM();
}
bool Recorder::RecordNextFrame(const std::function<void(const Path &)> callback) {
if (PSP_CoreParameter().fileType == IdentifiedFileType::PPSSPP_GE_DUMP) {
return false;
}
std::lock_guard<std::mutex> guard(callbackLock_);
// Don't take over a recording in progress, it would get the wrong callback and end point.
if (nextFrame || active) {
return false;
}
flipLastAction = gpuStats.totals.numFlips;
flipFinishAt = -1;
writeCallback = callback;
nextFrame = true;
return true;
}
void Recorder::FinishRecording() {
// We're done - this was just to write the result out.
if (!active) {
return;
}
Path filename = WriteRecording();
commands.clear();
pushbuf.clear();
lastVRAM.clear();
NOTICE_LOG(Log::System, "Recording finished");
flipLastAction = gpuStats.totals.numFlips;
flipFinishAt = -1;
lastEdramTrans = 0x400;
std::function<void(const Path &)> callback;
{
std::lock_guard<std::mutex> guard(callbackLock_);
callback = std::move(writeCallback);
writeCallback = nullptr;
active = false;
}
if (callback && !filename.empty()) {
callback(filename);
}
}
void Recorder::CheckEdramTrans() {
if (!gpu)
return;
uint32_t value = gpu->GetAddrTranslation();
if (value == lastEdramTrans)
return;
lastEdramTrans = value;
FlushRegisters();
Command cmd{ CommandType::EDRAMTRANS, sizeof(value), AppendToPushbuf(pushbuf, &value, sizeof(value)) };
commands.push_back(cmd);
}
void Recorder::NotifyCommand(u32 pc) {
if (!active) {
return;
}
if (!Memory::IsValid4AlignedAddress(pc)) {
ERROR_LOG(Log::G3D, "Bad pc in Recorder: %08x", pc);
return;
}
CheckEdramTrans();
const u32 op = Memory::ReadUnchecked_U32(pc);
const GECommand cmd = GECommand(op >> 24);
switch (cmd) {
case GE_CMD_VADDR:
case GE_CMD_IADDR:
case GE_CMD_JUMP:
case GE_CMD_CALL:
case GE_CMD_RET:
case GE_CMD_END:
case GE_CMD_SIGNAL:
case GE_CMD_FINISH:
case GE_CMD_BASE:
case GE_CMD_OFFSETADDR:
case GE_CMD_ORIGIN:
// These just prepare future commands, and are flushed with those commands.
// TODO: Maybe add a command just to log that these were hit?
break;
case GE_CMD_BOUNDINGBOX:
case GE_CMD_BJUMP:
// Since we record each command, this is theoretically not relevant.
// TODO: Output a CommandType to validate this.
break;
case GE_CMD_PRIM:
EmitPrim(op);
break;
case GE_CMD_BEZIER:
case GE_CMD_SPLINE:
EmitBezierSpline(op);
break;
case GE_CMD_LOADCLUT:
EmitClut(op);
break;
case GE_CMD_TRANSFERSTART:
EmitTransfer(op);
break;
default:
lastRegisters.push_back(op);
break;
}
}
void Recorder::NotifyMemcpy(u32 dest, u32 src, u32 sz) {
if (!active) {
return;
}
CheckEdramTrans();
if (Memory::IsVRAMAddress(dest)) {
FlushRegisters();
Command cmd{ CommandType::MEMCPYDEST, sizeof(dest), AppendToPushbuf(pushbuf, &dest, sizeof(dest)) };
commands.push_back(cmd);
sz = Memory::ClampValidSizeAt(dest, sz);
if (sz != 0) {
EmitCommandWithRAM(CommandType::MEMCPYDATA, Memory::GetPointerUnchecked(dest), sz, 1);
UpdateLastVRAM(dest, sz);
DirtyVRAM(dest, sz, DirtyVRAMFlag::CLEAN);
}
}
}
void Recorder::NotifyMemset(u32 dest, int v, u32 sz) {
if (!active) {
return;
}
CheckEdramTrans();
struct MemsetCommand {
u32 dest;
int value;
u32 sz;
};
if (Memory::IsVRAMAddress(dest)) {
sz = Memory::ClampValidSizeAt(dest, sz);
MemsetCommand data{ dest, v, sz };
FlushRegisters();
Command cmd{ CommandType::MEMSET, sizeof(data), AppendToPushbuf(pushbuf, &data, sizeof(data)) };
commands.push_back(cmd);
ClearLastVRAM(dest, v, sz);
DirtyVRAM(dest, sz, DirtyVRAMFlag::CLEAN);
}
}
void Recorder::NotifyUpload(u32 dest, u32 sz) {
// This also checks the edram translation value and dirties VRAM.
NotifyMemcpy(dest, dest, sz);
}
bool Recorder::HasDrawCommands() const {
if (commands.empty())
return false;
for (const Command &cmd : commands) {
switch (cmd.type) {
case CommandType::INIT:
case CommandType::DISPLAY:
continue;
default:
return true;
}
}
// Only init and display commands, keep going.
return false;
}
void Recorder::NotifyDisplay(u32 framebuf, int stride, int fmt) {
bool writePending = false;
if (active && HasDrawCommands()) {
writePending = true;
}
if (!active && nextFrame && (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) == 0) {
NOTICE_LOG(Log::System, "Recording starting on display...");
BeginRecording(); // TODO: Handle return value.
}
if (!active) {
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<u8> topaddr;
int linesize, pixelFormat;
};
DisplayBufData disp{ { framebuf }, stride, fmt };
FlushRegisters();
u32 sz = (u32)sizeof(disp);
u32 ptr = AppendToPushbuf(pushbuf, &disp, sz);
commands.push_back({ CommandType::DISPLAY, sz, ptr });
}
void Recorder::NotifyBeginFrame() {
const bool noDisplayAction = flipLastAction + 4 < gpuStats.totals.numFlips;
// We do this only to catch things that don't call NotifyDisplay.
if (active && HasDrawCommands() && (noDisplayAction || gpuStats.totals.numFlips == flipFinishAt)) {
NOTICE_LOG(Log::System, "Recording complete on frame");
CheckEdramTrans();
struct DisplayBufData {
PSPPointer<u8> topaddr;
u32 linesize;
GEBufferFormat pixelFormat;
};
DisplayBufData disp;
__DisplayGetFramebuf(&disp.topaddr, &disp.linesize, &disp.pixelFormat, 0);
FlushRegisters();
u32 sz = (u32)sizeof(disp);
u32 ptr = AppendToPushbuf(pushbuf, &disp, sz);
commands.push_back({ CommandType::DISPLAY, sz, ptr });
FinishRecording();
}
if (!active && nextFrame && (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) == 0 && noDisplayAction) {
NOTICE_LOG(Log::System, "Recording starting on frame...");
BeginRecording();
// If we began on a BeginFrame, end on a BeginFrame.
flipFinishAt = gpuStats.totals.numFlips + 1;
}
}
void Recorder::NotifyCPU() {
if (!active) {
return;
}
DirtyAllVRAM(DirtyVRAMFlag::UNKNOWN);
}
} // namespace GPURecord