diff --git a/Core/Debugger/WebSocket/GameSubscriber.cpp b/Core/Debugger/WebSocket/GameSubscriber.cpp index 9d3601888f..c925b724ec 100644 --- a/Core/Debugger/WebSocket/GameSubscriber.cpp +++ b/Core/Debugger/WebSocket/GameSubscriber.cpp @@ -89,8 +89,9 @@ private: // 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. +// Rewind snapshots are only taken with the RewindSnapshotInterval setting on. A rewind restores the +// newest snapshot and keeps it, so a later rewind comes back to it, but one within 0.8 seconds of +// the last steps further back. // // Parameters: // - path: string, path to the .ppst file (only for load and save). diff --git a/Core/SaveStateRewind.cpp b/Core/SaveStateRewind.cpp index 8129f4d509..224767d137 100644 --- a/Core/SaveStateRewind.cpp +++ b/Core/SaveStateRewind.cpp @@ -4,6 +4,7 @@ #include "Core/SaveState.h" #include "Core/SaveStateRewind.h" #include "Core/Core.h" +#include "Core/CoreTiming.h" #include "Core/Config.h" namespace SaveState { @@ -36,7 +37,7 @@ CChunkFileReader::Error StateRingbuffer::Save() { if (err == CChunkFileReader::ERROR_NONE) { ScheduleCompress(&states_[n].stateBuffer, compressBuffer, &bases_[base_]); - states_[n].savedTime = time_now_d(); + states_[n].emuTime = CoreTiming::GetGlobalTimeUs() / 1000000.0; baseMapping_[n] = baseGeneration_[base_]; next_ = (next_ + 1) % size_; if (count_ < size_) { @@ -45,6 +46,8 @@ CChunkFileReader::Error StateRingbuffer::Save() { } else { states_[n].clear(); } + // The state a rewind restored isn't the newest any more, so the next rewind comes back to this one. + newestRestored_ = false; // States compressed against a base that has since been replaced can't be decoded any more. // They're the oldest ones, so drop them from the end. @@ -74,18 +77,26 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std:: if (Empty()) return CChunkFileReader::ERROR_BAD_FILE; + // Restoring a state doesn't use it up: rewind, play on a bit, rewind again, and you're back at the + // same state. Rewinding again quickly steps further back instead (except from the oldest state). + const double now = time_now_d(); + const double emuNow = CoreTiming::GetGlobalTimeUs() / 1000000.0; + const bool quick = newestRestored_ && now - lastRestoreTime_ < STEP_BACK_SECONDS; + if (quick) { + if (count_ > 1) { + DropNewest(); + } + } else if (!retryingAfterFailure_) { + rewindFromEmuTime_ = emuNow; + } + retryingAfterFailure_ = false; + const int n = (next_ - 1 + size_) % size_; - next_ = n; - count_--; - if (states_[n].empty()) - return CChunkFileReader::ERROR_BAD_FILE; - - auto pa = GetI18NCategory(I18NCat::PAUSE); - const int generation = baseMapping_[n]; - if (!BaseAlive(generation)) { - // Save() drops these, so this shouldn't happen. - WARN_LOG(Log::SaveState, "Rewind: state %d was compressed against a base that's gone", n); + if (states_[n].empty() || !BaseAlive(generation)) { + // Save() drops states whose base is gone, so this shouldn't happen. + WARN_LOG(Log::SaveState, "Rewind: state %d can't be decoded", n); + DropNewest(); return CChunkFileReader::ERROR_BAD_FILE; } const int baseSlot = generation % (int)ARRAY_SIZE(bases_); @@ -93,19 +104,39 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std:: static std::vector buffer; LockedDecompress(buffer, states_[n].stateBuffer, bases_[baseSlot]); CChunkFileReader::Error error = LoadFromRam(buffer, errorString); - *metadata = pa->T("Rewind"); + if (error != CChunkFileReader::ERROR_NONE) { + // Don't come back to it. HandleLoadFailure() tries the next one, and after a partial load the + // clock is meaningless, so that keeps rewindFromEmuTime_. + DropNewest(); + retryingAfterFailure_ = true; + return error; + } + newestRestored_ = true; + lastRestoreTime_ = now; - if (states_[n].savedTime) { + auto pa = GetI18NCategory(I18NCat::PAUSE); + *metadata = pa->T("Rewind"); + // In emulated time, from where the rewinding started: wall-clock time would count pauses, + // fast-forward, and the time spent in states rewound to earlier. + const double secondsBack = rewindFromEmuTime_ - states_[n].emuTime; + if (secondsBack >= 0.0) { auto di = GetI18NCategory(I18NCat::DIALOG); metadata->append(" ("); - metadata->append(ApplySafeSubstitutions(di->T("%1 seconds ago"), static_cast(time_now_d() - states_[n].savedTime))); + metadata->append(ApplySafeSubstitutions(di->T("%1 seconds ago"), (int)(secondsBack + 0.5))); metadata->append(")"); } - rewindLastTime_ = time_now_d(); + rewindLastTime_ = now; return error; } +void StateRingbuffer::DropNewest() { + next_ = (next_ - 1 + size_) % size_; + states_[next_].clear(); + count_--; + newestRestored_ = false; +} + void StateRingbuffer::ScheduleCompress(std::vector *result, const std::vector *state, const std::vector *base) { if (compressThread_.joinable()) compressThread_.join(); @@ -193,6 +224,8 @@ void StateRingbuffer::Clear() { buffer_.clear(); base_ = -1; baseUsage_ = 0; + newestRestored_ = false; + retryingAfterFailure_ = false; rewindLastTime_ = time_now_d(); } diff --git a/Core/SaveStateRewind.h b/Core/SaveStateRewind.h index dfae060b8b..66626f28f8 100644 --- a/Core/SaveStateRewind.h +++ b/Core/SaveStateRewind.h @@ -45,20 +45,24 @@ public: private: bool BaseAlive(int generation) const; + void DropNewest(); const int BLOCK_SIZE = 8192; const int REWIND_NUM_STATES = 20; // TODO: Instead, based on size of compressed state? const int BASE_USAGE_INTERVAL = 15; + // A rewind this soon after the last one steps back past the state that one restored. + const double STEP_BACK_SECONDS = 0.8; typedef std::vector StateBuffer; struct RewindState { StateBuffer stateBuffer; - double savedTime; + // CoreTiming's clock when the state was taken, in seconds. + double emuTime = 0.0; bool empty() const { return stateBuffer.empty(); } - void clear() { stateBuffer.clear(); savedTime = 0.0; } + void clear() { stateBuffer.clear(); emuTime = 0.0; } }; // The slot the next save goes in, and how many slots before it hold states we can restore. @@ -81,6 +85,13 @@ private: int baseUsage_ = 0; double rewindLastTime_ = 0.0f; + + // Whether the newest state is the one the last rewind restored, and when that was. + bool newestRestored_ = false; + double lastRestoreTime_ = 0.0; + // CoreTiming's clock when the current run of rewinds started, for "seconds ago". + double rewindFromEmuTime_ = 0.0; + bool retryingAfterFailure_ = false; }; } // namespace SaveState diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index a756c9226a..0754ede886 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.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` | +| 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 keeps, so only a rewind within 0.8 seconds of the last steps further back; needs `RewindSnapshotInterval` set, and the `wait` response's `metadata` says how far back that is in emulated time), `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` |