Commit Graph
47472 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5 7d14efc331 Say why a savestate is refused while online, instead of dropping it silently
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
2026-09-05 13:34:12 -06:00
Henrik RydgårdandClaude Opus 5 be834b4446 Ban freeze-frame in hardcore mode, and tell the user when a savestate is refused
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
2026-09-05 13:29:50 -06:00
Henrik RydgårdandClaude Opus 5 255896e89f Re-check hardcore mode when a savestate op is actually applied
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
2026-09-05 13:27:28 -06:00
Henrik Rydgård b7258cab12 Merge pull request #22224 from hrydgard/kernel-hle-fixes
Claude code review: Kernel
2026-09-05 13:02:27 -06:00
Henrik Rydgård dcc23fca7c Merge pull request #22234 from hrydgard/naett-fixes
naett: Fix leaks, undefined behaviour and error handling across all backends
2026-09-05 12:52:24 -06:00
Henrik Rydgård a15e654f11 naett: Don't hand a closing response to backends that can't see its request
naettClose cleared res->request and then asked the backend to close the
response - but the request is what a backend needs to do that. On Windows it
owns the WinHTTP handles, so the close had nothing to work with, which is part
of why it did nothing at all. Backend first, then clear.

With that in place, the Windows close unhooks the status callback and shuts the
request handle, so a completion raised later can't write through the response
after it's freed. It stops short of a full cancel: a callback already running on
another thread isn't waited for, which needs the HANDLE_CLOSING handshake.

On Apple, invalidateAndCancel returns before the session has finished with its
delegate, so clear the delegate's pointer back to the response first and have
didReceiveData check it, the way didCompleteWithError already did.

Android is still the only backend that genuinely cancels and waits, so naett.h
now says a response should be complete before it's closed rather than leaving
that to be discovered.
2026-09-05 12:07:03 -06:00
Henrik Rydgård da2f30ad46 naett: Stop leaving threads attached to the JVM, and check JNI results
getEnv called AttachCurrentThread and nothing detached. processRequest gets away
with it by detaching its own thread at the end, but naettPlatformInitRequest and
naettPlatformFreeRequest run on whatever thread the caller is using - and a
thread that exits while still attached takes the process down on Android, with
"Native thread exiting without having called DetachCurrentThread". They now
attach only when the thread isn't already, and detach on the way out.

pthread_create's result was ignored. Without a worker nothing ever sets
res->complete, so the caller polls naettComplete forever.

getOutputStream throws for anything from a refused connection onwards, and the
calls after it ran with that exception still pending, which most of JNI doesn't
allow. GetMethodID also returns NULL for a method it can't find, and calling
with a NULL jmethodID aborts the VM - so the call helpers check. Same for a
header whose value list is empty, which handed GetStringUTFChars a null.

Checked with the NDK's clang; the other backends were syntax-checked the same
way, against stub headers.
2026-09-05 12:04:31 -06:00
Henrik Rydgård 60919a2ba9 naett: Fix the curl worker's pipe read, queueing and handle cleanup
The worker reads a queued CURL* out of a pipe eight bytes at a time, tracking
how much it has so far - but it always read into the start of the buffer rather
than at that offset. A short read would have resumed mid-pointer and eventually
handed curl_multi_add_handle a spliced pointer. It never bit because a write
that size to a pipe is atomic, so reads are all-or-nothing, but the code is
written as though it isn't.

The write that queues the request was unchecked. If it ever failed, the request
was never run and never completed, and the caller sits in naettComplete
forever. It reports the failure now, and retries on EINTR.

curl wants an easy handle out of its multi before cleanup; that needs
curl_multi_remove_handle, which meant adding it - and curl_multi_cleanup, for
the init failure paths that leaked the multi handle and the pipe - to the dlopen
table.

workerRunning is written by the worker and read by the request path, so it's an
atomic rather than a plain int. And the read/write callbacks passed the body
callbacks' int return straight back to curl, which takes a size_t: a negative
came through as an enormous count rather than the error it was.
2026-09-05 12:02:22 -06:00
Henrik Rydgård 0e9397b8b4 naett: Harden the WinHTTP backend's header and read paths
WinHttpQueryHeaders only writes the size it needs when it fails with
ERROR_INSUFFICIENT_BUFFER. Any other failure left bufSize at zero, so we
allocated nothing and unpackHeaders walked wcslen over it looking for the
double-null that terminates the list. Check the size, check the second query,
and allocate zeroed with room for a terminator.

