From 3a669891dcd077c60ca4226acbd7e15c56101772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 17 Aug 2026 00:40:38 +0200 Subject: [PATCH] Replay and GPUDisasm subscribers: read core state on the CPU thread replay.* mutates and reads replay/RTC state that the CPU thread consumes as it runs, and gpu.displaylist.disasm reads through the gpu pointer and emulated memory. Both did it straight from the WebSocket thread. Route through Core_RunOnCPUThread(), and fold the "is a game running" checks into the same trip rather than testing before it, where the answer could already be stale. Part of removing the WebSocket debugger's lifecycleLock. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 --- .../WebSocket/GPUDisasmSubscriber.cpp | 20 +++--- Core/Debugger/WebSocket/ReplaySubscriber.cpp | 72 ++++++++++++++----- 2 files changed, 66 insertions(+), 26 deletions(-) diff --git a/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp b/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp index 37d050c5f5..18071b98d3 100644 --- a/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp +++ b/Core/Debugger/WebSocket/GPUDisasmSubscriber.cpp @@ -20,6 +20,7 @@ #include "Common/StringUtils.h" #include "Core/Debugger/WebSocket/GPUDisasmSubscriber.h" #include "Core/Debugger/WebSocket/WebSocketUtils.h" +#include "Core/Core.h" #include "Core/MemMap.h" #include "GPU/GPU.h" #include "GPU/GPUCommon.h" @@ -58,13 +59,6 @@ DebuggerSubscriber *WebSocketGPUDisasmInit(DebuggerEventHandlerMap &map) { // "AAAAAAAA desc" - meant for skimming a display list by eye instead of parsing full JSON, // same idea as memory.disasm's own compact mode. void WebSocketGPUDisplayListDisasm(DebuggerRequest &req) { - if (!gpu) { - return req.Fail("No GPU active (game not booted?)"); - } - if (!Memory::IsActive()) { - return req.Fail("Memory not active"); - } - // Mirrors memory.disasm's own limit - keeps a client typo (e.g. count=0xFFFFFFFF) from // blocking the debugger connection for an unreasonable amount of time. static const uint32_t MAX_RANGE = 10000; @@ -89,7 +83,17 @@ void WebSocketGPUDisplayListDisasm(DebuggerRequest &req) { if (!req.ParamBool("compact", &compact, DebuggerParamType::OPTIONAL)) return; - std::vector ops = gpu->DisassembleOpRange(start, end); + // gpu and the memory it disassembles from are CPU-thread-owned, so do the read over there + // rather than from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h. + bool active = false; + std::vector ops; + Core_RunOnCPUThread([&] { + active = gpu && Memory::IsActive(); + if (active) + ops = gpu->DisassembleOpRange(start, end); + }); + if (!active) + return req.Fail("No GPU active (game not booted?)"); JsonWriter &json = req.Respond(); json.pushArray("lines"); diff --git a/Core/Debugger/WebSocket/ReplaySubscriber.cpp b/Core/Debugger/WebSocket/ReplaySubscriber.cpp index 9d518630eb..3bc54aaac8 100644 --- a/Core/Debugger/WebSocket/ReplaySubscriber.cpp +++ b/Core/Debugger/WebSocket/ReplaySubscriber.cpp @@ -19,6 +19,7 @@ #include "Common/Data/Encoding/Base64.h" #include "Common/Swap.h" #include "Core/HLE/sceRtc.h" +#include "Core/Core.h" #include "Core/Replay.h" #include "Core/System.h" #include "Core/Debugger/WebSocket/ReplaySubscriber.h" @@ -45,7 +46,12 @@ DebuggerSubscriber *WebSocketReplayInit(DebuggerEventHandlerMap &map) { // // Response (same event name) with no extra data. void WebSocketReplayBegin(DebuggerRequest &req) { - ReplayBeginSave(); + // Replay state is consumed by the CPU thread as it runs, so mutate it over there rather than + // from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h. Same for the rest + // of the handlers below. + Core_RunOnCPUThread([&] { + ReplayBeginSave(); + }); req.Respond(); } @@ -57,7 +63,9 @@ void WebSocketReplayBegin(DebuggerRequest &req) { // // Response (same event name) with no extra data. void WebSocketReplayAbort(DebuggerRequest &req) { - ReplayAbort(); + Core_RunOnCPUThread([&] { + ReplayAbort(); + }); req.Respond(); } @@ -71,11 +79,15 @@ void WebSocketReplayAbort(DebuggerRequest &req) { // - version: unsigned integer, version number of data. // - base64: base64 encode of binary data. void WebSocketReplayFlush(DebuggerRequest &req) { - if (PSP_GetBootState() != BootState::Complete) - return req.Fail("Game not running"); - + bool running = false; std::vector data; - ReplayFlushBlob(&data); + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + if (running) + ReplayFlushBlob(&data); + }); + if (!running) + return req.Fail("Game not running"); JsonWriter &json = req.Respond(); json.writeInt("version", ReplayVersion()); @@ -90,9 +102,6 @@ void WebSocketReplayFlush(DebuggerRequest &req) { // // Response (same event name) with no extra data. void WebSocketReplayExecute(DebuggerRequest &req) { - if (PSP_GetBootState() != BootState::Complete) - return req.Fail("Game not running"); - uint32_t version = -1; if (!req.ParamU32("version", &version)) return; @@ -101,7 +110,16 @@ void WebSocketReplayExecute(DebuggerRequest &req) { return; std::vector data = Base64Decode(encoded.data(), encoded.size()); - if (!ReplayExecuteBlob(version, data)) + bool running = false; + bool ok = false; + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + if (running) + ok = ReplayExecuteBlob(version, data); + }); + if (!running) + return req.Fail("Game not running"); + if (!ok) return req.Fail("Invalid replay data or version"); req.Respond(); @@ -115,9 +133,15 @@ void WebSocketReplayExecute(DebuggerRequest &req) { // - executing: boolean if a replay is being executed. // - saving: boolean if a replay is being recorded. void WebSocketReplayStatus(DebuggerRequest &req) { + bool executing = false, saving = false; + Core_RunOnCPUThread([&] { + executing = ReplayIsExecuting(); + saving = ReplayIsSaving(); + }); + JsonWriter &json = req.Respond(); - json.writeBool("executing", ReplayIsExecuting()); - json.writeBool("saving", ReplayIsSaving()); + json.writeBool("executing", executing); + json.writeBool("saving", saving); } // Get the base RTC (real time clock) time for replay data (replay.time.get) @@ -130,11 +154,18 @@ void WebSocketReplayStatus(DebuggerRequest &req) { // Response (same event name): // - value: unsigned integer, may have more than 32 integer bits. void WebSocketReplayTimeGet(DebuggerRequest &req) { - if (PSP_GetBootState() != BootState::Complete) + bool running = false; + uint32_t baseTime = 0; + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + if (running) + baseTime = RtcBaseTime(); + }); + if (!running) return req.Fail("Game not running"); JsonWriter &json = req.Respond(); - json.writeUint("value", RtcBaseTime()); + json.writeUint("value", baseTime); } // Overwrite the base RTC time (replay.time.set) @@ -144,14 +175,19 @@ void WebSocketReplayTimeGet(DebuggerRequest &req) { // // Response (same event name) with no extra data. void WebSocketReplayTimeSet(DebuggerRequest &req) { - if (PSP_GetBootState() != BootState::Complete) - return req.Fail("Game not running"); - uint32_t value; if (!req.ParamU32("value", &value, false)) { return; } - RtcSetBaseTime((int32_t)value); + bool running = false; + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + if (running) + RtcSetBaseTime((int32_t)value); + }); + if (!running) + return req.Fail("Game not running"); + req.Respond(); }