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) <noreply@anthropic.com>
The state that load undo kept was snapshotted before every slot load anyway.
Take it before every load, and if the load fails partway, load it back rather
than resetting the game. Load undo still gets it, now written after the load,
and not when the load was rolled back, so the undo on disk stays the useful one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MeCore owns a second MIPSState in the ME's address space, held in reset
until released (SetReset). It runs on the interpreter, a slice behind the
main CPU: CoreTiming runs it for the cycles the main CPU has just used, in
Advance and ForceCheck. Its own PRId (COP0 22) and mfic/mtic flag; MMIO
counts as kernel mode on it, and a syscall stops it with an error rather
than reaching HLE. The interpreter's block stepping reads delay slots
through the stepping CPU's memory, and cache instructions log at verbose
level, as ME code flushes its caches in a loop.
Created at boot only for homebrew (PBP with MG category or no SFO, ELF;
the VSH counts, as vshmain.prx loads as an ELF), when "Enable ME
emulation for homebrew" is on: a global checkbox in developer settings,
off by default and not translated yet, or --me-core. Saved in its own
section after MMIO (SaveStart v5), with its own memory; loading a state
creates or removes the ME to match.
The boot page now starts with a reset stub that sends the ME on to
0xBFC00040, where homebrew puts its code (from the firmware's reset code,
read on a PSP-1000).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The interpreter's MMIO dispatch was a hardcoded chain for GPIO and the syscon
serial port. Devices now register an address range in Core/HW/MMIO and get
each access with its width and the MIPSState making it, for registers that
answer each CPU differently. An access no device handles is logged and reads
as poison, as before.
GPIO and syscon serial become the first two devices. They're reset at boot,
which they never were, and saved in savestates (SaveStart section version 4).
ll/sc, lwl/lwr and lwc1/swc1 reach MMIO in the interpreter too, and kernel
blocks leave lwc1/swc1 to it like the other loads and stores. swl/swr and
lv/sv still treat it as a bad address. TestMMIO runs a kernel-mode access on
each core and the registers through a savestate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Freeze-frame saves and loads its own snapshot outside SaveState::Process,
so it missed the check for a display list stopped in the GE debugger:
stepping a draw in a frozen frame reloaded the state under the list.
And a slot load or rewind while frozen was undone by the next frame's
freeze reload, while saying "Loaded State". Now it becomes the frozen
state, and is a full load (the GPU only skips its resets for the freeze's
own reloads).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Loading a state from the pause menu closed it right away, and the game
ran a few frames from where it was (seen and heard) before the state
was applied. Now savestate operations run while the game is paused under
the menu, and the menu closes once the load (or rewind, or load undo) is
done. Rewind snapshots aren't taken while paused.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A save to a path with a save still being written counted as a repeated
key press, even with a load applied in between, so save, load, save
didn't save the loaded state. Now a repeat only joins the earlier save if
no load has been applied or queued since its snapshot.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A failed final flush (a full disk) or a failed rename into place was
reported as "Saved State". A failed load undo rename still pointed load
undo at the current game, while the file was another game's state.
- Refusing a save or load for being online didn't call the callback,
unlike every other refusal.
- A rewind dropped after its decode started stayed active, and stopped
rewind snapshots until the next clear.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every load hashed all known functions again, for the replacement hooks:
2.4-6 ms, about half the load. Functions in module code the load left as
it was keep their hash, so only the rest is hashed now (0.5-1 ms). The
code is compared on every load for this, not only with a JIT.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
libretro declared its own, which no longer matched once SaveStart got a
member, so serializing read an uninitialized byte. It's in SaveState.h
now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A load that changes the memory size remaps RAM, so there's nothing to
compare with, and the JIT's dispatcher points at the old mapping: it has
to start over. A load that fails partway may have replaced RAM without
the JIT being told, so it keeps nothing either.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Loading a state started the JIT over, so every block the game runs had to
be compiled again, which stutters. Now the loaded modules' code is
compared with the state's as RAM is loaded (a chunk that already matches
isn't copied either), and blocks entirely inside unchanged code stay.
Everything else is invalidated as before, and without a JIT, or with no
unchanged code, the JIT still starts over.
The JIT throws its blocks away if the loaded state needs code they weren't
compiled for (rounding mode switches, or no default prefixes at block
start). The other way round it keeps them, along with the flags they were
compiled under: before, a loaded hasSetRounding of false, or the reset of
lastSetRounding, had the next compile rebuild everything anyway.
In the three seconds after loading a state, Ridge Racer compiles 28 blocks
instead of 4823, Force Unleashed 31 instead of 7727. Frames rendered after
the load are identical either way.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The boot spinner, in the same corner without the background, while a
save, load or rewind is queued or in progress. Only after a quarter of a
second, so it doesn't flash up on every quick save; on desktop that's
rarely reached, but slow devices get some feedback.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Saving and loading stalled the emu thread for 30-75 ms, long enough to
glitch audio. Now only the snapshot and the apply run there; compression
and file I/O run on a savestate worker thread, in order.
- Save: snapshot in Process(), then compress, write and (for a slot) move
the old state to its undo name on the worker. The callback runs once the
file is written. Saving again to a path with a save pending is a repeat
key press, and gets that save's result without a message.
- Load: read and decompress on the worker while the game runs, then apply.
A slot load keeps the state it replaces for load undo at that point,
rather than saving it in full before starting.
- Rewind: compression and decoding are worker jobs, and the deltas get
zstd level 1 on top, which makes them about a third of the size.
- At most two save snapshots are in memory at once, and buffers are freed
when their operation is done.
- Shutdown waits for pending writes, and drops callbacks as before.
On desktop, a save now costs the emu thread 2-3 ms, a load or rewind
12-15 ms.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When the file reads fine but a section doesn't, that's almost always a
state from another version, so say that (LoadStateWrongVersion), with the
version that saved it and the failing section on the second line.
LoadPtr() now returns just the section title, for callers to phrase.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Rewind:
- first_ never wrapped, so after about 40 snapshots rewinding past the
oldest state jumped back to the newest ones. Track a count instead.
- States whose base had been replaced stayed in the ring, and each one
failed a rewind with an empty error. Drop them when the base goes.
- Rewinding after pausing for longer than the snapshot interval restored a
snapshot taken just before the rewind ran.
- Rewind snapshots (and verification) counted as saves for IsStale().
Slots:
- Loading an empty slot replaced the load undo backup before failing.
- A refused save (hardcore mode) rotated the slot's screenshots first.
- Refused or dropped operations never called their callback, so auto-load
didn't notice it had failed.
- The slot listing went stale after a save until something rescanned.
Also: Verify always failed (its write pass never set the buffer start),
and corrupt or truncated files were reported as being from an older
version.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The version comes from the file header and goes in the failure message's
metadata line. A state from another version is the usual reason a load
fails. Headless prints it too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SaveState::Load and Save silently drop the operation when the network or
achievements hardcore mode forbids it, but the debugger still answered as if
it had been queued. Return whether it was, and check the boot state in the
same CPU-thread call.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each retry went through ScheduleSaveScreenshot(), which reset the
failure count, so the limit was never reached and a state saved while no
screenshot could be taken kept retrying every frame.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Process() doesn't run while achievements block execution at boot, so
exiting then left the auto-load queued. The next game ran it, loading the
previous game's state, and calling back into the deleted EmuScreen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some old states have replacement hook ops in memory. Since hooks moved out of
memory, those changed the functions' hashes on load, so they weren't hooked
again, and the CPU hit the op as an illegal instruction. Restore the
instructions this session hooked at the same addresses, as the old hooking did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JITs used to find a block by overwriting its first instruction with an "emuhack" holding
the offset into the code space, and replacement functions installed their own emuhack the same
way. Anything that read code (savestates, the debugger, memory dumps, cheats, the function
hasher, games reading their own code) had to translate them back.
Blocks now live in a block shadow: one u32 per guest instruction, indexed by the PC masked with
BLOCK_SHADOW_MASK (1GB on 64-bit, folding the kernel and uncached mirrors; 256MB on 32-bit, iOS
and Switch, where address space is short), with the low bits cleared so a misaligned PC can't
read across two entries. Only the ranges under memory views are committed, so a bad PC still
faults in the dispatcher, and the shadow is reserved before the memory map so it can't land in
a gap of the 4GB window. Entries hold full 32-bit offsets, lifting the 16MB limit. The
dispatchers on every backend, and the IR interpreter's loop, look blocks up there.
A hooked instruction without a compiled block gets the JIT's hook value in its entry: the offset
of the dispatcher's compile path (1 for the interpreters, the IR arena keeping that offset
empty), so dispatching there compiles a block, and the frontends substitute the hook. Compiling
replaces the entry with the block's, and destroying the block puts the hook value back. The hook
value is rewritten when the JIT changes or regenerates its dispatcher.
Hooks live in a map keyed by the masked address, and are dropped when the game overwrites the
hooked instruction, or when an icache invalidation finds the function's hash (immediates masked,
as when matching it) changed. Those checks run when queued invalidations are processed, since
cheats and the debugger queue them before writing. Installing and removing hooks invalidates
deferred, as it happens in HLE calls that can run inside a block. The cache instruction goes
through MIPSState, so it drops changed hooks too.
With clean memory, the save/clear/restore around savestates, memory reads and dumps goes away,
as do the source-range invalidations in the memcpy replacements, sceDmac and the cheats.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Returning to the CPU flushed all queued drawing, also when a list only reached its stall address.
Wipeout Pure builds its lists as it goes and stalls about a hundred times a frame, so the main thread
spent a third of its time finishing the drawing at those flushes. A stalled list isn't done, so now
only a finished one flushes there, which makes Wipeout Pure 15% faster.
What relied on that flush now flushes itself: memory copies, sets, readbacks and uploads that overlap
the queued drawing, and save states, before memory is saved or loaded (FlushPendingDrawing). Syncs and
the display already did. Queued drawing still reads textures from PSP memory when it runs, so a game
that rewrites a texture between stalls without syncing would see the new one; on hardware the GE has
usually drawn with the old one by then.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The write pass stores memory with the JIT's emuhacks cleared, but the
verify pass compared against memory that still had them, so it would
report a mismatch under a JIT. Only EMULATOR_DEVCTL__VERIFY_STATE runs it,
and nothing currently does. It also counted as a save in the generation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The list belongs to the sceGe call still in progress, whose end would have
run on the loaded CPU state. Also stop the camera and GPS when a state has
them off, don't restart capture when saving, and fix a double free of the
pmp frame queue (it only holds the media engine's own frame).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both drained only in their own DoState, after memory and Atrac contexts
had already been replaced under a mix or read still in flight. Also fix
sceUmd loading umdActivated into the wrong variable, and count each save
once in saveStateGeneration (it also bumped in the measuring pass).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Replaces the temporary fix that did the hidden modes' IO inside Update.
The IO thread read and wrote the dialog's request, display state and save
list, all shared with the emulator thread, which kept using them to draw the
dialog and reload the request from the game.
Now the IO thread works on its own copy of the request, its own SavedataParam
and directory names resolved up front, and shares nothing else with the
emulator thread but the (locked) file system, MemoryStick_FreeSpace's cached
use and sceChnnlsv's scratch buffer and kirk state, the last two now under
locks too. It still reads and writes the game's buffers directly, like a PSP's
utility threads and sceIoReadAsync do, so a savestate waits for it before it
saves or loads memory. Save
bookkeeping, the save indicator and display changes happen on the emulator
thread when the results are taken, and only the request fields the IO changed
are copied back, so a game's own edits in the meantime survive.
Hidden modes take the results at the next Update (or, with Host IO timing,
the first Update that finds them done). The visible dialogs keep drawing and
take them once the IO is done; save and load used to stall the emulator
thread for the whole operation. Savestates keep results that haven't been
taken yet.
When the results land in PSP memory doesn't matter to games, so
utility/savedata/filelist now only prints them once the utility has finished.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deleting a savestate from the savedata screen goes through GameInfo::Delete,
not SaveState::DeleteSlot, and it only knew about the .jpg - so the slot's
.name.txt was left behind with nothing to belong to.
Rather than teach the UI the naming scheme a third time, SaveState now answers
what sits beside a state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GetSlotCustomName hit the disk for every slot each time the pause screen built
its views, almost always for a file that isn't there. Rescan's listing already
knows, and HasSaveInSlot - which gates whether the name is even shown - reads
the same map, so this can't hide a name the old code would have found.
SetSlotCustomName now rescans, so a rename is visible without depending on the
pause screen happening to rescan on its way back.
Also: clearing a name deletes the file instead of leaving an empty one behind,
and the extension is "name.txt" rather than plain "txt", so an unrelated
"<prefix>_<slot>.txt" in the savestate folder isn't read as a slot name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same problem as the hardcore checks: SaveState.cpp asked NetworkAllowSaveState()
and just returned, so a load or save refused because you're connected did
nothing at all, with no explanation. Switched all eight to
NetworkWarnUserIfOnlineAndCantSavestate(), which is the same predicate plus the
standard message; its OSD id already collapses duplicates for the paths that
check twice on the way in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
Freeze-frame restores a savestate every frame, straight through
SaveState::LoadFromRam(), so it never touched the operation queue and neither
hardcore check saw it. Blocked at the toggle in the dev menu, and again in the
render loop, since hardcore mode can come up after the fact once the game has
been identified.
Enqueue also just dropped operations silently, so a load that arrived through a
path with no check of its own (--state, auto-load) did nothing with no
explanation. Both it and Process now go through WarnUserIfHardcoreModeActive,
which is the same predicate plus the standard message. Callers that already ask
it themselves return before reaching Enqueue, so nothing shows the message twice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
The check lived only in Enqueue, but operations don't run there - they're
queued and applied later by Process(). During boot, HardcoreModeActive() reads
false even when hardcore is on, since it requires rc_client_is_processing_required(),
which only becomes true once RetroAchievements has finished identifying the game
asynchronously. Anything queued in that window passed the check, and was then
applied by Process() after identification completed and hardcore came up.
Auto-load wasn't even a race: EmuScreen::bootComplete() calls Achievements::SetGame(),
which starts the identify, and then checks HardcoreModeActive() a few lines below -
always false at that point. So "Auto load savestate" quietly worked in hardcore mode.
--state and a load-state hotkey pressed during boot got through the same way.
Re-checking per operation in Process() covers every entry point at once, and by
then identification has finished, so the answer is authoritative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
PointerWrap tracked no end-of-buffer, so DoState() implementations could
read past the end of a crafted or truncated savestate via DoVoid's
unchecked memcpy, and DoVector could resize to an attacker-controlled
size before reading.
- PointerWrap now tracks a read end; DoVoid/ExpectVoid fail (MODE_NOOP)
before reading out of bounds.
- String reads are bounds-checked for the whole string including NUL.
- DoVector rejects sizes that can't fit in the remaining buffer.
- LoadPtr takes the buffer size and sets the read end.
- Capping the decompression buffer allocation in LoadFile.