winToUTF8, winFromUTF8 and wcsndup can all return NULL - on a failed conversion
or a failed allocation - and not one caller checked. packHeaders is the one that
mattered: its result goes straight into headers[0].

res->bytesLeft is a size_t counting down from what WinHTTP announced. If a read
ever returned more than that, it wrapped to an enormous count and the loop kept
reading.
2026-09-05 11:55:59 -06:00
Henrik Rydgård cb2de6317c naett: Fix the Apple backend's unregistered class and stack-sized headers
createDelegate built its NSURLSessionDataDelegate with objc_allocateClassPair
and then sent it +alloc without ever calling objc_registerClassPair. The runtime
requires registration before a class pair can be used; everything up to then is
still being assembled. Register it, after the methods and the ivar go on.

The response header arrays were VLAs sized from the count the server sent, so a
response with enough headers walked the stack off the end, and one with no
headers at all declared zero-length VLAs, which is undefined by itself. Heap
now, and skipped when there's nothing to read.

The NSURLSession was also kept in the response without retaining it, while the
autorelease pool it came from is drained on the way out of the function. It only
survived because a session keeps itself alive while it has tasks running. Retain
it, release it when the response closes.

Finally, addMethod/addIvar signalled failure with assert alone, which is
compiled out in release - a delegate missing its methods would just never
receive data and the request would hang with nothing logged.
2026-09-05 11:54:49 -06:00
Henrik Rydgård 28962036ef naett: Fix two leaks and the response buffer's overflow
naettFree frees the method and url it strdup'd but never the user agent, which
stringSetter allocates exactly the same way. We set a user agent on every
request, so that leaked on every one of them, on all platforms.

On Linux, headerCallback strndup's each header line and only hands it to the
header list when it finds a colon - the status line and the blank line that ends
the block don't have one, so it leaked those every response, and again per hop
when following redirects.

defaultBodyWriter grew its capacity by doubling an int until the new data fit.
Both the length and the resulting capacity come from the response, so that's
signed overflow on a large one, and a negative capacity then reaches realloc as
a huge size_t. It also assigned the realloc result straight over the old
pointer, so a failed allocation lost the buffer and the memcpy below went
through NULL. Grow in int64_t, cap at INT_MAX, and report failure by returning
short - which is what every caller already checks for.
2026-09-05 11:53:32 -06:00
Henrik Rydgård 86de3307fb Merge pull request #22230 from hrydgard/sceFont-install-fonts
Install sceFont fonts off the disc when we don't have them in the NAND folder
2026-09-05 11:46:56 -06:00
Henrik Rydgård 6d4bc5f261 Merge pull request #22207 from acts-1631/security/fix-pgf-bpe-validation
Validate PGF bits-per-entry fields
2026-09-05 09:48:59 -06:00
Henrik Rydgård 8b69031197 Merge pull request #22215 from hrydgard/shutdown-crash-fixes
Misc shutdown fixes on Windows
2026-09-05 09:47:39 -06:00
Henrik Rydgård 981250daf3 ISOFileSystem: Don't crash when the image has no ISO9660 volume
The constructor leaves treeroot null when it can't find a CD001 volume
descriptor, but GetFromPath walked into it anyway - TreeEntry *entry = treeroot;
then entry->valid - so any path lookup on a failed mount dereferenced null.

Reachable from the firmware installer, which mounts whatever file it's handed
and asks for PSP_GAME/SYSDIR/UPDATE without consulting Error() first. Point it
at a PlayStation disc image, whose descriptor sits behind a Mode 2 subheader and
so fails the signature check, and PPSSPP goes down. Identify_File checks for
CD001 before reporting PSP_ISO, so the game browser was never exposed.

Return null instead, which is what the rest of the function already does for a
path that isn't there, and what every caller expects.
2026-09-05 09:47:00 -06:00
Henrik Rydgård 00939f76fc sceFont: Only require the fonts the running game's firmware could have
The disc-updater install ran when ltn0.pgf was missing, so a font set unpacked
from an old UMD looked complete forever, and a later game wanting a font its own
firmware added silently got a bundled substitute instead.

