Commit Graph
49316 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5.5 5cc5a2d802 Replacement: Find pack files whose names differ in case
- Fixes #18746

Packs are mostly made on Windows, where "portraits/x.png" in textures.ini finds Portraits/x.png. On a
case sensitive file system (Steam Deck, Linux) it didn't, while zipped packs already ignored case.

DirectoryReader gets an opt-in ignoreCase mode, used for texture packs. On the first miss it indexes
the directory by lowercased path and retries there, so packs where every name matches never pay for
it, and the walk happens on the IO thread that loads the texture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 034901ee87 Replacement: Copy levels on the calling thread, and end loading at a bad filename
The copies went through the thread pool, where they could wait behind other tasks like pipeline
compiles, and one core already saturates memory bandwidth.

A filename with a parent dir component left the texture in CANCEL_INIT, which the texture cache
keeps polling forever. Treat it like a missing file instead. Also remove an unused lastUsed field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 7b9e1a1c93 ZipFileReader: Look up the exact name before the case insensitive one
The case insensitive lookup compares against every name in the zip, while an exact one uses libzip's
hash table. Texture packs can have tens of thousands of files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 f74111835f Replacement: Dump all mip levels with ignoreMipmap = false
Saving level 0 marked the whole texture as saved, so the other levels were skipped. Track it per level.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 b18792e40e Replacement: Treat a fake mipmap level as level 0 of its own texture
- Fixes #19623

With a fake mipmap change (Japanese Tactics Ogre), each PSP level is cached and hashed as its own
2D texture, but since 73c06bb776 (v1.14) the replacement and dump code used the PSP level as the
replacement level. Loading a replacement for a level-2 font sheet asked for level 2 of a one-level
replacement and hit the "Invalid miplevel" assert, and dumps were named _2, which the ini then
treats as an ignored texture. Use the created texture's level for both again, as 1.13.2 did.

Also don't force mip filtering from a mipmapped replacement when only one level was uploaded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik Rydgård 189d5f8408 Merge pull request #22664 from hrydgard/inet-bind-untangle
sceNetInet: Only let the ad hoc settings change bind addresses during ad hoc
2026-10-20 10:24:39 +02:00
Henrik Rydgård 2be8d57f8c Merge pull request #22674 from hrydgard/nfs-visualizer-22104
Splines in HW renderers: Fix line and point curves too large to expand in one draw
2026-10-20 09:12:15 +02:00
Henrik RydgårdandClaude Opus 5.5 5080cb23b6 sceNetInet: Only let the ad hoc settings change bind addresses during ad hoc
- Fixes #22647

sceNetInetBind rewrote the address whenever the ad hoc server was set to
localhost (to 127.0.0.x, even an explicit address) or the built-in ad hoc
server was enabled (INADDR_ANY to the address facing the ad hoc server),
breaking infrastructure sockets. Both rewrites now only apply while
adhocctl is initialized, which keeps ad hoc as it was.

sceNetApctlGetInfo reported its IP from the same ad hoc-relative
getLocalIp(), so a localhost ad hoc server told infrastructure games their
IP was 127.0.0.x. It now uses the host interface's IP (getHostLocalIp(),
split out of getLocalIp()).

Adds the net/inet/bind test, which failed every bind under headless (whose
ad hoc server is localhost).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 08:59:03 +02:00
Henrik RydgårdandClaude Opus 5.5 9dd6d2de10 Headless: Always initialize networking
Without WSAStartup, every sceNetInet socket call failed on Windows with
WSANOTINITIALISED, so no inet test could run there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 08:59:03 +02:00
Henrik RydgårdandClaude Opus 5.5 f5c5991f98 Splines: Fix line and point curves too large to expand in one draw
- Fixes #22104

NFS Most Wanted's music visualizer is a 32x16 spline of lines at 8x8
tessellation: 144768 indices, which software transform expands to far more
than VERTEX_BUFFER_MAX vertices, so SubmitPrim splits it and flushes between
the parts. The curve's indices sat at the start of decIndex_, where each
flush writes its generated and expanded indices, so the first flush
overwrote most of the later parts and about half the visualizer was lost.
Tessellate the indices into the upper half instead, and don't use the
predecoded output for a curve that will be split, as the flushes decode
over it.

The game also sends an empty PRIM with VADDR 0 every frame, which the
hardware backends logged as a bad vertex address. Skip zero-count PRIMs
before the address check, as the software renderer does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 08:55:22 +02:00
Henrik Rydgård cf85e91e62 Merge pull request #22670 from hrydgard/dostate-review
SaveState: Fixes from a review of the remaining DoStates
2026-10-20 08:04:37 +02:00
Henrik RydgårdandClaude Opus 5.5 30cab94840 SaveState: Fixes from a review of the kernel savestates
- A partition memory block restored its allocator through
  BlockAllocatorFromID, whose privilege check asked the thread current
  before the load, already deleted with the kernel objects. Loading a
  kernel-partition block under a user thread left it without an allocator,
  crashing when it was freed. The saved id maps straight back now, and the
  current thread is cleared along with the objects.
