CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
Path: blob/master/GPU/Debugger/Record.cpp
Views: 1401
// Copyright (c) 2017- PPSSPP Project.12// This program is free software: you can redistribute it and/or modify3// it under the terms of the GNU General Public License as published by4// the Free Software Foundation, version 2.0 or later versions.56// This program is distributed in the hope that it will be useful,7// but WITHOUT ANY WARRANTY; without even the implied warranty of8// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the9// GNU General Public License 2.0 for more details.1011// A copy of the GPL 2.0 should have been included with the program.12// If not, see http://www.gnu.org/licenses/1314// Official git repository and contact information can be found at15// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.1617#include <algorithm>18#include <atomic>19#include <cstring>20#include <functional>21#include <set>22#include <vector>23#include <mutex>24#include <zstd.h>2526#include "Common/CommonTypes.h"27#include "Common/File/FileUtil.h"28#include "Common/Thread/ParallelLoop.h"29#include "Common/Log.h"30#include "Common/StringUtils.h"31#include "Common/System/System.h"3233#include "Core/Core.h"34#include "Core/ELF/ParamSFO.h"35#include "Core/HLE/sceDisplay.h"36#include "Core/MemMap.h"37#include "Core/System.h"38#include "Core/ThreadPools.h"39#include "GPU/Common/GPUDebugInterface.h"40#include "GPU/GPUInterface.h"41#include "GPU/GPUState.h"42#include "GPU/ge_constants.h"43#include "GPU/Common/TextureDecoder.h"44#include "GPU/Common/VertexDecoderCommon.h"45#include "GPU/Debugger/Record.h"46#include "GPU/Debugger/RecordFormat.h"4748namespace GPURecord {4950static bool active = false;51static std::atomic<bool> nextFrame = false;52static int flipLastAction = -1;53static int flipFinishAt = -1;54static uint32_t lastEdramTrans = 0x400;55static std::function<void(const Path &)> writeCallback;5657static std::vector<u8> pushbuf;58static std::vector<Command> commands;59static std::vector<u32> lastRegisters;60static std::vector<u32> lastTextures;61static std::set<u32> lastRenderTargets;62static std::vector<u8> lastVRAM;6364enum class DirtyVRAMFlag : uint8_t {65CLEAN = 0,66UNKNOWN = 1,67DIRTY = 2,68DRAWN = 3,69};70static constexpr uint32_t DIRTY_VRAM_SHIFT = 8;71static constexpr uint32_t DIRTY_VRAM_ROUND = (1 << DIRTY_VRAM_SHIFT) - 1;72static constexpr uint32_t DIRTY_VRAM_SIZE = (2 * 1024 * 1024) >> DIRTY_VRAM_SHIFT;73static constexpr uint32_t DIRTY_VRAM_MASK = (2 * 1024 * 1024 - 1) >> DIRTY_VRAM_SHIFT;74static DirtyVRAMFlag dirtyVRAM[DIRTY_VRAM_SIZE];7576static void FlushRegisters() {77if (!lastRegisters.empty()) {78Command last{CommandType::REGISTERS};79last.ptr = (u32)pushbuf.size();80last.sz = (u32)(lastRegisters.size() * sizeof(u32));81pushbuf.resize(pushbuf.size() + last.sz);82memcpy(pushbuf.data() + last.ptr, lastRegisters.data(), last.sz);83lastRegisters.clear();8485commands.push_back(last);86}87}8889static Path GenRecordingFilename() {90const Path dumpDir = GetSysDirectory(DIRECTORY_DUMP);9192File::CreateFullPath(dumpDir);9394const std::string prefix = g_paramSFO.GetDiscID();9596for (int n = 1; n < 10000; ++n) {97std::string filename = StringFromFormat("%s_%04d.ppdmp", prefix.c_str(), n);9899const Path path = dumpDir / filename;100101if (!File::Exists(path)) {102return path;103}104}105106return dumpDir / StringFromFormat("%s_%04d.ppdmp", prefix.c_str(), 9999);107}108109static void DirtyAllVRAM(DirtyVRAMFlag flag) {110if (flag == DirtyVRAMFlag::UNKNOWN) {111for (uint32_t i = 0; i < DIRTY_VRAM_SIZE; ++i) {112if (dirtyVRAM[i] == DirtyVRAMFlag::CLEAN)113dirtyVRAM[i] = DirtyVRAMFlag::UNKNOWN;114}115} else {116for (uint32_t i = 0; i < DIRTY_VRAM_SIZE; ++i)117dirtyVRAM[i] = flag;118}119}120121static void DirtyVRAM(u32 start, u32 sz, DirtyVRAMFlag flag) {122u32 count = (sz + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT;123u32 first = (start >> DIRTY_VRAM_SHIFT) & DIRTY_VRAM_MASK;124if (first + count > DIRTY_VRAM_SIZE) {125DirtyAllVRAM(flag);126return;127}128129for (u32 i = 0; i < count; ++i)130dirtyVRAM[first + i] = flag;131}132133static void DirtyDrawnVRAM() {134int w = std::min(gstate.getScissorX2(), gstate.getRegionX2()) + 1;135int h = std::min(gstate.getScissorY2(), gstate.getRegionY2()) + 1;136137bool drawZ = !gstate.isModeClear() && gstate.isDepthWriteEnabled() && gstate.isDepthTestEnabled();138bool clearZ = gstate.isModeClear() && gstate.isClearModeDepthMask();139if (drawZ || clearZ) {140int bytes = 2 * gstate.DepthBufStride() * h;141if (w > gstate.DepthBufStride())142bytes += 2 * (w - gstate.DepthBufStride());143DirtyVRAM(gstate.getDepthBufAddress(), bytes, DirtyVRAMFlag::DRAWN);144}145146int bpp = gstate.FrameBufFormat() == GE_FORMAT_8888 ? 4 : 2;147int bytes = bpp * gstate.FrameBufStride() * h;148if (w > gstate.FrameBufStride())149bytes += bpp * (w - gstate.FrameBufStride());150DirtyVRAM(gstate.getFrameBufAddress(), bytes, DirtyVRAMFlag::DRAWN);151}152153static void BeginRecording() {154active = true;155nextFrame = false;156lastTextures.clear();157lastRenderTargets.clear();158flipLastAction = gpuStats.numFlips;159flipFinishAt = -1;160161u32 ptr = (u32)pushbuf.size();162u32 sz = 512 * 4;163pushbuf.resize(pushbuf.size() + sz);164gstate.Save((u32_le *)(pushbuf.data() + ptr));165commands.push_back({CommandType::INIT, sz, ptr});166lastVRAM.resize(2 * 1024 * 1024);167168// Also save the initial CLUT.169GPUDebugBuffer clut;170if (gpuDebug->GetCurrentClut(clut)) {171sz = clut.GetStride() * clut.PixelSize();172_assert_msg_(sz == 1024, "CLUT should be 1024 bytes");173ptr = (u32)pushbuf.size();174pushbuf.resize(pushbuf.size() + sz);175memcpy(pushbuf.data() + ptr, clut.GetData(), sz);176commands.push_back({ CommandType::CLUT, sz, ptr });177}178179DirtyAllVRAM(DirtyVRAMFlag::DIRTY);180}181182static void WriteCompressed(FILE *fp, const void *p, size_t sz) {183size_t compressed_size = ZSTD_compressBound(sz);184u8 *compressed = new u8[compressed_size];185compressed_size = ZSTD_compress(compressed, compressed_size, p, sz, 6);186187u32 write_size = (u32)compressed_size;188fwrite(&write_size, sizeof(write_size), 1, fp);189fwrite(compressed, compressed_size, 1, fp);190191delete [] compressed;192}193194static Path WriteRecording() {195FlushRegisters();196197const Path filename = GenRecordingFilename();198199NOTICE_LOG(Log::G3D, "Recording filename: %s", filename.c_str());200201FILE *fp = File::OpenCFile(filename, "wb");202Header header{};203strncpy(header.magic, HEADER_MAGIC, sizeof(header.magic));204header.version = VERSION;205strncpy(header.gameID, g_paramSFO.GetDiscID().c_str(), sizeof(header.gameID));206fwrite(&header, sizeof(header), 1, fp);207208u32 sz = (u32)commands.size();209fwrite(&sz, sizeof(sz), 1, fp);210u32 bufsz = (u32)pushbuf.size();211fwrite(&bufsz, sizeof(bufsz), 1, fp);212213WriteCompressed(fp, commands.data(), commands.size() * sizeof(Command));214WriteCompressed(fp, pushbuf.data(), bufsz);215216fclose(fp);217218return filename;219}220221static void GetVertDataSizes(int vcount, const void *indices, u32 &vbytes, u32 &ibytes) {222VertexDecoder vdec;223VertexDecoderOptions opts{};224vdec.SetVertexType(gstate.vertType, opts);225226if (indices) {227u16 lower = 0;228u16 upper = 0;229GetIndexBounds(indices, vcount, gstate.vertType, &lower, &upper);230231vbytes = (upper + 1) * vdec.VertexSize();232u32 idx = gstate.vertType & GE_VTYPE_IDX_MASK;233if (idx == GE_VTYPE_IDX_8BIT) {234ibytes = vcount * sizeof(u8);235} else if (idx == GE_VTYPE_IDX_16BIT) {236ibytes = vcount * sizeof(u16);237} else if (idx == GE_VTYPE_IDX_32BIT) {238ibytes = vcount * sizeof(u32);239}240} else {241vbytes = vcount * vdec.VertexSize();242}243}244245static const u8 *mymemmem(const u8 *haystack, size_t off, size_t hlen, const u8 *needle, size_t nlen, uintptr_t align) {246if (!nlen) {247return nullptr;248}249250const u8 *last_possible = haystack + hlen - nlen;251const u8 *first_possible = haystack + off;252int first = *needle;253254const u8 *result = nullptr;255std::mutex resultLock;256257int range = (int)(last_possible - first_possible);258ParallelRangeLoop(&g_threadManager, [&](int l, int h) {259const u8 *p = haystack + off + l;260const u8 *pend = haystack + off + h;261262const uintptr_t align_mask = align - 1;263auto poffset = [&]() {264return ((uintptr_t)(p - haystack) & align_mask);265};266auto alignp = [&]() {267uintptr_t offset = poffset();268if (offset != 0)269p += align - offset;270};271272alignp();273while (p <= pend) {274p = (const u8 *)memchr(p, first, pend - p + 1);275if (!p) {276return;277}278if (poffset() == 0 && !memcmp(p, needle, nlen)) {279std::lock_guard<std::mutex> guard(resultLock);280// Take the lowest result so we get the same file for any # of threads.281if (!result || p < result)282result = p;283return;284}285286p++;287alignp();288}289}, 0, range, 128 * 1024, TaskPriority::LOW);290291return result;292}293294static Command EmitCommandWithRAM(CommandType t, const void *p, u32 sz, u32 align) {295FlushRegisters();296297Command cmd{t, sz, 0};298299if (sz) {300// If at all possible, try to find it already in the buffer.301const u8 *prev = nullptr;302const size_t NEAR_WINDOW = std::max((int)sz * 2, 1024 * 10);303// Let's try nearby first... it will often be nearby.304if (pushbuf.size() > NEAR_WINDOW) {305prev = mymemmem(pushbuf.data(), pushbuf.size() - NEAR_WINDOW, pushbuf.size(), (const u8 *)p, sz, align);306}307if (!prev) {308prev = mymemmem(pushbuf.data(), 0, pushbuf.size(), (const u8 *)p, sz, align);309}310311if (prev) {312cmd.ptr = (u32)(prev - pushbuf.data());313} else {314cmd.ptr = (u32)pushbuf.size();315int pad = 0;316if (cmd.ptr & (align - 1)) {317pad = align - (cmd.ptr & (align - 1));318cmd.ptr += pad;319}320pushbuf.resize(pushbuf.size() + sz + pad);321if (pad) {322memset(pushbuf.data() + cmd.ptr - pad, 0, pad);323}324memcpy(pushbuf.data() + cmd.ptr, p, sz);325}326}327328commands.push_back(cmd);329330return cmd;331}332333static void UpdateLastVRAM(u32 addr, u32 bytes) {334u32 base = addr & 0x001FFFFF;335if (base + bytes > 0x00200000) {336memcpy(&lastVRAM[base], Memory::GetPointerUnchecked(0x04000000 | base), 0x00200000 - base);337bytes = base + bytes - 0x00200000;338base = 0;339}340memcpy(&lastVRAM[base], Memory::GetPointerUnchecked(0x04000000 | base), bytes);341}342343static void ClearLastVRAM(u32 addr, u8 c, u32 bytes) {344u32 base = addr & 0x001FFFFF;345if (base + bytes > 0x00200000) {346memset(&lastVRAM[base], c, 0x00200000 - base);347bytes = base + bytes - 0x00200000;348base = 0;349}350memset(&lastVRAM[base], c, bytes);351}352353static int CompareLastVRAM(u32 addr, u32 bytes) {354u32 base = addr & 0x001FFFFF;355if (base + bytes > 0x00200000) {356int result = memcmp(&lastVRAM[base], Memory::GetPointerUnchecked(0x04000000 | base), 0x00200000 - base);357if (result != 0)358return result;359360bytes = base + bytes - 0x00200000;361base = 0;362}363return memcmp(&lastVRAM[base], Memory::GetPointerUnchecked(0x04000000 | base), bytes);364}365366static u32 GetTargetFlags(u32 addr, u32 sizeInRAM) {367addr &= 0x041FFFFF;368const bool isTarget = lastRenderTargets.find(addr) != lastRenderTargets.end();369370bool isUnknownVRAM = false;371bool isDirtyVRAM = false;372bool isDrawnVRAM = false;373uint32_t start = (addr >> DIRTY_VRAM_SHIFT) & DIRTY_VRAM_MASK;374uint32_t blocks = (sizeInRAM + DIRTY_VRAM_ROUND) >> DIRTY_VRAM_SHIFT;375if (start + blocks >= DIRTY_VRAM_SIZE)376return 0;377bool startEven = (addr & DIRTY_VRAM_ROUND) == 0;378bool endEven = ((addr + sizeInRAM) & DIRTY_VRAM_ROUND) == 0;379for (uint32_t i = 0; i < blocks; ++i) {380DirtyVRAMFlag flag = dirtyVRAM[start + i];381isUnknownVRAM = (isUnknownVRAM || flag == DirtyVRAMFlag::UNKNOWN) && flag != DirtyVRAMFlag::DIRTY && flag != DirtyVRAMFlag::DRAWN;382isDirtyVRAM = isDirtyVRAM || flag != DirtyVRAMFlag::CLEAN;383isDrawnVRAM = isDrawnVRAM || flag == DirtyVRAMFlag::DRAWN;384385// Mark the VRAM clean now that it's been copied to VRAM.386if (flag == DirtyVRAMFlag::UNKNOWN || flag == DirtyVRAMFlag::DIRTY) {387if ((i > 0 || startEven) && (i < blocks || endEven))388dirtyVRAM[start + i] = DirtyVRAMFlag::CLEAN;389}390}391392if (isUnknownVRAM && isDirtyVRAM) {393// This means it's only UNKNOWN/CLEAN and not known to be actually dirty.394// Let's check our shadow copy of what we last sent for this VRAM.395int diff = CompareLastVRAM(addr, sizeInRAM);396if (diff == 0)397isDirtyVRAM = false;398}399400// The isTarget flag is mostly used for replay of dumps on a PSP.401u32 flags = isTarget ? 1 : 0;402// The unchangedVRAM flag tells us we can skip recopying.403if (!isDirtyVRAM)404flags |= 2;405// And the drawn flag tells us this data was potentially drawn to.406if (isDrawnVRAM)407flags |= 4;408409return flags;410}411412static void EmitTextureData(int level, u32 texaddr) {413GETextureFormat format = gstate.getTextureFormat();414int w = gstate.getTextureWidth(level);415int h = gstate.getTextureHeight(level);416int bufw = GetTextureBufw(level, texaddr, format);417int extraw = w > bufw ? w - bufw : 0;418u32 sizeInRAM = (textureBitsPerPixel[format] * (bufw * h + extraw)) / 8;419420CommandType type = CommandType((int)CommandType::TEXTURE0 + level);421const u8 *p = Memory::GetPointerUnchecked(texaddr);422u32 bytes = Memory::ValidSize(texaddr, sizeInRAM);423std::vector<u8> framebufData;424425if (Memory::IsVRAMAddress(texaddr)) {426struct FramebufData {427u32 addr;428int bufw;429u32 flags;430u32 pad;431};432433u32 flags = GetTargetFlags(texaddr, bytes);434FramebufData framebuf{ texaddr, bufw, flags };435framebufData.resize(sizeof(framebuf) + bytes);436memcpy(&framebufData[0], &framebuf, sizeof(framebuf));437memcpy(&framebufData[sizeof(framebuf)], p, bytes);438p = &framebufData[0];439440if ((flags & 2) == 0)441UpdateLastVRAM(texaddr, bytes);442443// Okay, now we'll just emit this instead.444type = CommandType((int)CommandType::FRAMEBUF0 + level);445bytes += (u32)sizeof(framebuf);446}447448if (bytes > 0) {449FlushRegisters();450451// Dumps are huge - let's try to find this already emitted.452for (u32 prevptr : lastTextures) {453if (pushbuf.size() < prevptr + bytes) {454continue;455}456457if (memcmp(pushbuf.data() + prevptr, p, bytes) == 0) {458commands.push_back({type, bytes, prevptr});459// Okay, that was easy. Bail out.460return;461}462}463464// Not there, gotta emit anew.465Command cmd = EmitCommandWithRAM(type, p, bytes, 16);466lastTextures.push_back(cmd.ptr);467}468}469470static void FlushPrimState(int vcount) {471// TODO: Eventually, how do we handle texturing from framebuf/zbuf?472// TODO: Do we need to preload color/depth/stencil (in case from last frame)?473474lastRenderTargets.insert(PSP_GetVidMemBase() | gstate.getFrameBufRawAddress());475lastRenderTargets.insert(PSP_GetVidMemBase() | gstate.getDepthBufRawAddress());476477// We re-flush textures always in case the game changed them... kinda expensive.478bool textureEnabled = gstate.isTextureMapEnabled() || gstate.isAntiAliasEnabled();479// Play it safe and allow texture coords to emit data too.480bool textureCoords = (gstate.vertType & GE_VTYPE_TC_MASK) != 0;481for (int level = 0; level < 8; ++level) {482u32 texaddr = gstate.getTextureAddress(level);483if (texaddr && (textureEnabled || textureCoords)) {484EmitTextureData(level, texaddr);485}486}487488const void *verts = Memory::GetPointer(gstate_c.vertexAddr);489const void *indices = nullptr;490if ((gstate.vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) {491indices = Memory::GetPointer(gstate_c.indexAddr);492}493494u32 ibytes = 0;495u32 vbytes = 0;496GetVertDataSizes(vcount, indices, vbytes, ibytes);497498if (indices && ibytes > 0) {499EmitCommandWithRAM(CommandType::INDICES, indices, ibytes, 4);500}501if (verts && vbytes > 0) {502EmitCommandWithRAM(CommandType::VERTICES, verts, vbytes, 4);503}504}505506static void EmitTransfer(u32 op) {507FlushRegisters();508509// This may not make a lot of sense right now, unless it's to a framebuf...510u32 dstBasePtr = gstate.getTransferDstAddress();511if (!Memory::IsVRAMAddress(dstBasePtr)) {512// Skip, not VRAM, so can't affect drawing (we flush textures each prim.)513return;514}515516u32 srcBasePtr = gstate.getTransferSrcAddress();517u32 srcStride = gstate.getTransferSrcStride();518int srcX = gstate.getTransferSrcX();519int srcY = gstate.getTransferSrcY();520u32 dstStride = gstate.getTransferDstStride();521int dstX = gstate.getTransferDstX();522int dstY = gstate.getTransferDstY();523int width = gstate.getTransferWidth();524int height = gstate.getTransferHeight();525int bpp = gstate.getTransferBpp();526527u32 srcBytes = ((srcY + height - 1) * srcStride + (srcX + width)) * bpp;528srcBytes = Memory::ValidSize(srcBasePtr, srcBytes);529530u32 dstBytes = ((dstY + height - 1) * dstStride + (dstX + width)) * bpp;531dstBytes = Memory::ValidSize(dstBasePtr, dstBytes);532533if (srcBytes != 0) {534EmitCommandWithRAM(CommandType::TRANSFERSRC, Memory::GetPointerUnchecked(srcBasePtr), srcBytes, 16);535DirtyVRAM(dstBasePtr, dstBytes, DirtyVRAMFlag::DIRTY);536}537538lastRegisters.push_back(op);539}540541static void EmitClut(u32 op) {542u32 addr = gstate.getClutAddress();543544// Hardware rendering may be using a framebuffer as CLUT.545// To get at this, we first run the command (normally we're called right before it has run.)546if (Memory::IsVRAMAddress(addr))547gpuDebug->SetCmdValue(op);548549// Actually should only be 0x3F, but we allow enhanced CLUTs. See #15727.550u32 blocks = (op & 0x7F) == 0x40 ? 0x40 : (op & 0x3F);551u32 bytes = blocks * 32;552bytes = Memory::ValidSize(addr, bytes);553554if (bytes != 0) {555// Send the original address so VRAM can be reasoned about.556if (Memory::IsVRAMAddress(addr)) {557struct ClutAddrData {558u32 addr;559u32 flags;560};561u32 flags = GetTargetFlags(addr, bytes);562ClutAddrData data{ addr, flags };563564FlushRegisters();565Command cmd{CommandType::CLUTADDR, sizeof(data), (u32)pushbuf.size()};566pushbuf.resize(pushbuf.size() + sizeof(data));567memcpy(pushbuf.data() + cmd.ptr, &data, sizeof(data));568commands.push_back(cmd);569570if ((flags & 2) == 0)571UpdateLastVRAM(addr, bytes);572}573EmitCommandWithRAM(CommandType::CLUT, Memory::GetPointerUnchecked(addr), bytes, 16);574}575576lastRegisters.push_back(op);577}578579static void EmitPrim(u32 op) {580FlushPrimState(op & 0x0000FFFF);581582lastRegisters.push_back(op);583DirtyDrawnVRAM();584}585586static void EmitBezierSpline(u32 op) {587int ucount = op & 0xFF;588int vcount = (op >> 8) & 0xFF;589FlushPrimState(ucount * vcount);590591lastRegisters.push_back(op);592DirtyDrawnVRAM();593}594595bool IsActive() {596return active;597}598599bool IsActivePending() {600return nextFrame || active;601}602603bool RecordNextFrame(const std::function<void(const Path &)> callback) {604if (!nextFrame) {605flipLastAction = gpuStats.numFlips;606flipFinishAt = -1;607writeCallback = callback;608nextFrame = true;609return true;610}611return false;612}613614void ClearCallback() {615// Not super thread safe..616writeCallback = nullptr;617}618619static void FinishRecording() {620// We're done - this was just to write the result out.621Path filename = WriteRecording();622commands.clear();623pushbuf.clear();624lastVRAM.clear();625626NOTICE_LOG(Log::System, "Recording finished");627active = false;628flipLastAction = gpuStats.numFlips;629flipFinishAt = -1;630lastEdramTrans = 0x400;631632if (writeCallback) {633writeCallback(filename);634}635writeCallback = nullptr;636}637638static void CheckEdramTrans() {639if (!gpuDebug)640return;641642uint32_t value = gpuDebug->GetAddrTranslation();643if (value == lastEdramTrans)644return;645lastEdramTrans = value;646647FlushRegisters();648Command cmd{CommandType::EDRAMTRANS, sizeof(value), (u32)pushbuf.size()};649pushbuf.resize(pushbuf.size() + sizeof(value));650memcpy(pushbuf.data() + cmd.ptr, &value, sizeof(value));651commands.push_back(cmd);652}653654void NotifyCommand(u32 pc) {655if (!active) {656return;657}658659CheckEdramTrans();660const u32 op = Memory::Read_U32(pc);661const GECommand cmd = GECommand(op >> 24);662663switch (cmd) {664case GE_CMD_VADDR:665case GE_CMD_IADDR:666case GE_CMD_JUMP:667case GE_CMD_CALL:668case GE_CMD_RET:669case GE_CMD_END:670case GE_CMD_SIGNAL:671case GE_CMD_FINISH:672case GE_CMD_BASE:673case GE_CMD_OFFSETADDR:674case GE_CMD_ORIGIN:675// These just prepare future commands, and are flushed with those commands.676// TODO: Maybe add a command just to log that these were hit?677break;678679case GE_CMD_BOUNDINGBOX:680case GE_CMD_BJUMP:681// Since we record each command, this is theoretically not relevant.682// TODO: Output a CommandType to validate this.683break;684685case GE_CMD_PRIM:686EmitPrim(op);687break;688689case GE_CMD_BEZIER:690case GE_CMD_SPLINE:691EmitBezierSpline(op);692break;693694case GE_CMD_LOADCLUT:695EmitClut(op);696break;697698case GE_CMD_TRANSFERSTART:699EmitTransfer(op);700break;701702default:703lastRegisters.push_back(op);704break;705}706}707708void NotifyMemcpy(u32 dest, u32 src, u32 sz) {709if (!active) {710return;711}712713CheckEdramTrans();714if (Memory::IsVRAMAddress(dest)) {715FlushRegisters();716Command cmd{CommandType::MEMCPYDEST, sizeof(dest), (u32)pushbuf.size()};717pushbuf.resize(pushbuf.size() + sizeof(dest));718memcpy(pushbuf.data() + cmd.ptr, &dest, sizeof(dest));719commands.push_back(cmd);720721sz = Memory::ValidSize(dest, sz);722if (sz != 0) {723EmitCommandWithRAM(CommandType::MEMCPYDATA, Memory::GetPointerUnchecked(dest), sz, 1);724UpdateLastVRAM(dest, sz);725DirtyVRAM(dest, sz, DirtyVRAMFlag::CLEAN);726}727}728}729730void NotifyMemset(u32 dest, int v, u32 sz) {731if (!active) {732return;733}734735CheckEdramTrans();736struct MemsetCommand {737u32 dest;738int value;739u32 sz;740};741742if (Memory::IsVRAMAddress(dest)) {743sz = Memory::ValidSize(dest, sz);744MemsetCommand data{dest, v, sz};745746FlushRegisters();747Command cmd{CommandType::MEMSET, sizeof(data), (u32)pushbuf.size()};748pushbuf.resize(pushbuf.size() + sizeof(data));749memcpy(pushbuf.data() + cmd.ptr, &data, sizeof(data));750commands.push_back(cmd);751ClearLastVRAM(dest, v, sz);752DirtyVRAM(dest, sz, DirtyVRAMFlag::CLEAN);753}754}755756void NotifyUpload(u32 dest, u32 sz) {757// This also checks the edram translation value and dirties VRAM.758NotifyMemcpy(dest, dest, sz);759}760761static bool HasDrawCommands() {762if (commands.empty())763return false;764765for (const Command &cmd : commands) {766switch (cmd.type) {767case CommandType::INIT:768case CommandType::DISPLAY:769continue;770771default:772return true;773}774}775776// Only init and display commands, keep going.777return false;778}779780void NotifyDisplay(u32 framebuf, int stride, int fmt) {781bool writePending = false;782if (active && HasDrawCommands()) {783writePending = true;784}785if (!active && nextFrame && (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) == 0) {786NOTICE_LOG(Log::System, "Recording starting on display...");787BeginRecording();788}789if (!active) {790return;791}792793CheckEdramTrans();794struct DisplayBufData {795PSPPointer<u8> topaddr;796int linesize, pixelFormat;797};798799DisplayBufData disp{ { framebuf }, stride, fmt };800801FlushRegisters();802u32 ptr = (u32)pushbuf.size();803u32 sz = (u32)sizeof(disp);804pushbuf.resize(pushbuf.size() + sz);805memcpy(pushbuf.data() + ptr, &disp, sz);806807commands.push_back({ CommandType::DISPLAY, sz, ptr });808809if (writePending) {810NOTICE_LOG(Log::System, "Recording complete on display");811FinishRecording();812}813}814815void NotifyBeginFrame() {816const bool noDisplayAction = flipLastAction + 4 < gpuStats.numFlips;817// We do this only to catch things that don't call NotifyDisplay.818if (active && HasDrawCommands() && (noDisplayAction || gpuStats.numFlips == flipFinishAt)) {819NOTICE_LOG(Log::System, "Recording complete on frame");820821CheckEdramTrans();822struct DisplayBufData {823PSPPointer<u8> topaddr;824u32 linesize, pixelFormat;825};826827DisplayBufData disp;828__DisplayGetFramebuf(&disp.topaddr, &disp.linesize, &disp.pixelFormat, 0);829830FlushRegisters();831u32 ptr = (u32)pushbuf.size();832u32 sz = (u32)sizeof(disp);833pushbuf.resize(pushbuf.size() + sz);834memcpy(pushbuf.data() + ptr, &disp, sz);835836commands.push_back({ CommandType::DISPLAY, sz, ptr });837838FinishRecording();839}840if (!active && nextFrame && (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) == 0 && noDisplayAction) {841NOTICE_LOG(Log::System, "Recording starting on frame...");842BeginRecording();843// If we began on a BeginFrame, end on a BeginFrame.844flipFinishAt = gpuStats.numFlips + 1;845}846}847848void NotifyCPU() {849if (!active) {850return;851}852853DirtyAllVRAM(DirtyVRAMFlag::UNKNOWN);854}855856};857858859