Requiring the whole registry doesn't work either: firmwares older than a font
can never satisfy it, so we'd unpack the same updater on every launch and
announce it each time. What settles it is that a game can't ask for a font that
didn't exist when it was made. Record the earliest firmware known to ship each
font in the registry, and only require the ones the running game's firmware
would have had.

The version comes from PARAM.SFO's PSP_SYSTEM_VER, with the bundled updater's
version as a fallback. That keeps the whole thing stateless - nothing recorded
that could go stale when flash0 or the ini gets moved around.

Survey of a large library, unpacking flash0:/font from each disc's updater
across firmware 1.50 to 6.60: jpn0 and ltn0..ltn15 are in every one of them, and
kr0.pgf is the only registry font that arrived later - absent in 1.50, present
from 1.52.

Uses one directory listing rather than a stat per font, since on Android's
scoped storage the individual checks are slow.
2026-09-05 09:36:26 -06:00
Henrik Rydgård 5d608e39a3 Merge pull request #22223 from hrydgard/iso-metadata-hardening
Clamp ISO metadata and file sizes to the max possible extent
2026-09-05 09:28:29 -06:00
Henrik Rydgård 64cdcbfca2 Merge pull request #22225 from hrydgard/misc-fixes
Assortment of minor fixes
2026-09-05 09:28:10 -06:00
Henrik Rydgård 28109855f0 Merge pull request #22228 from hrydgard/restart-args-fix
Fix the restart-into-a-screen arguments, broken by the cmdline refactor
2026-09-05 09:27:29 -06:00
Henrik Rydgård d789696405 Fix the restart-into-a-screen arguments, broken by the cmdline refactor
Centralizing command line parsing replaced the hand-rolled --gamesettings and
--touchscreentest argv checks in NativeInit with a single --start-screen=<name>
option, and made an unrecognized "--" argument a hard parse error. The two
System_RestartApp callers still passed the old flags.

So changing the graphics backend killed PPSSPP for good: the new process starts,
fails to parse --gamesettings, and returns 1 from WinMain before a window ever
exists. The error goes to stderr, which nobody sees in a GUI build, so it just
looks like the app quit instead of restarting.

Restarts that pass no arguments (the memstick screen, and the edit-then-restore
path) were unaffected, since an empty argument string makes ExitAndRestart reuse
the original command line.

Also move the TouchTestScreen push inside the touchscreentest branch - it looks
like a brace that didn't move during the refactor, and it would otherwise push a
touch test screen for every --start-screen value.
2026-09-05 08:28:43 -06:00
Henrik Rydgård 9368198d90 Merge pull request #22226 from 4RH1T3CT0R7/fix/sdl-launch-folder-linux
SDL: Open local files and folders with xdg-open on Linux
2026-09-05 07:58:01 -06:00
Henrik RydgårdandClaude Opus 5 111f01481c PSAR: don't decrypt entries a filter is going to reject
Decrypting an entry's contents is by far the most expensive part of
walking an archive, and it happened for every entry before the filter
was even consulted. Now it waits until entryData()/entryCompression()
asks, so pulling just the fonts out of an updater no longer costs a
full firmware decrypt. Records are decrypted independently of each
other, so deferring one is safe.

Unpacking fonts from a 3.11 updater goes 0.365s -> 0.133s; a full
unpack is unchanged and produces identical output. The compression
counts now describe the entries we actually decoded rather than
everything in the archive.

Also adds --unpack-updater-filter to headless, which is how the above
was measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:35:07 -06:00
Henrik RydgårdandClaude Opus 5 a641305c75 sceFont: use the fonts in NAND, and take them off the disc if we have none
sceFont never looked at flash0:, so a firmware the user installed was
ignored and we always fell back to our bundled substitutes - despite
the "ignoring NAND" warning suggesting otherwise. It reads flash0:/font
now, after the game's own fonts and the classic ms0 override.

And if there's nothing in NAND, we unpack just flash0:/font out of the
firmware updater on the running disc, which most UMDs carry. That turns
"install a firmware first" into something that happens by itself for
anyone playing a retail game.

