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>
- 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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
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 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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 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>
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>
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>
"Compress game" in the game screen's ... menu, for ISO, CSO, CHD and PSN
EBOOT.PBP games. It opens a list of the CSO presets (and Extract to ISO
for anything that's not an ISO already):
- Best: libdeflate level 12, 64K blocks
- PSP Compatible: zlib level 9, 2K blocks, readable by custom firmware
These match maxcso's presets, and the CSOs come out byte-identical to its.
The writer, Core/FileSystems/CSOCompress, reads the input through PPSSPP's
own block devices, so recompressing is a single pass with no temporary
file.
The original is deleted when done, unless "Keep the original file" is
checked (off by default). When the new name is taken, by a kept original
or another file, a number is added to it.
The work runs on a thread using every core, with a progress bar and time
left at the bottom of the game screen. It keeps going when the screen is
left, and the ... menu offers Cancel meanwhile. On the game list the
game's icon is covered by "Compressing...", and clicking it opens the
game screen instead of playing. When done, recents follow the new file
and the game screen and lists refresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCxekYqPnzNr5d8Tp1q7aZ
Only the compressor (deflate_compress.c, utils.c and their headers) from
libdeflate 1.7, the version LunaMoo/maxcso vendors, for writing CSO files.
Built as part of Core on every platform. MIT license.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CCxekYqPnzNr5d8Tp1q7aZ
- Fixes#16823
CLOCK_UPTIME_RAW is mach_absolute_time() in nanoseconds (the same value, not
just the same rate). That's the clock MoltenVK reports VK_GOOGLE_display_timing
present times in, which time_now_raw() is meant to match. Apple's
CLOCK_MONOTONIC counts time asleep, unlike Linux's, so on a Mac that had
slept since boot, from_time_raw() of a present time was off by all of it.
It's also cheaper to read: ~19ns vs ~30ns on M1. mach_absolute_time() itself
is ~8ns, but returns ticks, and the timebase on Apple Silicon is 125/3.
Timing risk: time_now_d() no longer advances while the machine sleeps, so a
sleep/wake shows up as a small delta rather than a huge one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The fallback (Switch, BSDs, anything not Windows/Linux/Android/Apple) mixed
clocks: time_now_d() read the wall clock with gettimeofday, time_now_raw()
and Instant a monotonic one, from_time_raw() didn't subtract the start time,
and time_to_unix_utc() added the timestamp to the current time instead of
the start time.
Now every non-Windows platform shares the same code, built on time_now_raw()
and time_now_unix_utc(). Those two use clock_gettime where we know it's there
and std::chrono's steady_clock/system_clock elsewhere. Instant stores a
single nanosecond timestamp, and time_to_unix_utc() (which was missing on
Linux/Android/Apple) is start time plus timestamp, like on Windows.
The time origin is now set in TimeInit() rather than lazily on the first
time_now_d(), which raced between threads. iOS never called TimeInit(), so
it does now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add the audio/atrac/headerfields and audio/audiocodec/at3errors tests
Add the audio/audiocodec/context test
Add the utility/savedata/noupdate and initstatus tests
headerfields already passes: SetData's end sample limit and a sample
offset of 0 counting as a frame match hardware.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Worked out by dumping the context after every call on hardware.
SceAudiocodecCodec now names all of them.
Atrac3+: CheckNeedMem and Init parse the format bytes (sample rate,
channels, frame size) into the context, and neededMem goes by the channel
count. Init also writes the bitrate and the output channels, checks the
rate (44100 or 48000), channels and frame size, and fails unless inited is
exactly 1. InitMono refuses stereo, and decode honours the output channels.
MP3: GetInfo, and every decode, read the whole frame header into the
context. Without a frame GetInfo fails and fills in -1, where we guessed.
AAC: Init takes the ME's list of sample rates (not 12000) and fails with
either flag byte set. GetOutputBytes works for AAC, and goes by the
context for Atrac3+ and MP3.
GetEDRAM gives every codec the same address, and ReleaseEDRAM clears the
allocation at 0x68 (pspautotests audio/audiocodec/context).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>