// 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 #include #include #include #include #include #include #include #include #include #include #include "Common/Profiler/Profiler.h" #include "Common/CommonTypes.h" #include "Common/Log.h" #include "Common/Thread/ThreadUtil.h" #include "Common/System/Request.h" #include "Core/Config.h" #include "Core/Core.h" #include "Core/CoreTiming.h" #include "Core/Debugger/MemBlockInfo.h" #include "Core/ELF/ParamSFO.h" #include "Core/FileSystems/MetaFileSystem.h" #include "Core/HLE/HLE.h" #include "Core/HLE/sceDisplay.h" #include "Core/HLE/sceKernelMemory.h" #include "Core/MemMap.h" #include "Core/MemMapHelpers.h" #include "Core/MIPS/MIPS.h" #include "Core/MIPS/MIPSCodeUtils.h" #include "Core/System.h" #include "Core/Util/GameDB.h" #include "Core/Util/PathUtil.h" #include "GPU/GPUCommon.h" #include "GPU/GPUState.h" #include "GPU/ge_constants.h" #include "GPU/Debugger/Playback.h" #include "GPU/Debugger/Record.h" #include "GPU/Debugger/RecordFormat.h" namespace GPURecord { // Provide the illusion of synchronous execution, although the playback is actually running on a different thread. enum class OpType { None, UpdateStallAddr, EnqueueList, ListSync, ReapplyGfxState, Done, }; static const char *OpTypeToString(OpType type) { switch (type) { case OpType::None: return "None"; case OpType::UpdateStallAddr: return "UpdateStallAddr"; case OpType::EnqueueList: return "EnqueueList"; case OpType::ListSync: return "ListSync"; case OpType::ReapplyGfxState: return "ReapplyGfxState"; case OpType::Done: return "Done"; default: return "N/A"; } } struct Operation { OpType type; u32 listID; // also listPC in EnqueueList u32 param; // stallAddr generally }; static std::string lastExecFilename; static uint32_t lastExecVersion; static std::vector lastExecCommands; static std::vector lastExecPushbuf; // This thread is restarted every frame (dump execution) for simplicity. TODO: Make persistent? // Alternatively, get rid of it, but the code is written in a way that makes it difficult (you'll see if you try). static std::thread replayThread; static std::mutex opStartLock; static std::condition_variable g_condOpStartWait; static std::mutex opFinishLock; static std::condition_variable opFinishWait; static Operation g_opToExec; static u32 g_retVal; static bool g_opDone = true; static bool g_cancelled = false; static int g_drawLimit = 0; void SetReplayDrawLimit(int lastPrim) { g_drawLimit = lastPrim; } // Runs on operation thread u32 ExecuteOnMain(Operation opToExec) { { std::unique_lock startLock(opStartLock); g_opToExec = opToExec; g_retVal = 0; g_opDone = false; g_condOpStartWait.notify_one(); } // now wait for completion. At that point, noone cares about g_opToExec anymore, and we can safely // overwrite it next time. { std::unique_lock lock(opFinishLock); opFinishWait.wait(lock, []() { return g_opDone || g_cancelled; }); } return g_retVal; } // This class maps pushbuffer (dump data) sections to PSP memory. // Dumps can be larger than available PSP memory, because they include generated data too. // // If possible, it maps to dynamically allocated "slabs" so nearby access is fast. // Otherwise it uses "extra" allocations to manage sections that straddle two slabs. // Slabs are managed with LRU, extra buffers are round-robin. class BufMapping { public: BufMapping(const std::vector &pushbuf) : pushbuf_(pushbuf) { } // Returns a pointer to contiguous memory for this access, aligned to align bytes, or else 0 (failure). u32 Map(u32 bufpos, u32 sz, const std::function &flush, u32 align = 1); // Clear and reset allocations made. void Reset() { slabGeneration_ = 0; extraOffset_ = 0; for (int i = 0; i < SLAB_COUNT; ++i) { slabs_[i].Free(); } for (int i = 0; i < EXTRA_COUNT; ++i) { extra_[i].Free(); } } protected: u32 MapSlab(u32 bufpos, const std::function &flush); u32 MapExtra(u32 bufpos, u32 sz, const std::function &flush); enum { // These numbers kept low because we only have 24 MB of user memory to map into. SLAB_SIZE = 1 * 1024 * 1024, // 10 is the number of texture units + verts + inds. // In the worst case, we could concurrently need 10 slabs/extras at the same time. SLAB_COUNT = 10, EXTRA_COUNT = 10, }; // The current "generation". Static simply as a convenience for access. // This increments on every allocation, for a simple LRU. static int slabGeneration_; // An aligned large mapping of the pushbuffer in PSP RAM. struct SlabInfo { u32 psp_pointer_ = 0; u32 buf_pointer_ = 0; int last_used_ = 0; bool Matches(u32 bufpos) const { // We check psp_pointer_ because bufpos = 0 is valid, and the initial value. return buf_pointer_ == bufpos && psp_pointer_ != 0; } // Automatically marks used for LRU purposes. u32 Ptr(u32 bufpos) { last_used_ = slabGeneration_; return psp_pointer_ + (bufpos - buf_pointer_); } int Age() const { // If not allocated, it's as expired as it's gonna get. if (psp_pointer_ == 0) return std::numeric_limits::max(); return slabGeneration_ - last_used_; } bool Alloc(); void Free(); bool Setup(u32 bufpos, const std::vector &pushbuf_); }; // An adhoc mapping of the pushbuffer (either larger than a slab or straddling slabs.) // Remember: texture data, verts, etc. must be contiguous. struct ExtraInfo { u32 psp_pointer_ = 0; u32 buf_pointer_ = 0; u32 size_ = 0; bool Matches(u32 bufpos, u32 sz) const { // We check psp_pointer_ because bufpos = 0 is valid, and the initial value. return buf_pointer_ == bufpos && psp_pointer_ != 0 && size_ >= sz; } u32 Ptr() const { return psp_pointer_; } bool Alloc(u32 bufpos, u32 sz, const std::vector &pushbuf_); void Free(); }; SlabInfo slabs_[SLAB_COUNT]{}; u32 lastSlab_ = 0; u32 extraOffset_ = 0; ExtraInfo extra_[EXTRA_COUNT]{}; const std::vector &pushbuf_; }; u32 BufMapping::Map(u32 bufpos, u32 sz, const std::function &flush, u32 align) { int slab1 = bufpos / SLAB_SIZE; int slab2 = (bufpos + sz - 1) / SLAB_SIZE; // Older recorders could reuse data at any offset (a CLUT found one byte into another), but the GE // ignores the low bits of a CLUT or texture address. The extra mappings are separate, aligned copies. if ((bufpos & (align - 1)) != 0) return MapExtra(bufpos, sz, flush); if (slab1 == slab2) { // Shortcut in case it's simply the most recent slab. if (slabs_[lastSlab_].Matches(slab1 * SLAB_SIZE)) return slabs_[lastSlab_].Ptr(bufpos); // Doesn't straddle, so we can just map to a slab. return MapSlab(bufpos, flush); } else { // We need contiguous, so we'll just allocate separately. return MapExtra(bufpos, sz, flush); } } u32 BufMapping::MapSlab(u32 bufpos, const std::function &flush) { u32 slab_pos = (bufpos / SLAB_SIZE) * SLAB_SIZE; int best = 0; for (int i = 0; i < SLAB_COUNT; ++i) { if (slabs_[i].Matches(slab_pos)) { return slabs_[i].Ptr(bufpos); } if (slabs_[i].Age() > slabs_[best].Age()) { best = i; } } // Stall before mapping a new slab. flush(); // Okay, we need to allocate. if (!slabs_[best].Setup(slab_pos, pushbuf_)) { return 0; } lastSlab_ = best; return slabs_[best].Ptr(bufpos); } u32 BufMapping::MapExtra(u32 bufpos, u32 sz, const std::function &flush) { for (int i = 0; i < EXTRA_COUNT; ++i) { // Might be likely to reuse larger buffers straddling slabs. if (extra_[i].Matches(bufpos, sz)) { return extra_[i].Ptr(); } } // Stall first, so we don't stomp existing RAM. flush(); int i = extraOffset_; extraOffset_ = (extraOffset_ + 1) % EXTRA_COUNT; if (!extra_[i].Alloc(bufpos, sz, pushbuf_)) { // Let's try to power on - hopefully none of these are still in use. for (int i = 0; i < EXTRA_COUNT; ++i) { extra_[i].Free(); } if (!extra_[i].Alloc(bufpos, sz, pushbuf_)) { return 0; } } return extra_[i].Ptr(); } bool BufMapping::SlabInfo::Alloc() { u32 sz = SLAB_SIZE; psp_pointer_ = userMemory.Alloc(sz, false, "Slab"); if (psp_pointer_ == -1) { psp_pointer_ = 0; } return psp_pointer_ != 0; } void BufMapping::SlabInfo::Free() { if (psp_pointer_) { userMemory.Free(psp_pointer_); psp_pointer_ = 0; buf_pointer_ = 0; last_used_ = 0; } } bool BufMapping::ExtraInfo::Alloc(u32 bufpos, u32 sz, const std::vector &pushbuf_) { // Make sure we've freed any previous allocation first. Free(); u32 allocSize = sz; psp_pointer_ = userMemory.Alloc(allocSize, false, "Straddle extra"); if (psp_pointer_ == -1) { psp_pointer_ = 0; } if (psp_pointer_ == 0) { return false; } buf_pointer_ = bufpos; size_ = sz; Memory::MemcpyUnchecked(psp_pointer_, pushbuf_.data() + bufpos, sz); return true; } void BufMapping::ExtraInfo::Free() { if (psp_pointer_) { userMemory.Free(psp_pointer_); psp_pointer_ = 0; buf_pointer_ = 0; } } bool BufMapping::SlabInfo::Setup(u32 bufpos, const std::vector &pushbuf_) { // If it already has RAM, we're simply taking it over. Slabs come only in one size. if (psp_pointer_ == 0) { if (!Alloc()) { return false; } } buf_pointer_ = bufpos; u32 sz = std::min((u32)SLAB_SIZE, (u32)pushbuf_.size() - bufpos); Memory::MemcpyUnchecked(psp_pointer_, pushbuf_.data() + bufpos, sz); slabGeneration_++; last_used_ = slabGeneration_; return true; } int BufMapping::slabGeneration_ = 0; class DumpExecute { public: DumpExecute(const std::vector &pushbuf, const std::vector &commands, uint32_t version) : pushbuf_(pushbuf), commands_(commands), mapping_(pushbuf), version_(version) { MarkNeededTargets(); } ~DumpExecute(); ReplayResult Run(); private: void SyncStall(); void SubmitListEnd(); void Init(u32 ptr, u32 sz); void Registers(u32 ptr, u32 sz); void Vertices(u32 ptr, u32 sz); void Indices(u32 ptr, u32 sz); void ClutAddr(u32 ptr, u32 sz); void Clut(u32 ptr, u32 sz); void TransferSrc(u32 ptr, u32 sz); void Memset(u32 ptr, u32 sz); void MemcpyDest(u32 ptr, u32 sz); void Memcpy(u32 ptr, u32 sz); void Texture(int level, u32 ptr, u32 sz); void Framebuf(int level, u32 ptr, u32 sz, bool needed); void Display(u32 ptr, u32 sz, bool allowFlip); void EdramTrans(u32 ptr, u32 sz); u32 execMemcpyDest = 0; u32 execClutAddr = 0; u32 execClutFlags = 0; u32 execListBuf = 0; u32 execListPos = 0; u32 execListID = 0; const int LIST_BUF_SIZE = 256 * 1024; std::vector execListQueue; u16 lastBufw_[8]{}; u32 lastTex_[8]{}; int prims_ = 0; u32 lastBase_ = 0; // The framebuffers drawn to so far by VRAM offset, with the areas the draws could reach, and the // current one's registers. struct DrawnRect { int x1, y1, x2, y2; bool Contains(int x, int y) const { return x >= x1 && x <= x2 && y >= y1 && y <= y2; } }; struct DrawnTarget { u32 strideBytes; u32 bpp; std::vector rects; }; std::map drawnTargets_; u32 fbPtr_ = 0; u32 fbWidth_ = 0; u32 fbFormat_ = 0; u32 region2_ = 0; u32 scissor2_ = 0; u32 vertType_ = 0; u32 zbPtr_ = 0; u32 zbWidth_ = 0; bool zTest_ = false; bool zWriteDisable_ = false; u32 clearMode_ = 0; // Right after INIT, where a CLUT command is the CLUT the GE had loaded. bool initialClut_ = false; // Whether the last DISPLAY showed something (address 0 turns the display off). bool haveDisplay_ = false; // The vertices recorded for the next draw. u32 lastVertsPtr_ = 0; u32 lastVertsSize_ = 0; void MarkDrawn(u32 prim); void CopyAroundDrawn(u32 addr, const u8 *data, u32 size, bool linear); void MarkNeededTargets(); // By command: the render target snapshots holding data that a later snapshot leaves out as unchanged. std::vector neededTargets_; const std::vector &pushbuf_; const std::vector &commands_; BufMapping mapping_; uint32_t version_ = 0; int resumeIndex_ = -1; }; void DumpExecute::SyncStall() { if (execListBuf == 0) { VERBOSE_LOG(Log::GeDebugger, "SyncStall: No active display list"); return; } ExecuteOnMain(Operation{ OpType::UpdateStallAddr, execListID, execListPos }); s64 listTicks = gpu->GetListTicks(execListID); if (listTicks != -1) { s64 nowTicks = CoreTiming::GetTicks(currentMIPS); if (listTicks > nowTicks) { currentMIPS->downcount -= listTicks - nowTicks; } } // Make sure downcount doesn't overflow. (can this even happen?) // Also this doesn't do anything in this context, we don't reschedule... or at least // aren't supposed to. // CoreTiming::ForceCheck(); } void DumpExecute::Registers(u32 ptr, u32 sz) { initialClut_ = false; if (execListBuf == 0) { u32 allocSize = LIST_BUF_SIZE; execListBuf = userMemory.Alloc(allocSize, true, "List buf"); if (execListBuf == -1) { execListBuf = 0; } if (execListBuf == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for display list"); return; } execListPos = execListBuf; Memory::WriteUnchecked_U32(GE_CMD_NOP << 24, execListPos); execListPos += 4; // TODO: Why do we disable interrupts here? gpu->EnableInterrupts(false); execListID = ExecuteOnMain(Operation{ OpType::EnqueueList, execListBuf, execListPos }); gpu->EnableInterrupts(true); } u32 pendingSize = (u32)execListQueue.size() * sizeof(u32); // Validate space for jump. u32 allocSize = pendingSize + sz + 8; if (execListPos + allocSize >= execListBuf + LIST_BUF_SIZE) { // Finish this list, wait for the GE to run it, and start a new one at the buffer's start. Jumping // back instead let the next lap overwrite commands the GE hadn't reached yet: Warriors (14660) lost // half its draws. The PSP replayer does the same. GE state, BASE included, carries over. Memory::WriteUnchecked_U32(GE_CMD_FINISH << 24, execListPos); Memory::WriteUnchecked_U32(GE_CMD_END << 24, execListPos + 4); execListPos += 8; SyncStall(); ExecuteOnMain(Operation{ OpType::ListSync, execListID }); execListPos = execListBuf; Memory::WriteUnchecked_U32(GE_CMD_NOP << 24, execListPos); execListPos += 4; gpu->EnableInterrupts(false); execListID = ExecuteOnMain(Operation{ OpType::EnqueueList, execListBuf, execListPos }); gpu->EnableInterrupts(true); } Memory::MemcpyUnchecked(execListPos, execListQueue.data(), pendingSize); execListPos += pendingSize; u32 writePos = execListPos; void *srcData = (void *)(pushbuf_.data() + ptr); Memory::MemcpyUnchecked(execListPos, srcData, sz); execListPos += sz; // TODO: Unfortunate. Maybe Texture commands should contain the bufw instead. // The goal here is to realistically combine prims in dumps. Stalling for the bufw flushes. u32_le *ops = (u32_le *)Memory::GetPointerUnchecked(writePos); u32 lastTexHigh[8]{}; for (int i = 0; i < 8; ++i) lastTexHigh[i] = ((lastTex_[i] & 0xFF000000) >> 8) | ((GE_CMD_TEXBUFWIDTH0 + i) << 24); for (u32 i = 0; i < sz / 4; ++i) { u32 cmd = ops[i] >> 24; if (cmd >= GE_CMD_TEXBUFWIDTH0 && cmd <= GE_CMD_TEXBUFWIDTH7) { int level = cmd - GE_CMD_TEXBUFWIDTH0; u16 bufw = ops[i] & 0xFFFF; // NOP the address part of the command to avoid a flush too. if (bufw == lastBufw_[level]) ops[i] = GE_CMD_NOP << 24; else ops[i] = lastTexHigh[level] | bufw; lastBufw_[level] = bufw; } if (cmd == GE_CMD_FRAMEBUFPTR) fbPtr_ = ops[i] & 0x00FFFFFF; if (cmd == GE_CMD_FRAMEBUFWIDTH) fbWidth_ = ops[i] & 0x00FFFFFF; if (cmd == GE_CMD_FRAMEBUFPIXFORMAT) fbFormat_ = ops[i] & 3; if (cmd == GE_CMD_REGION2) region2_ = ops[i] & 0x000FFFFF; if (cmd == GE_CMD_SCISSOR2) scissor2_ = ops[i] & 0x000FFFFF; if (cmd == GE_CMD_VERTEXTYPE) vertType_ = ops[i] & 0x00FFFFFF; if (cmd == GE_CMD_ZBUFPTR) zbPtr_ = ops[i] & 0x00FFFFFF; if (cmd == GE_CMD_ZBUFWIDTH) zbWidth_ = ops[i] & 0x00FFFFFF; if (cmd == GE_CMD_ZTESTENABLE) zTest_ = (ops[i] & 1) != 0; if (cmd == GE_CMD_ZWRITEDISABLE) zWriteDisable_ = (ops[i] & 1) != 0; if (cmd == GE_CMD_CLEARMODE) clearMode_ = ops[i] & 0xFFFF; if (cmd == GE_CMD_PRIM || cmd == GE_CMD_BEZIER || cmd == GE_CMD_SPLINE) { MarkDrawn(cmd == GE_CMD_PRIM ? ops[i] & 0x00FFFFFF : 0); prims_++; if (g_drawLimit > 0 && prims_ > g_drawLimit) ops[i] = GE_CMD_NOP << 24; } // Past the limit, keep the render target too, for --screenshot-render-target. if (g_drawLimit > 0 && prims_ >= g_drawLimit && (cmd == GE_CMD_FRAMEBUFPTR || cmd == GE_CMD_FRAMEBUFWIDTH || cmd == GE_CMD_FRAMEBUFPIXFORMAT)) { ops[i] = GE_CMD_NOP << 24; } // Since we're here anyway, also NOP out texture addresses. // This makes Step Tex not hit phantom textures, but we rely on it for lastTex_[]. if (cmd >= GE_CMD_TEXADDR0 && cmd <= GE_CMD_TEXADDR7) { ops[i] = GE_CMD_NOP << 24; } if (cmd == GE_CMD_SIGNAL || cmd == GE_CMD_BASE) { lastBase_ = 0xFFFFFFFF; } } execListQueue.clear(); } void DumpExecute::SubmitListEnd() { if (execListPos == 0 || g_cancelled) { return; } // There's always space for the end, same size as a jump. Memory::WriteUnchecked_U32(GE_CMD_FINISH << 24, execListPos); Memory::WriteUnchecked_U32(GE_CMD_END << 24, execListPos + 4); execListPos += 8; for (int i = 0; i < 8; ++i) lastTex_[i] = 0; lastBase_ = 0xFFFFFFFF; SyncStall(); ExecuteOnMain(Operation{ OpType::ListSync, execListID }); } void DumpExecute::Init(u32 ptr, u32 sz) { // Dumps from before savedContextVersion 1 hold PPSSPP's old context layout, with the matrices as raw // floats; the new one ends its commands with an END. Restoring an old one as new turned its matrices // into garbage and the scene disappeared (4140). The PSP replayer tells them apart the same way. const u32_le *context = (const u32_le *)(pushbuf_.data() + ptr); bool oldLayout = true; for (int i = 17; i < 512; ++i) { if (context[i] == GE_CMD_END << 24) { oldLayout = false; break; } } gstate.Restore(context, oldLayout); ExecuteOnMain(Operation{ OpType::ReapplyGfxState }); fbPtr_ = gstate.fbptr & 0x00FFFFFF; fbWidth_ = gstate.fbwidth & 0x00FFFFFF; fbFormat_ = gstate.framebufpixformat & 3; region2_ = gstate.region2 & 0x000FFFFF; scissor2_ = gstate.scissor2 & 0x000FFFFF; vertType_ = gstate.vertType & 0x00FFFFFF; zbPtr_ = gstate.zbptr & 0x00FFFFFF; zbWidth_ = gstate.zbwidth & 0x00FFFFFF; zTest_ = (gstate.zTestEnable & 1) != 0; zWriteDisable_ = (gstate.zmsk & 1) != 0; clearMode_ = gstate.clearmode & 0xFFFF; // Hm, why are we doing this here? Should it be before? for (int i = 0; i < 8; ++i) { lastBufw_[i] = 0; lastTex_[i] = 0; } lastBase_ = 0xFFFFFFFF; initialClut_ = true; } // The drawing-space bounds of a through mode draw's vertices, or false if they aren't simple to read // (indexed, morphed, or not through mode). static bool ThroughModeBounds(u32 vtype, const u8 *data, u32 size, u32 count, int &x1, int &y1, int &x2, int &y2) { if ((vtype & GE_VTYPE_THROUGH) == 0 || (vtype & GE_VTYPE_IDX_MASK) != 0 || (vtype & GE_VTYPE_MORPHCOUNT_MASK) != 0 || count == 0) return false; // Each component is aligned to its own size, and the vertex to the largest. static const u8 compSizes[4] = { 0, 1, 2, 4 }; static const u8 colorSizes[8] = { 0, 0, 0, 0, 2, 2, 2, 4 }; u32 offset = 0, align = 1; auto add = [&](u32 elemSize, u32 n) { if (elemSize == 0) return; offset = (offset + elemSize - 1) & ~(elemSize - 1); offset += elemSize * n; align = std::max(align, elemSize); }; add(compSizes[(vtype & GE_VTYPE_WEIGHT_MASK) >> GE_VTYPE_WEIGHT_SHIFT], ((vtype & GE_VTYPE_WEIGHTCOUNT_MASK) >> GE_VTYPE_WEIGHTCOUNT_SHIFT) + 1); add(compSizes[(vtype & GE_VTYPE_TC_MASK) >> GE_VTYPE_TC_SHIFT], 2); add(colorSizes[(vtype & GE_VTYPE_COL_MASK) >> GE_VTYPE_COL_SHIFT], 1); add(compSizes[(vtype & GE_VTYPE_NRM_MASK) >> GE_VTYPE_NRM_SHIFT], 3); const u32 posSize = compSizes[(vtype & GE_VTYPE_POS_MASK) >> GE_VTYPE_POS_SHIFT]; if (posSize == 0) return false; offset = (offset + posSize - 1) & ~(posSize - 1); const u32 posOffset = offset; add(posSize, 3); const u32 stride = (offset + align - 1) & ~(align - 1); if (stride * count > size) return false; x1 = y1 = INT_MAX; x2 = y2 = INT_MIN; for (u32 i = 0; i < count; ++i) { const u8 *p = data + i * stride + posOffset; int x, y; if (posSize == 4) { float fx, fy; memcpy(&fx, p, 4); memcpy(&fy, p + 4, 4); if (!(fx >= -4096.0f && fx <= 4096.0f && fy >= -4096.0f && fy <= 4096.0f)) return false; x = (int)fx; y = (int)fy; } else if (posSize == 2) { x = (s16)(p[0] | (p[1] << 8)); y = (s16)(p[2] | (p[3] << 8)); } else { x = (s8)p[0]; y = (s8)p[1]; } // Truncation can be a pixel short at either end; the bounds only need to be generous. x1 = std::min(x1, x - 1); y1 = std::min(y1, y - 1); x2 = std::max(x2, x + 1); y2 = std::max(y2, y + 1); } return true; } void DumpExecute::MarkDrawn(u32 prim) { const u32 start = ((fbPtr_ & 0xFFFFF0) | ((fbWidth_ & 0xFF0000) << 8)) & 0x001FFFFF; const u32 bpp = fbFormat_ == GE_FORMAT_8888 ? 4 : 2; int x1 = 0, y1 = 0; int x2 = std::min(region2_ & 0x3FF, scissor2_ & 0x3FF); int y2 = std::min((region2_ >> 10) & 0x3FF, (scissor2_ >> 10) & 0x3FF); // A 2D draw (a sprite into a small buffer whose nominal area overlaps textures, Burnout) can say where // it is exactly. int vx1, vy1, vx2, vy2; if (prim != 0 && lastVertsSize_ != 0 && ThroughModeBounds(vertType_, pushbuf_.data() + lastVertsPtr_, lastVertsSize_, prim & 0xFFFF, vx1, vy1, vx2, vy2)) { x1 = std::max(x1, vx1); y1 = std::max(y1, vy1); x2 = std::min(x2, vx2); y2 = std::min(y2, vy2); if (x1 > x2 || y1 > y2) return; } auto add = [&](u32 base, u32 strideBytes, u32 bytesPerPixel, const DrawnRect &rect) { DrawnTarget &t = drawnTargets_[base]; t.strideBytes = strideBytes; t.bpp = bytesPerPixel; for (const DrawnRect &r : t.rects) { if (r.Contains(rect.x1, rect.y1) && r.Contains(rect.x2, rect.y2)) return; } t.rects.push_back(rect); }; add(start, (fbWidth_ & 0x07FC) * bpp, bpp, DrawnRect{ x1, y1, x2, y2 }); const bool writesDepth = (clearMode_ & 1) ? (clearMode_ & 0x400) != 0 : (zTest_ && !zWriteDisable_); const int zbw = zbWidth_ & 0x07FC; if (writesDepth && zbw != 0) { // The PSP stores depth swizzled, so what lands in a drawn area is spread over whole rows in bands // (SOCOM's frame texture copy ran into its depth buffer's columns 480-511). add(zbPtr_ & 0x001FFFF0, zbw * 2, 2, DrawnRect{ 0, y1 & ~7, zbw - 1, y2 | 7 }); } } void DumpExecute::Vertices(u32 ptr, u32 sz) { lastVertsPtr_ = ptr; lastVertsSize_ = sz; u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); if (psp == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for vertices"); return; } if (lastBase_ != (psp & 0xFF000000)) { execListQueue.push_back((GE_CMD_BASE << 24) | ((psp >> 8) & 0x00FF0000)); lastBase_ = psp & 0xFF000000; } execListQueue.push_back((GE_CMD_VADDR << 24) | (psp & 0x00FFFFFF)); } void DumpExecute::Indices(u32 ptr, u32 sz) { u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); if (psp == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for indices"); return; } if (lastBase_ != (psp & 0xFF000000)) { execListQueue.push_back((GE_CMD_BASE << 24) | ((psp >> 8) & 0x00FF0000)); lastBase_ = psp & 0xFF000000; } execListQueue.push_back((GE_CMD_IADDR << 24) | (psp & 0x00FFFFFF)); } void DumpExecute::ClutAddr(u32 ptr, u32 sz) { struct ClutAddrData { u32 addr; u32 flags; }; const ClutAddrData *data = (const ClutAddrData *)(pushbuf_.data() + ptr); execClutAddr = data->addr; execClutFlags = data->flags; } void DumpExecute::Clut(u32 ptr, u32 sz) { // This is always run when we have the actual address set. if (execClutAddr != 0) { const bool isTarget = (execClutFlags & 1) != 0; // Could potentially always skip if !isTarget, but playing it safe for offset texture behavior. if (Memory::IsValidRange(execClutAddr, sz) && (!isTarget || !g_Config.bSoftwareRendering)) { // Intentionally don't trigger an upload here. Memory::MemcpyUnchecked(execClutAddr, pushbuf_.data() + ptr, sz); NotifyMemInfo(MemBlockFlags::WRITE, execClutAddr, sz, "ReplayClut"); } execClutAddr = 0; } else { u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this), 16); if (psp == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for clut"); return; } execListQueue.push_back((GE_CMD_CLUTADDRUPPER << 24) | ((psp >> 8) & 0x00FF0000)); execListQueue.push_back((GE_CMD_CLUTADDR << 24) | (psp & 0x00FFFFFF)); if (initialClut_) { // The CLUT the GE had loaded when the recording started (the recorder saves it right after // INIT), with no LOADCLUT to follow, unlike the CLUTs recorded at a LOADCLUT: load it here, // then put the game's CLUT address back. Without this, draws used whatever CLUT was loaded // before (HotBrain 16131, ULUS10268, drew its save screen background black). execListQueue.push_back((GE_CMD_LOADCLUT << 24) | ((sz / 32) & 0x3F)); execListQueue.push_back(gstate.clutaddrupper); execListQueue.push_back(gstate.clutaddr); initialClut_ = false; } } } void DumpExecute::TransferSrc(u32 ptr, u32 sz) { u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this), 16); if (psp == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for transfer"); return; } // Need to sync in order to access gstate.transfersrcw. SyncStall(); execListQueue.push_back((gstate.transfersrcw & 0xFF00FFFF) | ((psp >> 8) & 0x00FF0000)); execListQueue.push_back(((GE_CMD_TRANSFERSRC) << 24) | (psp & 0x00FFFFFF)); } // The PSP replayer writes recorded memory with the CPU, and the PSP sends every CPU access to VRAM's 0x04200000 and // 0x04600000 mirrors through the depth layout. With the software renderer PPSSPP's mirrors do that for 16-bit // accesses only, so a write into one goes a run of 32 aligned bytes at a time, which the layout keeps together. // IL-2's depth texture at 0x04710000 is recorded as the game sampled it, and only comes back so. static void CopyToMemory(u32 addr, const u8 *data, u32 size) { if (!Memory::DepthMirrorsActive() || !Memory::IsDepthTexVRAMAddress(addr)) { Memory::MemcpyUnchecked(addr, data, size); return; } for (u32 pos = 0; pos < size; ) { const u32 a = addr + pos; const u32 n = std::min(32 - (a & 31), size - pos); Memory::MemcpyUnchecked(Memory::DepthMirrored16(a), data + pos, n); pos += n; } } static void SetMemory(u32 addr, u8 value, u32 size) { if (!Memory::DepthMirrorsActive() || !Memory::IsDepthTexVRAMAddress(addr)) { Memory::Memset(addr, value, size); return; } for (u32 pos = 0; pos < size; ) { const u32 a = addr + pos; const u32 n = std::min(32 - (a & 31), size - pos); Memory::Memset(Memory::DepthMirrored16(a), value, n); pos += n; } } void DumpExecute::Memset(u32 ptr, u32 sz) { PROFILE_THIS_SCOPE("ReplayMemset"); struct MemsetCommand { u32 dest; int value; u32 sz; }; const MemsetCommand *data = (const MemsetCommand *)(pushbuf_.data() + ptr); if (Memory::IsVRAMAddress(data->dest)) { SyncStall(); // Draws the software renderer still has queued come before the write. gpu->Flush(); // TODO: should probably do this as an operation. // The software renderer leaves the memset to the caller, like sceKernelMemset does. if (!gpu->PerformMemorySet(data->dest, (u8)data->value, data->sz)) SetMemory(data->dest, (u8)data->value, data->sz); } } void DumpExecute::MemcpyDest(u32 ptr, u32 sz) { execMemcpyDest = *(const u32 *)(pushbuf_.data() + ptr); } void DumpExecute::Memcpy(u32 ptr, u32 sz) { PROFILE_THIS_SCOPE("ReplayMemcpy"); if (Memory::IsVRAMAddress(execMemcpyDest)) { SyncStall(); gpu->Flush(); CopyToMemory(execMemcpyDest, pushbuf_.data() + ptr, sz); NotifyMemInfo(MemBlockFlags::WRITE, execMemcpyDest, sz, "ReplayMemcpy"); gpu->PerformWriteColorFromMemory(execMemcpyDest, sz); } } void DumpExecute::Texture(int level, u32 ptr, u32 sz) { u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this), 16); if (psp == 0) { ERROR_LOG(Log::GeDebugger, "Unable to allocate for texture"); return; } if (lastTex_[level] != psp) { u32 bufwCmd = GE_CMD_TEXBUFWIDTH0 + level; u32 addrCmd = GE_CMD_TEXADDR0 + level; execListQueue.push_back((bufwCmd << 24) | ((psp >> 8) & 0x00FF0000) | lastBufw_[level]); execListQueue.push_back((addrCmd << 24) | (psp & 0x00FFFFFF)); lastTex_[level] = psp; } } void DumpExecute::Framebuf(int level, u32 ptr, u32 sz, bool needed) { PROFILE_THIS_SCOPE("ReplayFramebuf"); struct FramebufData { u32 addr; int bufw; u32 flags; u32 pad; }; FramebufData *framebuf = (FramebufData *)(pushbuf_.data() + ptr); if (lastTex_[level] != framebuf->addr || lastBufw_[level] != framebuf->bufw) { u32 bufwCmd = GE_CMD_TEXBUFWIDTH0 + level; u32 addrCmd = GE_CMD_TEXADDR0 + level; execListQueue.push_back((bufwCmd << 24) | ((framebuf->addr >> 8) & 0x00FF0000) | framebuf->bufw); execListQueue.push_back((addrCmd << 24) | (framebuf->addr & 0x00FFFFFF)); lastTex_[level] = framebuf->addr; lastBufw_[level] = framebuf->bufw; } // And now also copy the data into VRAM (in case it wasn't actually rendered.) u32 headerSize = (u32)sizeof(FramebufData); u32 pspSize = sz - headerSize; const bool isTarget = (framebuf->flags & 1) != 0; const bool unchangedVRAM = version_ >= 6 && (framebuf->flags & 2) != 0; // TODO: Could use drawnVRAM flag, but it can be wrong. // Could potentially always skip if !isTarget, but playing it safe for offset texture behavior. // The software renderer skips a render target's unless something later relies on it: one drawn before the // dump started holds data no other snapshot brings in (Madden's field logo, Need for Speed ProStreet). if (Memory::IsValidRange(framebuf->addr, pspSize) && !unchangedVRAM && (!isTarget || !g_Config.bSoftwareRendering || needed)) { // After the draws before it, which the software renderer may still have queued. SyncStall(); gpu->Flush(); // Intentionally don't trigger an upload here. CopyAroundDrawn(framebuf->addr, pushbuf_.data() + ptr + headerSize, pspSize, needed); } } // The recorder leaves VRAM out of a snapshot as unchanged when an earlier snapshot had it. Marks the render // target snapshots that were that earlier one, by 256-byte block as the recorder tracks it. void DumpExecute::MarkNeededTargets() { neededTargets_.assign(commands_.size(), false); if (version_ < 6) { return; } const u32 BLOCK_SHIFT = 8; const u32 VRAM_SIZE = 0x00200000; // For each block, the command whose snapshot last brought data there, and whether it was a render target's. std::vector lastSource(VRAM_SIZE >> BLOCK_SHIFT, -1); std::vector lastIsTarget(lastSource.size(), false); for (size_t i = 0; i < commands_.size(); i++) { const Command &cmd = commands_[i]; if (cmd.type < CommandType::FRAMEBUF0 || cmd.type > CommandType::FRAMEBUF7 || cmd.sz < 16) { continue; } const u32 *header = (const u32 *)(pushbuf_.data() + cmd.ptr); if (!Memory::IsVRAMAddress(header[0])) { continue; } const bool isTarget = (header[2] & 1) != 0; const bool unchangedVRAM = (header[2] & 2) != 0; const u32 start = header[0] & (VRAM_SIZE - 1); const u32 end = std::min(start + (cmd.sz - 16), VRAM_SIZE); for (u32 b = start >> BLOCK_SHIFT; b < (end + (1 << BLOCK_SHIFT) - 1) >> BLOCK_SHIFT; b++) { if (unchangedVRAM) { if (lastSource[b] >= 0 && lastIsTarget[b]) { neededTargets_[lastSource[b]] = true; } } else { lastSource[b] = (int)i; lastIsTarget[b] = isTarget; } } } } // The software renderer has the real contents of what this replay drew, while the dump's copy can be stale // (a hardware backend that didn't read it back, GTA LCS). So a copy leaves out the areas the replay's draws // could reach: a texture inside one (Princess Maker 5 textures from the right half of its 1024 wide // framebuffer), or one whose recorded size runs over a buffer drawn next to it (Rainbow Six copies its // 512x512 frame texture before each strip it draws into the display buffer inside that range). A texture // beside the drawn area still comes through (Burnout keeps one in columns 480-511). void DumpExecute::CopyAroundDrawn(u32 addr, const u8 *data, u32 size, bool linear) { // A texture recorded at its full size can run past the end of VRAM, into the depth swizzle mirror // (Princess Maker 5's 1024x1024 one at 0x04000400). That isn't the game's data. if (Memory::IsVRAMAddress(addr)) size = std::min(size, 0x00200000 - (addr & 0x001FFFFF)); std::vector> skip; const int64_t start = addr & 0x001FFFFF, end = start + size; // A snapshot at a depth mirror holds what the game saw through it (PPSSPP stored depth that way), so it goes // back through the mirror. Except a render target's that later snapshots rely on: those read its data at // linear addresses (Me & My Katamari textures a 512x512 one through 0x04288000, past its depth buffer). linear = linear && Memory::IsVRAMAddress(addr); if (g_Config.bSoftwareRendering) { for (const auto &it : drawnTargets_) { const DrawnTarget &t = it.second; if (t.strideBytes == 0) continue; for (const DrawnRect &r : t.rects) { for (int y = std::max(r.y1, 0); y <= r.y2; ++y) { const int64_t row = (int64_t)it.first + (int64_t)y * t.strideBytes; const int64_t a = row + (int64_t)std::max(r.x1, 0) * t.bpp; const int64_t b = row + (int64_t)(r.x2 + 1) * t.bpp; if (b > start && a < end) skip.emplace_back(std::max(a, start), std::min(b, end)); } } } std::sort(skip.begin(), skip.end()); } int64_t pos = start; auto copyTo = [&](int64_t until) { if (until > pos) { if (linear) { Memory::MemcpyUnchecked(0x04000000 + (u32)pos, data + (pos - start), (u32)(until - pos)); } else { CopyToMemory(addr + (u32)(pos - start), data + (pos - start), (u32)(until - pos)); } NotifyMemInfo(MemBlockFlags::WRITE, addr + (u32)(pos - start), (u32)(until - pos), "ReplayTex"); } }; for (const auto &s : skip) { copyTo(s.first); pos = std::max(pos, s.second); } copyTo(end); } void DumpExecute::Display(u32 ptr, u32 sz, bool allowFlip) { struct DisplayBufData { PSPPointer topaddr; int linesize, pixelFormat; }; DisplayBufData *disp = (DisplayBufData *)(pushbuf_.data() + ptr); // Sync up drawing. SyncStall(); // A display turned off shows nothing to compare, so the result falls back to the last framebuffer // drawn to, as for a dump without a DISPLAY (Auditorium 9213). The PSP replayer does the same. haveDisplay_ = disp->topaddr.ptr != 0; if (!haveDisplay_) return; __DisplaySetFramebuf(disp->topaddr.ptr, disp->linesize, disp->pixelFormat, 1); if (allowFlip) { __DisplaySetFramebuf(disp->topaddr.ptr, disp->linesize, disp->pixelFormat, 0); } } void DumpExecute::EdramTrans(u32 ptr, u32 sz) { uint32_t value; memcpy(&value, pushbuf_.data() + ptr, 4); // Sync up drawing. SyncStall(); if (gpu) gpu->SetAddrTranslation(value); } DumpExecute::~DumpExecute() { execMemcpyDest = 0; if (execListBuf) { userMemory.Free(execListBuf); execListBuf = 0; } execListPos = 0; mapping_.Reset(); } ReplayResult DumpExecute::Run() { // Start with the default value. if (gpu) gpu->SetAddrTranslation(0x400); if (resumeIndex_ >= 0) { SyncStall(); } int start = resumeIndex_ >= 0 ? resumeIndex_ : 0; for (size_t i = start; i < commands_.size(); i++) { if (g_cancelled) { break; } const Command &cmd = commands_[i]; // Only the first INIT is real. Before 71210f3fa2 a second dump request during a recording started // it again, writing another INIT (and initial CLUT) into the dump: the GE state of that moment, // which already includes register writes the dump only has after it. Applied, it ran the last // draws with the wrong state (GOD EATER BURST 13950 came out black, Street Riders 14746 garbled). if (cmd.type == CommandType::INIT && i != 0) { if (i + 1 < commands_.size() && commands_[i + 1].type == CommandType::CLUT) ++i; continue; } switch (cmd.type) { case CommandType::INIT: Init(cmd.ptr, cmd.sz); break; case CommandType::REGISTERS: Registers(cmd.ptr, cmd.sz); break; case CommandType::VERTICES: Vertices(cmd.ptr, cmd.sz); break; case CommandType::INDICES: Indices(cmd.ptr, cmd.sz); break; case CommandType::CLUTADDR: ClutAddr(cmd.ptr, cmd.sz); break; case CommandType::CLUT: Clut(cmd.ptr, cmd.sz); break; case CommandType::TRANSFERSRC: TransferSrc(cmd.ptr, cmd.sz); break; case CommandType::MEMSET: Memset(cmd.ptr, cmd.sz); break; case CommandType::MEMCPYDEST: MemcpyDest(cmd.ptr, cmd.sz); break; case CommandType::MEMCPYDATA: Memcpy(cmd.ptr, cmd.sz); break; case CommandType::EDRAMTRANS: EdramTrans(cmd.ptr, cmd.sz); break; case CommandType::TEXTURE0: case CommandType::TEXTURE1: case CommandType::TEXTURE2: case CommandType::TEXTURE3: case CommandType::TEXTURE4: case CommandType::TEXTURE5: case CommandType::TEXTURE6: case CommandType::TEXTURE7: Texture((int)cmd.type - (int)CommandType::TEXTURE0, cmd.ptr, cmd.sz); break; case CommandType::FRAMEBUF0: case CommandType::FRAMEBUF1: case CommandType::FRAMEBUF2: case CommandType::FRAMEBUF3: case CommandType::FRAMEBUF4: case CommandType::FRAMEBUF5: case CommandType::FRAMEBUF6: case CommandType::FRAMEBUF7: Framebuf((int)cmd.type - (int)CommandType::FRAMEBUF0, cmd.ptr, cmd.sz, neededTargets_[i]); break; case CommandType::DISPLAY: Display(cmd.ptr, cmd.sz, i == commands_.size() - 1); break; default: ERROR_LOG(Log::GeDebugger, "Unsupported GE dump command: %d", (int)cmd.type); return ReplayResult::Error; } } SubmitListEnd(); if (!haveDisplay_ && fbWidth_ != 0) { SyncStall(); const u32 addr = 0x04000000 | (fbPtr_ & 0x001FFFF0); __DisplaySetFramebuf(addr, fbWidth_ & 0x07FC, fbFormat_, 1); __DisplaySetFramebuf(addr, fbWidth_ & 0x07FC, fbFormat_, 0); } return ReplayResult::Done; } static bool ReadCompressed(u32 fp, void *dest, size_t sz, uint32_t version) { u32 compressed_size = 0; if (pspFileSystem.ReadFile(fp, (u8 *)&compressed_size, sizeof(compressed_size)) != sizeof(compressed_size)) { return false; } u8 *compressed = new u8[compressed_size]; if (pspFileSystem.ReadFile(fp, compressed, compressed_size) != compressed_size) { delete[] compressed; return false; } size_t real_size = sz; if (version < 5) snappy_uncompress((const char *)compressed, compressed_size, (char *)dest, &real_size); else real_size = ZSTD_decompress(dest, real_size, compressed, compressed_size); delete[] compressed; return real_size == sz; } static u32 LoadReplay(const std::string &filename) { PROFILE_THIS_SCOPE("ReplayLoad"); NOTICE_LOG(Log::GeDebugger, "LoadReplay %s", filename.c_str()); g_cancelled = false; u32 fp = pspFileSystem.OpenFile(filename, FILEACCESS_READ); Header header; pspFileSystem.ReadFile(fp, (u8 *)&header, sizeof(header)); u32 version = header.version; if (memcmp(header.magic, HEADER_MAGIC, sizeof(header.magic)) != 0 || header.version > VERSION || header.version < MIN_VERSION) { ERROR_LOG(Log::GeDebugger, "Invalid GE dump or unsupported version"); pspFileSystem.CloseFile(fp); return 0; } if (header.version <= 3) { pspFileSystem.SeekFile(fp, 12, FILEMOVE_BEGIN); memset(header.gameID, 0, sizeof(header.gameID)); } const std::string gameID(header.gameID, strnlen(header.gameID, sizeof(header.gameID))); // The ID picks the texture pack directory, among other things. if (IsSafePathComponent(gameID)) { g_paramSFO.SetValue("DISC_ID", gameID, (int)sizeof(header.gameID)); std::vector info; std::string gameTitle = "(unknown title)"; #if !defined(__LIBRETRO__) if (g_gameDB.GetGameInfos(gameID, &info)) { gameTitle = info[0].title; g_paramSFO.SetValue("TITLE", gameTitle, (int)gameTitle.size()); } #endif System_SetWindowTitle(g_paramSFO.GetValueString("DISC_ID") + " : " + gameTitle + " (GE frame dump)"); } else { System_SetWindowTitle("(GE frame dump: old format, missing DISC_ID)"); } u32 sz = 0; pspFileSystem.ReadFile(fp, (u8 *)&sz, sizeof(sz)); u32 bufsz = 0; pspFileSystem.ReadFile(fp, (u8 *)&bufsz, sizeof(bufsz)); lastExecCommands.resize(sz); lastExecPushbuf.resize(bufsz); bool truncated = false; truncated = truncated || !ReadCompressed(fp, lastExecCommands.data(), sizeof(Command) * sz, header.version); truncated = truncated || !ReadCompressed(fp, lastExecPushbuf.data(), bufsz, header.version); pspFileSystem.CloseFile(fp); if (truncated) { ERROR_LOG(Log::GeDebugger, "Truncated GE dump detected - can't replay"); return 0; } lastExecFilename = filename; lastExecVersion = version; return version; } void Replay_Unload() { // We might be paused inside a replay - in this case, the thread is still running and we need to tell it to stop. if (replayThread.joinable()) { { // We just finish processing the commands until done. g_cancelled = true; std::unique_lock lock(opFinishLock); opFinishWait.notify_one(); } replayThread.join(); } _dbg_assert_(!replayThread.joinable()); lastExecFilename.clear(); lastExecVersion = 0; lastExecCommands.clear(); lastExecPushbuf.clear(); g_opDone = true; g_retVal = 0; } void WriteRunDumpCode(u32 codeStart) { // NOTE: Not static, since parts are run-time computed (MIPS_MAKE_SYSCALL etc) const u32 runDumpCode[] = { // Save the filename. MIPS_MAKE_ORI(MIPS_REG_S0, MIPS_REG_A0, 0), MIPS_MAKE_ORI(MIPS_REG_S1, MIPS_REG_A1, 0), // Call the actual render. Jump here to start over. MIPS_MAKE_SYSCALL("FakeSysCalls", "__KernelGPUReplay"), MIPS_MAKE_NOP(), // Re-run immediately if requested by the return value from __KernelGPUReplay MIPS_MAKE_BNEZ(codeStart + 4 * 4, codeStart + 8, MIPS_REG_V0), MIPS_MAKE_NOP(), // When done (__KernelGPUReplay returned 0), make sure we don't get out of sync (is this needed?) MIPS_MAKE_LUI(MIPS_REG_A0, 0), MIPS_MAKE_SYSCALL("sceGe_user", "sceGeDrawSync"), MIPS_MAKE_NOP(), // Wait for the next vblank to render again, then (through the delay slot) jump right back up to __KernelGPUReplay. MIPS_MAKE_SYSCALL("sceDisplay", "sceDisplayWaitVblankStart"), MIPS_MAKE_NOP(), MIPS_MAKE_J(codeStart + 8), MIPS_MAKE_NOP(), // This never gets reached, just here to be "safe". MIPS_MAKE_BREAK(0), }; for (size_t i = 0; i < ARRAY_SIZE(runDumpCode); ++i) { Memory::WriteUnchecked_U32(runDumpCode[i], codeStart + (u32)i * sizeof(u32_le)); } } // This is called by the syscall. It spawns a "replayThread" which parses the file and sends the commands. // A long term goal is inversion of control here, but it's tricky for a number of reasons that you'll find // out if you try. ReplayResult RunMountedReplay(const std::string &filename) { _assert_msg_(!gpu->GetRecorder()->IsActivePending(), "Cannot run replay while recording."); uint32_t version = lastExecVersion; if (lastExecFilename != filename) { // Does this ever happen? Can the filename change, without going through core shutdown/startup? if (replayThread.joinable()) { replayThread.join(); } version = LoadReplay(filename); if (!version) { ERROR_LOG(Log::GeDebugger, "bad version %08x", version); return ReplayResult::Error; } } if (g_opToExec.type != OpType::None) { std::unique_lock waitLock(opFinishLock); g_opDone = true; g_opToExec = Operation{ OpType::None }; opFinishWait.notify_one(); } if (!replayThread.joinable()) { _dbg_assert_(g_opToExec.type == OpType::None); g_opToExec = Operation{ OpType::None }; replayThread = std::thread([version]() { SetCurrentThreadName("Replay"); DumpExecute executor(lastExecPushbuf, lastExecCommands, version); GPURecord::ReplayResult retval = executor.Run(); // Finish up ExecuteOnMain(Operation{ OpType::Done }); }); } // OK, now wait for and perform the desired action. { std::unique_lock lock(opStartLock); g_condOpStartWait.wait(lock, []() { return g_opToExec.type != OpType::None; }); } switch (g_opToExec.type) { case OpType::UpdateStallAddr: { bool runList; hleEatCycles(190); hleCoreTimingForceCheck(); gpu->UpdateStall(g_opToExec.listID, g_opToExec.param, &runList); if (runList) { hleSplitSyscallOverGe(); } // We're not done yet, request another go. return ReplayResult::Break; } case OpType::EnqueueList: { bool runList; u32 listPC = g_opToExec.listID; u32 execListPos = g_opToExec.param; auto optParam = PSPPointer::Create(0); g_retVal = gpu->EnqueueList(listPC, execListPos, -1, optParam, false, &runList); if (runList) { hleSplitSyscallOverGe(); } // We're not done yet, request another go. hleEatCycles(490); hleCoreTimingForceCheck(); return ReplayResult::Break; } case OpType::ReapplyGfxState: { // try again but no need to split the sys call gpu->ReapplyGfxState(); return ReplayResult::Break; } case OpType::ListSync: { u32 execListID = g_opToExec.listID; u32 mode = g_opToExec.param; // try again but no need to split the sys call hleEatCycles(220); gpu->ListSync(execListID, mode); return ReplayResult::Break; } case OpType::Done: { _dbg_assert_(replayThread.joinable()); { std::unique_lock lock(opFinishLock); g_opDone = true; opFinishWait.notify_one(); } replayThread.join(); g_opToExec = { OpType::None }; break; } case OpType::None: break; } return ReplayResult::Done; } } // namespace GPURecord