- The thread LoadExec leaves waiting (with no timeout) for plugins and VSH
  modules to start wasn't saved, so a state saved while one was starting
  hung the game thread after loading. The VSH modules still to start
  weren't saved either, nor cleared.
- A state without the exit callback id left the previous session's.
- From a broken or crafted state: thread ids that GetFast trusts (current,
  idle and ready), interrupt handler numbers, FPL and TLSPL indices, and a
  MIPS call or action slot that isn't there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 00:20:24 +02:00
Henrik RydgårdandClaude Opus 5.5 d40137a007 SaveState: Fixes from a review of the IO and dialog savestates
- Savedata list entries lost their directory across a load. Confirming a
  delete in a list dialog loaded from a state failed with "delete failed"
  and deleted nothing. LISTALLDELETE can't rebuild it from the names, so
  it's saved now, with the broken flag.
- Registry values the game wrote (CLANNAD's font plugin registers its font
  there) weren't saved, and a state without a sceReg section wiped them.
- Slots sharing the "no save" icon came back as separate images of one
  texture, which Clear() then freed once per slot.
- Old directory states left a file opened with TRUNCATE due for truncation,
  so it read as empty and was truncated on close. Old async IO states were
  loaded on top of this session's results.
- From a broken or crafted state: OSK keyboard and Korean composition
  indices, gamedata install file counts, font ownership modes (an internal
  font could be deleted) and FontLib's per-font vector sizes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 00:20:24 +02:00
Henrik RydgårdandClaude Opus 5.5 43f7674f9c SaveState: Check the audio and media modules' values from a state
A review of their savestates found nothing wrong with a normal save and
load, but a broken or crafted state could crash or write out of bounds:

- Atrac2's context pointer was dereferenced and its SAS buffer index used
  unchecked. Atrac v1's file size sized an allocation before it was known
  to be in the file, and its first buffer could be larger than the file.
- MediaEngine's MPEG header size and read position index a fixed array,
  and its ringbuffer size sizes buffers.
- AuCtx's PCM buffer must hold two frames, as the game side requires. And
  AuDecode compared a decoded frame with the whole PCM buffer instead of
  the half it fills, so a frame between the two zero-filled a negative
  size.
- BufferQueue's fill level, VAG block positions (now ending the voice,
  like the other bad VAG state), sceAudiocodec's context count, the SRC
  mix frequency (divided by), and old sceMpeg states' ringbuffer address.
- SAS decoding with an atrac id that has no atrac crashed after warning.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 00:20:24 +02:00
Henrik Rydgård 8fdadf74bc Merge pull request #22668 from hrydgard/pause-menu-load
Savestate loads: keep JIT blocks and textures, pause menu waits for loads, review fixes
2026-10-20 00:15:56 +02:00
Henrik Rydgård 19869834b2 Merge pull request #22671 from hrydgard/alarm-vtimer-pending-delete
Kernel: Keep alarm and vtimer queues in step with their pending interrupts
2026-10-20 00:10:58 +02:00
Henrik RydgårdandClaude Opus 5.5 d42f34d3de Kernel: Keep alarm and vtimer queues in step with their pending interrupts
Each due alarm or vtimer queues its id next to a pending interrupt, which
pops it when it runs. An alarm cancelled with its interrupt pending was
never popped, so every later alarm interrupt found the cancelled one at
the front and was dropped: no alarm ran again. Deleting a vtimer erased
its id instead, leaving the last pending interrupt with an empty list.

Both now pop a timer that's gone when its interrupt comes, as
threads/alarm/pending and threads/vtimers/pending show the PSP still runs
the rest, in order.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 21:41:48 +02:00
Henrik RydgårdandClaude Opus 5.5 7202904f60 libretro: Include SaveState.h for SaveStart, now declared there
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 21:32:43 +02:00
Henrik RydgårdandClaude Opus 5.5 28686cf428 sceGe: Check the pending interrupt before finishing it
A broken savestate can have a GE interrupt running with nothing pending,
or a pending entry for an invalid list. Finishing it read past the queue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 ca0e40bb0a GPU: Don't load the host's window-minimized bit from a savestate
skipDrawReason was saved whole, including the bit that says the host
window is minimized. A state saved while minimized (a rewind snapshot)
kept it set after loading in a normal window, and the other way round.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 130ea9ced9 GPU: Save the next display list id, and restart frame dumps on load
- Which list slot and id the next sceGeListEnQueue gets wasn't saved, so
  after a load it depended on the session before it, and replays and
  rewinds diverged. Older states start it at 0, and also get the default
  EDRAM translation and visible matrices they don't carry, rather than the
  previous session's.
