From 0877c3d19a7f189fc2878f0945e44566a56e0cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 21 Oct 2026 18:25:01 +0200 Subject: [PATCH] SaveState: Refuse savestates and rewind in a session booted into the XMB Inserting and ejecting a disc there adds and removes the umd0:/umd1:/ umd:/disc0: mounts, and MetaFileSystem's state pairs sections with mounts by position without recording which disc was in. A state could then fail to load, or load one disc's open files onto another. The pause screen already hid its save buttons for vshmain.prx; the save hotkeys, rewind, the ImDebugger and the WebSocket debugger all went around that. The check is now in SaveState itself, and rewind snapshots aren't taken. Verify still runs, since it reads back with the mounts as they are. A proper fix records the mounted image in sceUmd's state and remounts it on load. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/SaveState.cpp | 29 ++++++++++++++++++++++++++--- assets/lang/en_US.ini | 1 + docs/WebSocketDebugger.md | 2 +- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 87d8cbb845..dffde2d7a9 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -31,6 +31,7 @@ #include "Common/Thread/ThreadUtil.h" #include "Common/Data/Text/Parsers.h" #include "Common/System/System.h" +#include "Common/System/OSD.h" #include "Common/File/FileUtil.h" #include "Common/File/DirListing.h" @@ -424,6 +425,23 @@ int g_screenshotFailures; } } + // A session booted into the XMB inserts and ejects discs, which changes the mounts, and keeps + // doing so for a game launched from it. MetaFileSystem's state pairs its sections with mounts by + // position and doesn't record which disc was in, so a state from such a session could fail to + // load, or load one disc's file state onto another. + static bool IsVSHSession() { + return PSP_CoreParameter().fileToStart.GetFilename() == "vshmain.prx"; + } + + static bool WarnUserIfVSHSession() { + if (!IsVSHSession()) { + return false; + } + auto pa = GetI18NCategory(I18NCat::PAUSE); + g_OSD.Show(OSDType::MESSAGE_INFO, pa->T("Save states are not available in the XMB"), 3.0f, "savevsh"); + return true; + } + bool Enqueue(const SaveState::Operation &op) { // These two both refuse the operation, and both say so - dropping it silently just // looks like the hotkey didn't work. Callers that ask the same questions themselves @@ -438,6 +456,11 @@ int g_screenshotFailures; CallbackRefused(op.callback); return false; } + // Verify saves and loads back in place, with the mounts as they are. + if (op.type != OperationType::Verify && WarnUserIfVSHSession()) { + CallbackRefused(op.callback); + return false; + } std::lock_guard guard(mutex); g_pendingOperations.push_back(op); @@ -660,7 +683,7 @@ int g_screenshotFailures; void LoadSlot(std::string_view gamePrefix, int slot, Callback callback) { // Enqueue() would refuse too, but only after the load undo backup below has replaced the old one. - if (NetworkWarnUserIfOnlineAndCantSavestate() || Achievements::WarnUserIfHardcoreModeActive(false)) { + if (NetworkWarnUserIfOnlineAndCantSavestate() || Achievements::WarnUserIfHardcoreModeActive(false) || WarnUserIfVSHSession()) { CallbackRefused(callback); return; } @@ -716,7 +739,7 @@ int g_screenshotFailures; void SaveSlot(std::string_view gamePrefix, int slot, Callback callback) { // Enqueue() would refuse too, but only after the screenshots below have been rotated. - if (NetworkWarnUserIfOnlineAndCantSavestate() || Achievements::WarnUserIfHardcoreModeActive(true)) { + if (NetworkWarnUserIfOnlineAndCantSavestate() || Achievements::WarnUserIfHardcoreModeActive(true) || WarnUserIfVSHSession()) { CallbackRefused(callback); return; } @@ -1481,7 +1504,7 @@ int g_screenshotFailures; // NOTE: This can cause ending of the current renderpass, due to the readback needed for the screenshot. void Process(bool takeRewindSnapshots) { - if (takeRewindSnapshots) { + if (takeRewindSnapshots && !IsVSHSession()) { rewindStates.Process(); } RunCompletions(true); diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index ee3673dbf6..09581d5c3c 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -1082,6 +1082,7 @@ Insert UMD = Insert UMD Load State = Load state Rewind = Rewind Save State = Save state +Save states are not available in the XMB = Save states are not available in the XMB Settings = Settings Switch UMD = Switch UMD Undo last load = Undo last load diff --git a/docs/WebSocketDebugger.md b/docs/WebSocketDebugger.md index 2258345e4a..6f9f9cae0e 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 given as `path`, or a `slot` of the running game, 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), `game.umd.insert` / `game.umd.eject` (put a disc image, given as `path`, in an empty drive, or take it out: for the XMB booted with `--vsh` and no game, which then lists the disc and can launch it), `version` | `GameSubscriber.cpp` | +| Game/version | `game.reset`, `game.status`, `game.state.load` (queue loading a `.ppst` file given as `path`, or a `slot` of the running game, 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; all but this one are refused in a session booted into the XMB, whose discs come and go), `game.speed.get/set` (emulation speed - unlimited fast-forward, or a percentage of 60 FPS; see below), `game.umd.insert` / `game.umd.eject` (put a disc image, given as `path`, in an empty drive, or take it out: for the XMB booted with `--vsh` and no game, which then lists the disc and can launch it), `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` |