diff --git a/Core/Debugger/WebSocket/GameSubscriber.cpp b/Core/Debugger/WebSocket/GameSubscriber.cpp index 909942b4d6..9d3601888f 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.cpp +++ b/Core/Debugger/WebSocket/GameSubscriber.cpp @@ -57,10 +57,18 @@ struct SaveStateResult { bool done = false; SaveState::Status status = SaveState::Status::FAILURE; std::string message; + std::string metadata; +}; + +enum class StateOp { + LOAD, + SAVE, + REWIND, + VERIFY, }; struct WebSocketGameState : public DebuggerSubscriber { - void StateLoadSave(DebuggerRequest &req, bool save); + void StateOperation(DebuggerRequest &req, StateOp op); void Broadcast(net::WebSocketServer *ws) override; private: @@ -74,23 +82,32 @@ private: } // namespace -// Load a savestate file (game.state.load), or save one (game.state.save) +// Load a savestate file (game.state.load), save one (game.state.save), rewind to the newest +// rewind snapshot (game.state.rewind), or check that saving and reading back the current state +// agree (game.state.verify) // // 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. // +// Rewind snapshots are only taken with the RewindSnapshotInterval setting on. Each rewind uses up +// the snapshot it restores, so repeating it goes further back. +// // Parameters: -// - path: string, path to the .ppst file. +// - path: string, path to the .ppst file (only for load and save). // - 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"; +// mode refuses it, or there's nothing to rewind to. With wait, it's sent once done instead, with +// message and metadata strings (for a rewind, how long ago the snapshot was taken), or an error +// if the operation failed. +void WebSocketGameState::StateOperation(DebuggerRequest &req, StateOp op) { + static const char *const events[] = { "game.state.load", "game.state.save", "game.state.rewind", "game.state.verify" }; + const char *event = events[(int)op]; std::string path; bool wait = false; - if (!req.ParamString("path", &path) || !req.ParamBool("wait", &wait, DebuggerParamType::OPTIONAL)) + if ((op == StateOp::LOAD || op == StateOp::SAVE) && !req.ParamString("path", &path)) + return; + if (!req.ParamBool("wait", &wait, DebuggerParamType::OPTIONAL)) return; // The queued operation holds the only other reference, so Broadcast() can also tell when it's @@ -98,7 +115,7 @@ void WebSocketGameState::StateLoadSave(DebuggerRequest &req, bool save) { std::shared_ptr result; if (wait) result = std::make_shared(); - auto callback = [event, result](SaveState::Status status, std::string_view message, std::string_view) { + auto callback = [event, result](SaveState::Status status, std::string_view message, std::string_view metadata) { if (status == SaveState::Status::FAILURE) { ERROR_LOG(Log::System, "%s: %.*s", event, STR_VIEW(message)); } @@ -106,22 +123,43 @@ void WebSocketGameState::StateLoadSave(DebuggerRequest &req, bool save) { std::lock_guard guard(result->lock); result->status = status; result->message = message; + result->metadata = metadata; result->done = true; } }; bool running = false; + bool canRewind = true; 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; + } + switch (op) { + case StateOp::LOAD: + queued = SaveState::Load(Path(path), -1, callback); + break; + case StateOp::SAVE: + queued = SaveState::Save(Path(path), -1, callback); + break; + case StateOp::REWIND: + canRewind = SaveState::CanRewind(); + if (canRewind) { + queued = SaveState::Rewind(callback); + } + break; + case StateOp::VERIFY: + queued = SaveState::Verify(callback); + break; } }); if (!running) return req.Fail("Game not running"); + if (!canRewind) + return req.Fail("No rewind snapshots"); if (!queued) { - return req.Fail(save ? "Could not queue savestate save" : "Could not queue savestate load"); + return req.Fail(std::string("Could not queue ") + event); } if (!wait) { @@ -136,17 +174,22 @@ 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) { + // Our own reference, since erasing the entry below must not free the mutex we're holding. + std::shared_ptr result = it->result; + std::lock_guard guard(result->lock); + if (!result->done && !dropped) { ++it; continue; } - bool failed = !it->result->done || it->result->status == SaveState::Status::FAILURE; + bool failed = !result->done || 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"); + j.writeString("message", result->done ? result->message : "Savestate operation was dropped"); + if (result->done) { + j.writeString("metadata", result->metadata); + } if (failed) { j.writeInt("level", (int)LogLevel::LERROR); } @@ -173,8 +216,10 @@ DebuggerSubscriber *WebSocketGameInit(DebuggerEventHandlerMap &map) { auto p = new WebSocketGameState(); map["game.reset"] = &WebSocketGameReset; map["game.status"] = &WebSocketGameStatus; - map["game.state.load"] = [p](DebuggerRequest &req) { p->StateLoadSave(req, false); }; - map["game.state.save"] = [p](DebuggerRequest &req) { p->StateLoadSave(req, true); }; + map["game.state.load"] = [p](DebuggerRequest &req) { p->StateOperation(req, StateOp::LOAD); }; + map["game.state.save"] = [p](DebuggerRequest &req) { p->StateOperation(req, StateOp::SAVE); }; + map["game.state.rewind"] = [p](DebuggerRequest &req) { p->StateOperation(req, StateOp::REWIND); }; + map["game.state.verify"] = [p](DebuggerRequest &req) { p->StateOperation(req, StateOp::VERIFY); }; map["game.speed.get"] = &WebSocketGameSpeedGet; map["game.speed.set"] = &WebSocketGameSpeedSet; map["version"] = &WebSocketVersion; diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 3ca6b81899..a756c9226a 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; 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` | +| 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.state.rewind` (the same for the newest rewind snapshot, which it uses up; needs `RewindSnapshotInterval` set, and the `wait` response's `metadata` says how long ago the snapshot was taken), `game.state.verify` (the same for checking that saving the current state and reading it back agree), `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` |