mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-20 17:59:08 +02:00
Debugger: Add wait to game.state.load/save
With wait: true, respond once the savestate operation has finished (with its message), or with an error if it failed or was dropped without running. Otherwise a client only learns the operation was queued. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
681a60b3ca
commit
4bfeb59fcd
3 files changed
+118
-75
No files matched your search
@@ -29,6 +29,9 @@
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#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<SaveStateResult> result;
|
||||
};
|
||||
std::vector<Waiting> 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<SaveStateResult> result;
|
||||
if (wait)
|
||||
result = std::make_shared<SaveStateResult>();
|
||||
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<std::mutex> 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<std::mutex> 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) {
|
||||
|
||||
@@ -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);
|
||||
@@ -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` |
|
||||
|
||||
Reference in new issue
Block a user