diff --git a/Core/Debugger/WebSocket/GameSubscriber.cpp b/Core/Debugger/WebSocket/GameSubscriber.cpp index 10739b20b9..909942b4d6 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.cpp +++ b/Core/Debugger/WebSocket/GameSubscriber.cpp @@ -29,6 +29,9 @@ #include #endif +#include +#include + #include "Common/System/System.h" #include "Core/Config.h" #include "Core/Core.h" @@ -46,6 +49,116 @@ // DebuggerParamType::OPTIONAL, exactly the problem the note above is avoiding. bool NetworkAllowSpeedControl(); +namespace { + +// Filled in by the savestate callback, on the emu thread. +struct SaveStateResult { + std::mutex lock; + bool done = false; + SaveState::Status status = SaveState::Status::FAILURE; + std::string message; +}; + +struct WebSocketGameState : public DebuggerSubscriber { + void StateLoadSave(DebuggerRequest &req, bool save); + void Broadcast(net::WebSocketServer *ws) override; + +private: + struct Waiting { + const char *event; + std::string ticket; + std::shared_ptr result; + }; + std::vector waiting_; +}; + +} // namespace + +// Load a savestate file (game.state.load), or save one (game.state.save) +// +// The operation is queued, and happens at the end of the next frame the CPU runs, so follow this +// with something that runs it, like cpu.runUntilTime. +// +// Parameters: +// - path: string, path to the .ppst file. +// - wait: optional boolean, respond when the operation has finished rather than when queued. +// +// Response (same event name) once queued, or an error if the network or achievements hardcore +// mode refuses it. With wait, it's sent once done instead, with a message string, or an error +// if the load or save failed. +void WebSocketGameState::StateLoadSave(DebuggerRequest &req, bool save) { + const char *event = save ? "game.state.save" : "game.state.load"; + std::string path; + bool wait = false; + if (!req.ParamString("path", &path) || !req.ParamBool("wait", &wait, DebuggerParamType::OPTIONAL)) + return; + + // The queued operation holds the only other reference, so Broadcast() can also tell when it's + // dropped without ever running (at shutdown, for one). + std::shared_ptr result; + if (wait) + result = std::make_shared(); + auto callback = [event, result](SaveState::Status status, std::string_view message, std::string_view) { + if (status == SaveState::Status::FAILURE) { + ERROR_LOG(Log::System, "%s: %.*s", event, STR_VIEW(message)); + } + if (result) { + std::lock_guard guard(result->lock); + result->status = status; + result->message = message; + result->done = true; + } + }; + + bool running = false; + bool queued = false; + Core_RunOnCPUThread([&] { + running = PSP_GetBootState() == BootState::Complete; + if (running) { + queued = save ? SaveState::Save(Path(path), -1, callback) : SaveState::Load(Path(path), -1, callback); + } + }); + if (!running) + return req.Fail("Game not running"); + if (!queued) { + return req.Fail(save ? "Could not queue savestate save" : "Could not queue savestate load"); + } + + if (!wait) { + req.Respond(); + return; + } + const JsonNode *ticket = req.data.get("ticket"); + waiting_.push_back({ event, ticket ? json_stringify(ticket) : "", result }); +} + +void WebSocketGameState::Broadcast(net::WebSocketServer *ws) { + for (auto it = waiting_.begin(); it != waiting_.end(); ) { + // Check this first: if the callback ran, it was done before letting go. + bool dropped = it->result.use_count() == 1; + std::lock_guard guard(it->result->lock); + if (!it->result->done && !dropped) { + ++it; + continue; + } + + bool failed = !it->result->done || it->result->status == SaveState::Status::FAILURE; + JsonWriter j; + j.begin(); + j.writeString("event", failed ? "error" : it->event); + j.writeString("message", it->result->done ? it->result->message : "Savestate operation was dropped"); + if (failed) { + j.writeInt("level", (int)LogLevel::LERROR); + } + if (!it->ticket.empty()) { + j.writeRaw("ticket", it->ticket); + } + j.end(); + ws->Send(j.str()); + it = waiting_.erase(it); + } +} + static uint32_t GetOwnProcessID() { #if PPSSPP_PLATFORM(UWP) return (uint32_t)GetCurrentProcessId(); @@ -57,15 +170,16 @@ static uint32_t GetOwnProcessID() { } DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map) { + auto p = new WebSocketGameState(); map["game.reset"] = &WebSocketGameReset; map["game.status"] = &WebSocketGameStatus; - map["game.state.load"] = &WebSocketGameStateLoad; - map["game.state.save"] = &WebSocketGameStateSave; + map["game.state.load"] = [p](DebuggerRequest &req) { p->StateLoadSave(req, false); }; + map["game.state.save"] = [p](DebuggerRequest &req) { p->StateLoadSave(req, true); }; map["game.speed.get"] = &WebSocketGameSpeedGet; map["game.speed.set"] = &WebSocketGameSpeedSet; map["version"] = &WebSocketVersion; - return nullptr; + return p; } // Reset emulation (game.reset) @@ -128,75 +242,6 @@ void WebSocketGameStatus(DebuggerRequest &req) { }); } -// Load a savestate file (game.state.load) -// -// The load is queued, and happens at the end of the next frame the CPU runs, so follow this with -// something that runs it, like cpu.runUntilTime. -// -// Parameters: -// - path: string, path to the .ppst file. -// -// Response (same event name) with no extra data, once queued, or an error if the network or -// achievements hardcore mode refuses it. -void WebSocketGameStateLoad(DebuggerRequest &req) { - std::string path; - if (!req.ParamString("path", &path)) - return; - - bool running = false; - bool queued = false; - Core_RunOnCPUThread([&] { - running = PSP_GetBootState() == BootState::Complete; - if (running) { - queued = SaveState::Load(Path(path), -1, [](SaveState::Status status, std::string_view message, std::string_view) { - if (status == SaveState::Status::FAILURE) { - ERROR_LOG(Log::System, "game.state.load: %.*s", STR_VIEW(message)); - } - }); - } - }); - if (!running) - return req.Fail("Game not running"); - if (!queued) { - return req.Fail("Could not queue savestate load"); - } - req.Respond(); -} - -// Save a savestate file (game.state.save) -// -// Like game.state.load, the save is queued and happens at the end of the next frame the CPU runs. -// -// Parameters: -// - path: string, path to write the .ppst file to. -// -// Response (same event name) with no extra data, once queued, or an error if the network or -// achievements hardcore mode refuses it. -void WebSocketGameStateSave(DebuggerRequest &req) { - std::string path; - if (!req.ParamString("path", &path)) - return; - - bool running = false; - bool queued = false; - Core_RunOnCPUThread([&] { - running = PSP_GetBootState() == BootState::Complete; - if (running) { - queued = SaveState::Save(Path(path), -1, [](SaveState::Status status, std::string_view message, std::string_view) { - if (status == SaveState::Status::FAILURE) { - ERROR_LOG(Log::System, "game.state.save: %.*s", STR_VIEW(message)); - } - }); - } - }); - if (!running) - return req.Fail("Game not running"); - if (!queued) { - return req.Fail("Could not queue savestate save"); - } - req.Respond(); -} - // Percentages are relative to 60 FPS, matching how the in-app settings present the alternative // speeds (see GameSettingsScreen.cpp) - so a client and the UI agree on what "200%" means. static int PercentToFps(int percent) { diff --git a/Core/Debugger/WebSocket/GameSubscriber.h b/Core/Debugger/WebSocket/GameSubscriber.h index 220ba2ecf2..68943c1af7 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.h +++ b/Core/Debugger/WebSocket/GameSubscriber.h @@ -23,8 +23,6 @@ DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map); void WebSocketGameReset(DebuggerRequest &req); void WebSocketGameStatus(DebuggerRequest &req); -void WebSocketGameStateLoad(DebuggerRequest &req); -void WebSocketGameStateSave(DebuggerRequest &req); void WebSocketGameSpeedGet(DebuggerRequest &req); void WebSocketGameSpeedSet(DebuggerRequest &req); void WebSocketVersion(DebuggerRequest &req); diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 4054ae7a15..3ca6b81899 100644 --- a/docs/WebSocketDebugger.md +++ b/docs/WebSocketDebugger.md @@ -271,7 +271,7 @@ file - this is just an index. | Category | Events | File | |---|---|---| -| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file, applied at the end of the next frame the CPU runs), `game.state.save` (the same for saving one), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `version` | `GameSubscriber.cpp` | +| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file, applied at the end of the next frame the CPU runs; with `wait: true` the response comes once it's done, or as an error if it failed), `game.state.save` (the same for saving one), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `version` | `GameSubscriber.cpp` | | CPU core | `cpu.stepping`, `cpu.resume`, `cpu.status` (reports `ticks` plus `us`, emulated microseconds, and `clockHz` - use `us` to line up with wall-clock timings, since games change the clock frequency and the ticks-per-second ratio isn't fixed), `cpu.getAllRegs`, `cpu.getReg`, `cpu.setReg`, `cpu.evaluate` | `CPUCoreSubscriber.cpp` | | Stepping | `cpu.stepInto`, `cpu.stepOver`, `cpu.stepOut`, `cpu.runUntil`, `cpu.runUntilTime` (run until a point in emulated time - `us` absolute or `relativeUs` from now - and break there; this is how to get a scripted repro reproducibly "N seconds into the game" instead of polling `cpu.status` in a loop), `cpu.nextHLE` | `SteppingSubscriber.cpp` | | Breakpoints | `cpu.breakpoint.add/update/remove/list`, `memory.breakpoint.add/update/remove/list`, `cpu.regBreakpoint.add/update/remove/list` (break when a register is written to, by any instruction anywhere - currently GPRs only; interpreter-only, no effect under a JIT backend) | `BreakpointSubscriber.cpp` |