mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-03 04:52:14 +01:00
Debugger: Add game.state.rewind and game.state.verify
Same as game.state.load/save, and the wait response now carries the metadata string (for a rewind, how long ago the snapshot was taken). Also fixes Broadcast() erasing a waiting entry while holding its mutex, which could free the mutex while locked (the debug CRT aborts on it). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e3916c0acc
commit
79c2b9b0fd
2 files changed
+64
-19
No files matched your search
@@ -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<SaveStateResult> result;
|
||||
if (wait)
|
||||
result = std::make_shared<SaveStateResult>();
|
||||
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<std::mutex> 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<std::mutex> 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<SaveStateResult> result = it->result;
|
||||
std::lock_guard<std::mutex> 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;
|
||||
|
||||
Reference in new issue
Block a user