- A frame dump recording during a load replayed the loaded state's
  commands on the registers from before it. It now starts over from the
  loaded state.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 814593ee4d Framebuffers: Forget the depth shift tracking with the framebuffers
DestroyAllFBOs (on a savestate load) kept depthShiftedFor_ and
lastRenderAddress_, so a new framebuffer at the same address as the old
one skipped the depth shift for a frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 fa3eff96fb Display: Don't read past memory presenting a framebuffer from RAM
The PSP accepts display strides that run past the end of VRAM, and
negative ones (setframebuf.expected has -64 and 2048), and presenting
straight from memory read all 272 rows of whatever stride was set.
Without a matching framebuffer, that's a read far outside PSP memory.
Now the whole image has to be in memory, or the display shows black, on
the hardware backends and the software renderer.

A savestate's display framebuffer with an address outside RAM and VRAM,
or an invalid format, turns the display off instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 ef8961e17a Freeze frame: Keep away from GE stepping, and keep other loads
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>
2026-10-19 20:54:29 +02:00
Henrik RydgårdandClaude Opus 5.5 7e4127ae69 Pause menu: Don't close on a state load until the state is in place
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>
2026-10-19 20:42:14 +02:00
Henrik RydgårdandClaude Opus 5.5 6e16f46445 GPU: Keep textures across a savestate load, rehashing them first
A load cleared the texture cache, so every texture the game drew next
was decoded and uploaded again. Now each one is rehashed against its
memory the next time it's used, and only decoded again if it changed. In
the three seconds after a load, Ridge Racer decodes no textures instead
of 109, Force Unleashed none instead of 43. Rendered frames are the same.

Framebuffers are still all destroyed on load, since loading a state is a
known way out of some framebuffer problems.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:30:02 +02:00
Henrik RydgårdandClaude Opus 5.5 26d9e54059 SaveState: Don't fold a save into one from before a load
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>
2026-10-19 20:18:47 +02:00
Henrik RydgårdandClaude Opus 5.5 3cc61d3f8e SaveState: Report failed writes, and call back on network refusals
- 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>
2026-10-19 20:18:46 +02:00
Henrik RydgårdandClaude Opus 5.5 f45f4d5c85 SaveState: Only rehash functions in code a load changed
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>
2026-10-19 20:18:46 +02:00
Henrik RydgårdandClaude Opus 5.5 a62aa69986 libretro: Use the real SaveStart
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>
2026-10-19 20:18:46 +02:00
Henrik RydgårdandClaude Opus 5.5 198652d9b3 SaveState: Don't keep JIT blocks across a memory remap or a failed load
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>
2026-10-19 20:18:46 +02:00
Henrik RydgårdandClaude Opus 5.5 eec6afaeca SaveState: Reject implausible event counts, ids and CPU speed from a state
They came straight from the file: a CPU frequency of 0 divided by zero,
and counts or ids sized tables of up to tens of gigabytes, or tripped an
assert. A file like that now fails to load like any other broken state.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:17:47 +02:00
Henrik RydgårdandClaude Opus 5.5 79d340f4fe SaveState: Keep JIT blocks in module code a load doesn't change
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>
2026-10-19 19:47:05 +02:00
Henrik Rydgård 3ac908345c Merge pull request #22667 from hrydgard/async-savestates
Async savestates: compress and write on a worker thread
2026-10-19 19:25:24 +02:00
Henrik Rydgård 6015ff06a7 Merge pull request #22616 from LunaMoo/native-cso-pack
Add built-in CSO packing functionality
2026-10-19 18:53:00 +02:00
Henrik RydgårdandClaude Opus 5.5 86e420b9ce EmuScreen: Show the loading spinner while a savestate saves or loads
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>
2026-10-19 18:51:10 +02:00
Henrik Rydgård e2e0e0c748 Merge pull request #22666 from hrydgard/savestate-fixes
Savestate fixes: rewind ring, slot operations, load failure messages
2026-10-19 18:46:20 +02:00
Henrik RydgårdandClaude Opus 5.5 5bbeb6254b Debugger: Savestate slots in game.state.save/load, and async savestate docs
game.state.save and game.state.load take a slot of the running game
instead of a path, going through the same code as the save and load keys
(undo rotation, load undo, repeats), so that can be tested headless.