EmulatedModelGeneration moves from InstallUpdateScreen into PSARUnpack
so both callers pick the same firmware file list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:35:07 -06:00
Henrik Rydgård f929a74780 Merge pull request #22221 from 4RH1T3CT0R7/fix/debugger-breakpoint-list-refresh
Win32 debugger: refresh the breakpoint list after memcheck changes
2026-09-04 18:17:31 -06:00
Henrik RydgårdandClaude Opus 5 2389b8ce96 headless: add --save-state, the counterpart to --state
--state could load a savestate but nothing could produce one without a
GUI, so savestate bugs couldn't be reproduced or regression-tested from
a script. This saves one partway through the run, once the game is
actually up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:14:41 -06:00
Henrik RydgårdandClaude Opus 5 5a84f287c4 sceIoRename: refuse wildcards, an existing destination, and don't wait on XDEV
Three ways our rename differed from the PSP's:

- A wildcard in either path was passed through to the host, so
  renaming "test*.txt" could quietly rename a real file. The PSP
  doesn't expand them here, it rejects them outright.
- Renaming onto a file that already exists succeeded, because the host
  rename() replaces the destination. The PSP refuses, and renaming a
  file onto itself counts as that too.
- Crossing devices returned the right error, but after the same wait
  as everything else. The hardware fails that one immediately.

Fixes io/file/rename, moved to tests_good.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:13:10 -06:00
Henrik RydgårdandClaude Opus 5 fd72f308af sceAudio: fix a crash when reserving a channel with none free
GetFreeChannel counted down with an unsigned loop variable, so i >= 0
was always true. With every channel already reserved it wrapped past
zero and kept indexing g_audioChans until it walked off the end -
sceAudioChReserve(-1, ...) segfaulted the emulator instead of returning
"no channels available". Reproduces on audio/sceaudio/reserve, which
crashed before printing anything.

Also gives sceVaudioChReserve the parameter checks it never had. It
took any sample count, channel count and frequency; the hardware allows
256, 1024 or 2048 samples, stereo only, and the same sample rates the
SRC channel accepts. Every value in the test now matches - what's left
there is only reschedule markers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:12:31 -06:00
Henrik RydgårdandClaude Opus 5 d675b467c1 sceUtility: fix the system param string size check and the adhoc channel error
Two things utility/systemparam caught:

A negative size passed to sceUtilityGetSystemParamString went through
Memory::IsValidRange, where it became an enormous range and came back
as a generic -1. The PSP just reports that the string doesn't fit, same
as any other size too small to hold it.

sceUtilityGetSystemParamInt returned 0x800ADF4 for an automatic adhoc
channel unconditionally. The FIXME there wondered whether the hardware
only does that once adhocctl is initialized - it does. Before any adhoc
module is up, which is the state nearly every game asks this in, the
hardware returns 0 and writes the channel out.

