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>
On hardware InitMono only takes the mono Atrac3 parameters (0x0E, 0x0F),
and the stereo ones fail with 0x807f00ff. Init fills in the context: 44100
at 0x2c, the frame size at 0x30, and the output channels at 0x34, which
InitMono sets to 1. The ME reads that at every decode, and writes a mono
layout as stereo only when it's 2. We accepted every parameter, wrote
nothing there, and always wrote stereo.
A bad frame's ctx.err is 0x183 when a joint stereo frame's second part
lacks its marker, and 0x182 for any other bad sound unit. The decoder now
reports which, and we give 0x183 where we gave 0x182
(pspautotests audio/audiocodec/at3errors).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NativeKey runs on the input thread, but timed mouse wheel releases are
sent from NativeFrame. A press landing between the release check and its
reset lost the release and left the wheel key held. The debug F9 hack
recreated all views straight from the input thread; it now runs on the
main thread.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
NativeKey calls KeyEventToFocusMoves directly on the input thread, while
ProcessHeldKeys iterates heldKeys on the UI thread, which tripped an
iterator assert in debug builds. ProcessHeldKeys also dispatched key
events mid-iteration, where a handler calling EnableFocusMovement(false)
would clear the set under it. Now it collects due repeats under the lock
and dispatches them after.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Like the Vulkan backend: a multisampled framebuffer has multisampled
color and depth textures that are drawn to and kept between passes, and
each pass resolves them into the ordinary ones (depth and stencil to
sample 0), which textures, copies and readbacks use.
BindFramebufferSamplesAsTexture binds the samples, for the shared code's
sample 0 copies. Copies between multisampled framebuffers copy the
samples too, both depth and stencil, since Metal can't copy one plane of
a multisampled texture.
Also skips empty draws, which Metal doesn't allow.
With 4x MSAA, 1012 of the 1061 frame dumps are identical to Vulkan's.
Most of the rest differ at edges because programmable blending blends
each sample with its own color. In Resistance: Retribution, the sample 0
copy for a palette lookup matches the render without MSAA here, and
Vulkan's doesn't.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Remove what nothing used: the vertex texture and sampler commands,
the byte-upload setters, and some unused render manager members.
- thin3d pushes its uniform buffer once for both shader stages.
- Build the thin3d depth/stencil cache key from bytes, so memcmp
doesn't compare padding.
- Only create the upload command buffer when there are uploads.
- Compare copy destinations by their base texture when merging passes,
like the textures the passes read.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
They get their graphics context from the frontend, so they compiled and
linked the Metal backend without being able to use it. Adds
PPSSPP_API_METAL, like PPSSPP_API_D3D11, for the code that only exists
with it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Headless renders to an offscreen backbuffer that nothing ever draws to,
so the GE debugger's screenshot read uninitialized memory. Fail the
readback instead, like headless Vulkan (which has no backbuffer), so the
debugger falls back to the displayed PSP framebuffer.
All the GE debugger buffers (screenshot, render color/depth/stencil,
texture, CLUT) now read back the same as Vulkan's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Runs the same compute shaders as the Vulkan backend, translated to MSL,
in the upload command buffer: levels decode unscaled into the push
buffer and get scaled into a view of the level. Push constants go in a
buffer of their own. Multipass shaders (NNEDI3) use scratch textures
that live for the frame.
Shaders with a constant buffer (only the smiley test shader) aren't
supported.
All the texture shaders give the same result as Vulkan's on LocoRoco
and God of War savestates.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On Metal (and Vulkan through MoltenVK), SPIRV-Cross keeps the function
name, which collides with Metal's texture2d type, so the shader failed
to compile and the screen went black.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Like the Vulkan backend, with "Auto Max Quality" texture filtering give
textures up to 256x256 a full mip chain, generating the levels the game
didn't supply with a blit. Mipmapping is now controlled by the
sampler's LOD clamps alone, as on Vulkan, so the mip filter is always
set.
With max quality on, the frame dumps that change are the same 829 on
both backends, and match Vulkan's to within 41 pixels.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The vertex decoder can write past the end of its output, so a draw
decoded into the last bytes of a push buffer block wrote into unmapped
memory and crashed, now and then, in DrawEngineMetal::Flush. Keep 256
bytes free after each allocation, like the Vulkan backend's push pool.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Like the Vulkan backend, upload the decoder's 5650, 5551 and 4444
pixels as they are, in B5G6R5Unorm, BGR5A1Unorm and ABGR4Unorm with a
swizzle (Metal names packed channels from the lowest bits up, the PSP
stores red lowest), instead of expanding them to 8888. Halves the
decoding writes and memory for those textures.
It also makes Everybody's Golf's dump identical to Vulkan's, which
already sampled these at 16 bits.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>