docs/async-savestates.md now describes what was built, with the results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 18:43:27 +02:00
Henrik RydgårdandClaude Opus 5.5 821fd301e6 SaveState: Compress and write savestates on a worker thread
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>
2026-10-19 18:43:27 +02:00
Henrik RydgårdandClaude Opus 5.5 5d8566ee1e Serializer: Stream zstd when saving and loading state files
Saving compressed into a ZSTD_compressBound() buffer, about the size of
the whole state, and loading read the whole compressed file before
decompressing it. Now saving compresses into a buffer that grows with its
output and frees the uncompressed state before writing, and loading
decompresses the file in chunks. The file format doesn't change.

Saving also compresses before opening the file, which truncates it, and
drops the unused snappy path. LoadFile and SaveFile are public, for doing
the file part away from the state itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 18:43:27 +02:00
Henrik RydgårdandClaude Opus 5.5 d8df483fc4 docs: Design for async savestates
Move compression and file I/O off the emu thread, leaving it only the
snapshot and the apply, without wasting memory along the way. Not
implemented yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 18:17:52 +02:00
Henrik Rydgård 33a9d3072b Merge pull request #22665 from hrydgard/faster-firmware-install
Firmware install: Faster through parallelism
2026-10-19 18:02:05 +02:00
Henrik RydgårdandClaude Opus 5.5 2fbb67f207 lang: A savestate that fails to load is from a newer version, not an older one
States from older versions load fine. The string is now shown when a
section of the state fails to load, which in practice means a newer
build saved it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 17:58:36 +02:00
Henrik RydgårdandClaude Opus 5.5 ca72577702 Rewind: Keep the restored state, and measure "seconds ago" in game time
A rewind used up the state it restored, so rewinding, playing on a bit and
rewinding again went further back than the state you just came from. Now
the state stays, and only a rewind within 0.8 seconds of the last one
steps back past it.

"Seconds ago" was wall-clock time since the snapshot, which counted
pauses, fast-forward and time spent in states rewound to earlier. It's now
emulated time from where the rewinding started.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 17:58:36 +02:00
Henrik RydgårdandClaude Opus 5.5 b75f487dc5 SaveState: Report a state that fails to load as being from another version
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>
2026-10-19 17:50:48 +02:00
Henrik RydgårdandClaude Opus 5.5 79c2b9b0fd 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>
2026-10-19 17:29:01 +02:00
Henrik RydgårdandClaude Opus 5.5 e3916c0acc SaveState: Fix rewind ring and savestate slot bugs
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>
2026-10-19 17:29:01 +02:00
Henrik RydgårdandClaude Opus 5.5 b9a79b2375 Screenshot: Fix use-after-free when a savestate screenshot retries
The failure callback ran from inside g_pendingScreenshot. The savestate
one schedules a retry, which overwrote g_pendingScreenshot (destroying the
running callback and the path it was reading), and the caller then wiped
the retry anyway, so the screenshot never got taken. Move the pending
screenshot out before calling back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 17:29:01 +02:00
Henrik RydgårdandClaude Opus 5.5 6163bf5911 SaveState: Show which PPSSPP version saved a state when loading it fails
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>
2026-10-19 16:28:47 +02:00
Henrik RydgårdandClaude Opus 5.5 9ddfcca670 Firmware install: Fewer and parallel file operations into the staging directory
Through Android's storage framework, every exists check, create and open is a round trip to the
storage provider, and writing each file took four of them (a check of its directory, a check of
the file, the create, the open). That made a 6.61 install take around 30 s on a phone, against a
fraction of a second of decrypting and inflating.

The install unpacks into a staging directory it has just cleared, so nothing in it can exist yet.
With the new outputDirIsEmpty option, each directory is created once and each file is created and
opened without checking for it first (File::WriteDataToNewFile), on the IO threads while decoding
goes on. A name that turns up twice still goes the careful way, overwriting the first. The swap
into place at the end is unchanged.

Headless --unpack-updater uses the same path when the output directory doesn't exist yet. Its
output is identical either way. With SIMULATE_SLOW_IO, unpacking the fonts went from 17 s to 7.5 s,
most of what's left being the fixed cost of opening the updater.

The UI's install already runs on a thread pool IO thread, so writes queued on the same pool
could stall behind other long IO tasks, and the pool drops queued tasks unrun at teardown,
which would have left the install waiting forever on its in-flight count. Eight threads owned by
the writer (attached for JNI) avoid both, and headless no longer has to start the pool for it.

Also compare file names case-insensitively when looking for repeats, since the storage usually
is case-insensitive and a second create there makes a renamed copy.

Checked with ThreadSanitizer, and on a read-only destination (every file fails, no hang).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 16:06:47 +02:00