Fixes utility/systemparam/systemparam, moved to tests_good.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 18:12:24 -06:00
Henrik Rydgård ef00df5298 docs: Cover msgpipe and interrupt dispatch in the kernel review notes 2026-09-04 18:10:10 -06:00
Henrik Rydgård a9ee254862 docs: Notes from the sceKernel HLE review
Records the recurring bug classes, the things that look wrong but are verified
correct (so they don't get re-flagged), what was deliberately left alone, and
how far the review actually got.
2026-09-04 18:10:10 -06:00
Henrik Rydgård bd3da95236 sceKernelMbx: Don't hang on a corrupt message list
ReceiveMessage walks the packet list until it comes back around to the head.
Every pointer it follows is validated, so a list containing a loop that doesn't
include the head just keeps walking - the emulator hangs inside the syscall with
no way out. Bound the walk by the message count.
2026-09-04 18:10:10 -06:00
Henrik Rydgård 61cb592b36 sceKernelMemory: Check the aligned pool size for overflow
sceKernelCreateFpl and sceKernelCreateTlspl validate blockSize * count with the
formula from hardware, but that works in 4 byte alignment while the actual
allocation uses the alignment from the options struct, which the caller picks.
A large alignment inflates each block enough that the aligned total can wrap:
we'd then allocate a small block but keep the full block count, and
sceKernelAllocateTlspl / the Fpl allocate paths hand out
address + block * alignedSize well outside the allocation.

Also give PartitionMemoryBlock::address a default. Only the savestate
constructor leaves it unset, and DoState returns early when the section is
missing, which left the destructor freeing an uninitialized address.
2026-09-04 18:10:10 -06:00
Henrik Rydgård ed2d118812 sysclib: Fix buffer overflows in strcpy and strcat
Both wrote past the end of guest memory with game-supplied pointers:

- strcpy validated the destination with IsValidAddress, which covers a single
  byte, then wrote strlen(src) + 1 bytes there.
- strcat validated both strings as they were, but the concatenation is longer
  than either, and that's what gets written.

Check that the result actually fits, and copy with an explicit size instead of
the unbounded str* functions.

Also, snprintf with a negative size fell into result.resize(limit - 1), which
converts to a huge size_t and throws.
2026-09-04 18:10:10 -06:00
Henrik Rydgård abed1ee9eb sceKernelModule: Bound the module import parsing
KernelImportModuleFuncs walks data straight out of the module being loaded, and
validated addresses without validating extents:

- The variable relocation list was scanned until it happened to hit a zero word,
  with no bound at all - an unterminated list runs off the end of guest memory.
  Now bounded by what's actually mapped from that address.
- nidData and varData were checked with IsValidAddress, which covers one word,
  and then indexed numFuncs (up to 65535) and numVars times. Check the whole
  array instead.
- The entry walk could read a final PspLibStubEntry that starts just short of
  libstubend and extends past it.

All of these need a corrupt or crafted module, not a normal game.
2026-09-04 18:10:10 -06:00
Henrik Rydgård c4160b4eed HLE: Small kernel cleanups
Initialize the wokeThreads locals that were passed by reference uninitialized in
the event flag, VPL and semaphore timeout handlers. Harmless today since the
callee only ever assigns to them, but every other use in the same files starts
at false.

Do the semaphore overflow check in 64-bit, so a large signal value can't wrap
past it into currentCount.
2026-09-04 18:10:10 -06:00
Henrik Rydgård 00e2e89914 sceKernelHeap: Return a null pointer on failure, not the allocator's -1
sceKernelAllocHeapMemory and sceKernelAllocHeapMemoryWithOption return a
pointer, so failure has to be 0 - JPCSP documents it as "the address of the
allocated memory block, or NULL on error". We passed BlockAllocator::Alloc's
result straight out, which is (u32)-1 when the allocation fails, and returned
UID error codes for a bad heap id. Anything checking for a null pointer took
those for success.
2026-09-04 18:10:10 -06:00
Henrik Rydgård 58c59f2cb4 sceKernelAlarm: Fix crash when a handler deletes its own alarm
AlarmIntrHandler::handleResult looked the alarm up again and passed the result
straight to __KernelScheduleAlarm without a null check - run() a few lines above
does check. An alarm handler that cancels its own alarm and then returns a
positive reschedule value dereferenced null.

Also, sceKernelSetSysClockAlarm validated four bytes and then read eight, and
returned a bare -1 instead of an error code.
2026-09-04 18:10:10 -06:00
Henrik Rydgård 0f94e01241 HLE: Clamp the wait timeout remaining time to zero
CoreTiming::UnscheduleEvent returns the scheduled time minus the current time,
which is negative when the event is overdue but hasn't been processed yet - the
exact situation when a wait is satisfied right around its own timeout. Only the
semaphore clamped it; everywhere else we wrote (u32)cyclesToUs(negative) into
the game's timeout variable, i.e. a huge bogus "remaining time".

Pulled the shared shape into HLEKernel::WriteRemainingTimeout so it can't drift
apart again - event flags, mbx, fpl, vpl, msgpipe, mutex, lwmutex and semaphore
all go through it now. The two thread-end sites keep their own copy since they
unschedule even when the game passed no timeout pointer, and sceUsb just gets
the clamp.
2026-09-04 18:10:10 -06:00
Henrik Rydgård 68fd30bbba Merge pull request #22217 from hrydgard/gpu-fixes
Claude code review: Vulkan
2026-09-04 18:03:47 -06:00
Henrik Rydgård a807ffa984 Merge pull request #22220 from hrydgard/easy-fixes-1.21
Assorted easy fixes for 1.21
2026-09-04 17:52:09 -06:00
Artem Lytkin ec9eb7230e SDL: Open local files and folders with xdg-open on Linux
Since 1.20 the "Show Memory Stick folder" button and the log folder
button in developer tools call System_LaunchUrl with LOCAL_FOLDER or
LOCAL_FILE. The SDL implementation only handled macOS there, so on
Linux the click did nothing. Before that they went through the
SHOW_FILE_IN_FOLDER request, which already forks xdg-open.

Move that fork/exec into a LaunchXdgOpen helper and use it from both
places. The child now calls _exit after a failed exec so it doesn't
run the parent's atexit handlers.
2026-09-05 01:07:46 +03:00
Artem Lytkin 20e0185707 Win32 debugger: refresh the breakpoint list after memcheck changes
The list reloads on SystemNotification::DISASSEMBLY, which every CPU
breakpoint mutation in BreakpointManager has posted since 8d0d601b5, but
none of the MemCheck ones do, so a memory breakpoint added from the list
(or the disasm view's dialog, the ImDebugger, the WebSocket API) only
showed up after the next Break. The enable checkbox then toggled against
the list's stale copy of the memcheck, so re-enabling one it had just
disabled disabled it again. Post the notification from the memcheck
mutations too, and from the condition setters of both kinds, since the
list's cached copy also feeds the edit dialog.
2026-09-05 00:54:50 +03:00
Henrik RydgårdandClaude Opus 5 fbdb54300e Put a ceiling on the CSO frame size
Follow-up to #22208, which bounded the index table but left the frame
size itself unbounded - readBuffer and zlibBuffer are sized straight
from it, so a 96-byte header could still ask for a couple of gigabytes.
Harmless enough on 64-bit, where the pages never get touched, but a
32-bit build would just fail the allocation.

Real images use 2KB to 64KB frames, so 16MB leaves plenty of headroom.
All 19 CSOs I have on hand still load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 30bea36141 docs: explain the [x]/[r] markers in pspautotests output
They record whether a reschedule happened while the code under test
ran, so a diff where only the marker differs is a scheduling
difference, not a wrong value - worth knowing before going looking for
a value bug that isn't there.

Also fixes the O/E description, which had them the wrong way round: O
is PPSSPP's output, E is the expected file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 c93b0de8cb Translate "Show timestamps in chat"
31 languages, following each file's existing chat terminology (chatt,
bate-papo, obrolan, sembang, 聊天, الشات, ...). The rest are left to fall
back to English rather than guessed at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 a506d0be6f Lowercase the extension reported by ZipFileLoader
Identify_File and friends compare against lowercase extensions, the way
Path::GetFileExtension returns them, but ZipFileLoader passed the name
from the zip through unchanged. So a file stored as e.g. "DUMP.PPDMP"
inside a zip failed to load, while "dump.ppdmp" worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 fbdf611fc4 Chat: keep more lines of history, and add timestamps
The log was trimmed to 50 lines, which isn't enough to scroll back
through a conversation - now 250. Chat entries also carry the time they
arrived, shown as a dimmed HH:MM in front of the name, behind a new
"Show timestamps in chat" setting.

The timestamp is kept next to the text rather than baked into it, since
the chat view finds the sender by splitting the line at the first colon.

Fixes #15444

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 eb1bc34123 Map the right analog stick by default on Android pads
The generic Android pad map and the Retroid map were the only pad
defaults without it, so the right stick did nothing until mapped by
hand. Uses the same axes and directions as the desktop pad default.

Fixes #21591

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 b115206a66 SDL: Release all input when a controller is disconnected
The last platform that was missing this - it was held back waiting for
the SDL 3.0 migration, which has now happened. Without it, whatever was
held when the pad vanished stays held, which tends to walk the player
off a cliff.

Fixes #20418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:54 -06:00
Henrik RydgårdandClaude Opus 5 b5bafa43b8 Prefer upper case PSP/SHADERS and PSP/THEMES
These two were lower case for legacy reasons, unlike every other
directory, which actually matters on case sensitive file systems like
the one iOS uses. An existing lower case directory is still used if
there's no upper case one, so nobody has to move their files.

Fixes #20527

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-04 13:10:53 -06:00