Commit Graph
100 Commits
Author SHA1 Message Date
Henrik Rydgård 3d619dad0e Merge pull request #22156 from hrydgard/tab-focus-recent
Fix a tab focus problem
2026-08-28 22:41:33 +02:00
Henrik Rydgård a61e8135dd Fix a tab focus problem 2026-08-28 15:29:14 +02:00
Henrik Rydgård fe13074679 Merge pull request #22155 from saboten731/pr/savedata-unused-savename-list
Avoid validating unused savedata name lists
2026-08-28 14:58:07 +02:00
Henrik Rydgård fad7b93776 Merge pull request #22151 from hrydgard/ui-tab-navigation
UI: Add tab navigation
2026-08-27 22:17:47 +02:00
Henrik Rydgård e751f6f138 Merge pull request #22152 from NABN00B/slider-buttons
Replace plus/minus strings with UI icons in SliderPopup
2026-08-27 22:09:42 +02:00
Henrik RydgårdandClaude Opus 5 ea1ad8ffed UI: Tab and Shift+Tab move focus through the view hierarchy
Unlike the directional moves, this doesn't look at where anything ended up on
screen - it walks the hierarchy in the order views were added, flattening nested
groups in place. That's what makes it predictable in the layouts where "what's
to the right of this" has no good answer.

A view is a stop if it's focusable and enabled, the same test the directional
moves apply, so the two agree on what's reachable. Hidden subtrees are skipped
whole, which is what keeps a TabHolder's inactive tabs - V_GONE rather than
removed - out of the order without any special casing. Containers are gated on
visibility only, not enabled, matching Key/Touch/Axis: disabling a container
doesn't stop its children being interactive anywhere else either.

Ctrl+Tab stays with ChoiceStrip, which uses it to switch tabs.

focusMoves now holds FocusMove rather than raw keycodes, so the direction is
decided in one place while the modifiers are still around, and a held key
repeats in the direction it was originally pressed with - the synthesized repeat
has no modifiers of its own. That also retires the keycode switch in
UpdateViewHierarchy and IsScrollKey, which had no other callers.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 20:52:20 +02:00
Henrik RydgårdandClaude Opus 5 8cb5ce7585 Restore the modifier flags on key events reaching the UI
NativeKey builds a copy of the key with the Ctrl/Shift/Alt/Meta flags attached,
but has been queueing the original ever since a47edbf6ef moved the dispatch from
a direct g_screenManager->key(modKey) call to the event queue - so modKey has
just been dead since then, and nothing downstream ever sees a modifier.

That's every shortcut matched on one: Ctrl+Tab tab switching in ChoiceStrip,
Ctrl+F in the game list, and Ctrl+C/V/Z in text fields.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 20:52:07 +02:00
Henrik Rydgård 04bf3e56ef Merge pull request #22149 from hrydgard/symbol-map-cache-fix
ImDebugger: fix stale symbol list after a game is reloaded
2026-08-27 20:19:03 +02:00
Henrik Rydgård 518bc7a7fc Merge pull request #22147 from saboten731/pr/libretro-auto-load-savestate
Fix PPSSPP libretro startup auto-load savestate restoration
2026-08-27 19:16:47 +02:00
Henrik Rydgård eda06f02d8 Merge pull request #22144 from hrydgard/symbol-demangler
Debugger: Demangle C++ symbol names when loading ELF symbols
2026-08-27 18:56:34 +02:00
Henrik RydgårdandClaude Opus 5 daa18fc25a ImDebugger: fix stale symbol list after a game is reloaded
The disasm window cached the flattened symbol list and only rebuilt it when one
of three menu items said so. Nothing marked it dirty when a game booted or
exited, and a new SymbolMap is allocated per boot, so the list kept showing the
previous game's functions.

Give SymbolMap a version counter that every mutator bumps, and let the window
compare against it instead. The counter is process-wide rather than per-map, so
a fresh map can't hand out a version a cached copy already holds.

Also re-find the selected symbol by address after a rebuild (the index means
something else afterwards), and drop the unused symbol cache members in
ImMemWindow.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01SfY7iFJEjmRXf1XGrTs4MF
2026-08-27 10:43:08 +02:00
Henrik Rydgård bef07b7a6b Merge pull request #22146 from NickWick13/patch-3
Update and improve Swedish translation
2026-08-27 09:06:34 +02:00
Henrik RydgårdandClaude Opus 5 049bcd5483 Demangle C++ symbol names when loading ELF symbols
C++ homebrew has an unreadable symbol table -
everything is _ZN10PxRenderer7DrawImmE... - which makes the disassembly and
symbol list nearly useless. Add an Itanium C++ ABI demangler and run ELF
symbols through it on load, in both ElfReader::LoadSymbols (unstripped EXECs,
which is what a CMake pspdev EBOOT actually contains) and the companion-ELF
path.

The demangling standard is called Itanium for historical reasons - it
was defined for Itanium but ended up being almost universally
applicable.

Written from scratch rather than using __cxa_demangle, which doesn't exist on
MSVC/UWP, or vendoring LLVM's demangler, whose license doesn't fit. Anything
unrecognized (arbitrary constant expressions, decltype) aborts the parse and
the caller gets the original mangled name back, so a caller never sees a
half-parsed result. Recursion is depth-capped since the input comes from a
file we didn't write.

Checked against c++filt as an oracle: of 1089 mangled symbols in a real C++
homebrew EBOOT, one differs; of 55189 from libstdc++/libLLVM/cc1plus, 22
differ and 413 are declined. Fuzzed with 220k mutated and random inputs under
ASan/UBSan.

Also adds a right-click menu to the ImDebugger symbol list.

Note that SymbolMap stores names in char[128], so the longest STL names get
truncated in the UI. Still far more readable than the mangled form.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01X3DbkJ8ShYiXU7q5Tv1LZu
2026-08-26 08:12:45 +02:00
Henrik Rydgård adccd302e5 Merge pull request #22143 from a-blondel/feature/madden06
Add madden 06 to infra-dns.json
2026-08-26 00:19:56 +02:00
Henrik Rydgård a0c10a0ea6 Merge pull request #22142 from darkguy2008/libretro-pause-handshake
libretro: restore emu-thread pause handshake so retro_serialize can't deadlock
2026-08-25 17:36:58 +02:00
Henrik Rydgård 4b57da4450 Merge pull request #22141 from hrydgard/mmio-work
Implement MMIO for the JIT
2026-08-25 01:11:54 +02:00
Henrik Rydgård a810597ac0 Implement MMIO for the JIT (by falling back to the interpreter for load/stores from kernel addresses)
Fixes the VSH in JIT mode (but NOT ir)
2026-08-25 00:22:40 +02:00
Henrik Rydgård fb385e7627 Merge pull request #22138 from hrydgard/vsh-auto-install
Allow auto-installing firmware from EBOOT.PBP firmware updates
2026-08-24 18:01:48 +02:00
Henrik Rydgård 442dc91556 Merge pull request #22139 from hrydgard/more-nid-names
Apply proper names to some more HLE functions
2026-08-24 18:01:38 +02:00
Henrik Rydgård 11b4509158 Apply proper names to some more HLE functions 2026-08-24 17:37:39 +02:00
Henrik RydgårdandClaude Opus 5 65592effc5 Block until game info is ready in LaunchFile
When launching a file from outside the main screen (file association, shortcut,
drag-and-drop), the info hasn't been computed yet, so the file type and ID checks
that decide what to do with the file were reading empty data. Add
GameInfo::WaitUntilReady() - a condition variable signalled from
MarkReadyNoLock(), which every exit path of the work item goes through - and use
it there.

Also demote a noisy PRX decryption log line to DEBUG.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EMLTdwyyzU6Mze3w8VC2JL
2026-08-24 17:23:20 +02:00
Henrik RydgårdandClaude Opus 5 cc1cdfcec4 Translate the firmware updater install strings
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-24 17:23:20 +02:00
Henrik RydgårdandClaude Opus 5 4b761f0715 Offer to install PSP firmware updaters instead of running them
Opening an official updater (a PSP/GAME/UPDATE EBOOT.PBP, identified by the
MSTKUPDATE disc ID) from the main screen now brings up a confirmation dialog
that unpacks the firmware into the NAND directory, where the emulated
flash0/flash1 live. Running the updater itself doesn't work, so there was
nothing useful to do with one before.

Unpacks the file list for the model we claim to be (iPSPModel), on a worker
thread, with a progress bar - for which PSARUnpackOptions gets an optional
progress callback.

AGENTS.md: translate UI strings last, in a separate commit

The English string is what all ~47 languages get derived from, so rewording it
after the sweep means redoing the sweep. Check the wording first.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-24 17:23:14 +02:00
Henrik Rydgård c989c2553e Merge pull request #22126 from hrydgard/vsh-hle-boot
Boot the VSH (XMB) through HLE
2026-08-24 13:30:08 +02:00
Henrik Rydgård 9205eb642a UWP buildfix, tweaks and de-clauding 2026-08-24 12:15:31 +02:00
Henrik Rydgård e9cdb2b20f UI work for VSH (Windows and ImGui) 2026-08-24 11:13:00 +02:00
Henrik RydgårdandClaude Opus 5 9de3eb1244 Answer the UMD region check, which the VSH opens with an error without
sceIoDevctl 0x01E18030 asks whether the disc's region matches the console's.
Unusually it answers through the return value rather than an output buffer -
1 matches, 0 doesn't - so leaving it unimplemented read as a mismatch, and the
VSH opened on "This disc cannot be started. The region code is not correct.",
asking 12 times. PPSSPP has no region-locked discs; anything it can load is
something it should run, so this always matches. The call now happens once.

Behind it is a second thing, not fixed here: the VSH believes a disc is inserted
at all because nothing in PPSSPP models an empty drive. sceUmd reports
PSP_UMD_PRESENT | PSP_UMD_READY unconditionally, and devctl 0x01F20001 always
answers "game disc". JPCSP answers "no disc" when no ISO is loaded, which is why
it never reaches the region question. Giving those two a notion of "no disc"
would be the real fix - low risk for games, which always have one, but it is on a
path every game uses, so it is written up in docs/VSHBootInvestigation.md rather
than done as a drive-by.

With this the shell reaches the interactive XMB: the error is dismissable with
circle and the menu behind it works. The per-frame display list stops settling
into one repeated frame and alternates between 45 and 48 stall points, which is
the headless-visible sign of a live menu rather than a static dialog.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 11:13:00 +02:00
Henrik RydgårdandClaude Opus 5 708e7e1d9c Decrypt the VSH's XMB index: add PRX decryption type 9
pspDecryptPRX() tried types 0, 1, 2, 5 and 6. flash0:/vsh/etc/index_XXg.dat -
the index of what the XMB shows, fetched through sceResmgr_9DC14891 - needs
type 9, so it failed and the shell had no menu to build.

Type 9 is type 6 with three differences, all following from a type 9 file
carrying a real ECDSA signature at 0x104..0x12C where a type 6 file has nothing:

- The "must be empty" header check stops at 0x104 instead of 0x10C. The index's
  signature starts there, so 8 of its bytes were failing type 6's check - the
  original failure.
- The signature is left out of the hashed header rather than fed into it. JPCSP
  zeroes buf2[0x34..0x5C), which is that same range once its header
  rearrangement is undone, so PRXType9 just leaves the field zero.
- ecdsa_hash in the KIRK CMD1 header stays 0. Type 6/7 set it, but the branch
  type 9 takes writes only the mode word, and setting it made KIRK reject the
  block.

Tried last in the chain: its header check is a subset of type 6's, so a genuine
type 6 PRX would pass it and then fail on the hash, and trying it earlier would
shadow the real answer. False positives are not really possible either way - the
SHA1 check inside has to match before anything is decrypted.

Verified end to end: 496 bytes in, 159 out (the comp_size in the header),
starting "release:". sceResmgr checks that prefix and says so in its log line,
since a wrong-but-plausible decrypt would otherwise look like success here and
fail much later as an unreadable index.

The VSH now draws something different - the per-frame display list settles at 24
stall points rather than 38 - but what it shows is not visually confirmed;
framebuffer readback doesn't work under headless on either backend.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 11:13:00 +02:00
Henrik RydgårdandClaude Opus 5 e7e8d362f0 Add sceResmgr, which is what the VSH uses to decrypt the XMB index
Found the cause of the red error screen the VSH ends on. Every resource load in
the boot succeeds - fonts, all the plugin RCOs, topmenu_icon.rco - and then:

  sceIoOpen(flash0:/vsh/etc/index_02g.dat) -> fd 8
  sceIoRead(8, 092a2d40, 496)
  sceIoClose(8)
  unresolved import sceResmgr/9dc14891, called from 'vsh_module'
  sceKernelExitDeleteThread(1)

index_02g.dat is the index of what the XMB displays, and it is encrypted (it
starts "PSPsysGP"). sceResmgr_9DC14891 decrypts it. There was no sceResmgr module
at all, so the call trapped, the index stayed encrypted, and the ScePafJob thread
building the top menu exited - a shell with everything loaded and nothing to show.

This adds the module and the three tags it needs (0x0B2B90F0/91F0/92F0, keys and
code 0x5C) to PrxDecrypter.

It is not the whole fix yet: pspDecryptPRX() tries decryption types 0, 1, 2, 5
and 6, and this needs type 9, which JPCSP passes explicitly. So the call is now
reached and fails cleanly with a logged error instead of trapping, but does not
yet decrypt. Type 9 is a variant of type 2 and is the next job; the notes in
docs/VSHBootInvestigation.md say where it is in JPCSP and how to check a port
(159 bytes out, starting "release:").

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 11:12:59 +02:00
Henrik Rydgård 0f36b3310f Merge pull request #22137 from hrydgard/emu-api
Document and wrap the PPSSPP "emulator:" devctls (API for homebrew)
2026-08-24 11:03:57 +02:00
Henrik RydgårdandClaude Opus 5 f449fa6ab2 Implement a batch of the VSH's unresolved imports (and document three we must not)
Mostly small stubs:

- sceImpose: GetParam/SetParam/Changes/SetStatus, plus the 6.60 alias of
  sceImposeGetBatteryIconStatus. Also fixes that function's first output - it
  is a plain "is it charging" boolean, not a BATTICON_ value. We wrote
  PSP_IMPOSE_BATTICON_NONE (0x80000000) there, which games ignore but which the
  VSH reads as "no battery" and draws the empty-battery indicator for. These
  are the bulk of the traffic: the VSH calls sceImposeChanges once a frame, so
  this alone removes ~10000 trapped calls from a boot.
- SysMemForKernel: sceKernelSetRebootKernel, sceKernelSetUmdCacheOn.
- scePower_driver: scePowerSetWakeupCondition.
- sceHprm_driver, sceUsb: one NID-named call each, as in JPCSP.

Three groups are deliberately left unresolved, with comments explaining why,
because resolving them lets real flash0 drivers walk into hardware we do not
emulate and the boot dies where it used to reach the shell:

- ThreadManForKernel mutex/fpl NIDs: the NAND and ID storage drivers use these
  to init, then poll the NAND controller at 0xbd101300 forever.
- InterruptManagerForKernel intr registration: 31 calls, then a stall in GE
  list execution with no plugin module ever started.

73 unresolved import hits over 37 distinct module/NID pairs remain in a VSH
boot, mostly sceSysEventForKernel, sceSuspendForKernel and the various
*_driver modules that need real hardware behind them.

The sceImpose savestate section goes to v2 for the two new state variables.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 10:58:35 +02:00
Henrik RydgårdandClaude Opus 5 4667f5e2da HLE: implement sceKernelLoadModuleVSH - the VSH now draws its UI
ModuleMgrForKernel/0xD5DDAB1F is how the VSH loads its own plugins. The XMB's
interface lives in flash0:/vsh/module/*_plugin.prx and vshmain pulls those in
through this kernel call rather than the user-mode sceKernelLoadModule, so the
existing note that vshmain never imports sceKernelLoadModule was true but
incomplete - it imports this instead, and it was unresolved.

The consequence was quiet: vshmain got no module id back and then called
sceKernelStartModule with id 0, which failed with UNKNOWN_MODULE. None of the
plugins that populate the XMB ever ran. The scene still had its containers,
which is why every frame set up render state per node and drew nothing inside
them - the "6x render-state-setup, 0 draws" symptom this investigation has been
chasing.

Also implements 0xD86DD11B sceKernelSearchModuleByName, the other unresolved
ModuleMgrForKernel import.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 10:58:35 +02:00
Henrik RydgårdandClaude Opus 5 23f219d276 docs: add VSHBootInvestigation.md
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-24 10:58:35 +02:00
Henrik Rydgård 75bd16d0f4 Apply two hacks to scePaf memory-arena hack
For some reason, a pointer used to allocate the heap for scePaf is not
initialized. This hacks aroung that.

Additionally zero out the specific 4-byte "category 1 alarm count" address in vsh_module.

This gets us much further.
2026-08-24 10:58:35 +02:00
Henrik Rydgård 6d14a10d83 HW: implement real Syscon serial protocol emulation (SysconSerialMMIO)
Extends the SysconSerialMMIO stub added for VSH boot into a real command/
response protocol matching uofw's Syscon_cmd() reference exactly (packet
framing, checksum, GPIO4 "response ready" handshake via a new GpioMMIO
cross-module hook), with handling for NOP/read-write clock/read-write
alarm commands.

Still hitting a SIGSEGV though.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
(cherry picked from commit 11887bf9e1fecd1eac56ec705c01b6fcfac09b2e)
2026-08-24 10:58:35 +02:00
Henrik Rydgård ed5c05fb28 VSH boot: load real kd/ kernel driver modules
Extends LoadAndStartVshKernelModules() to load the 11 real kd/*.prx
kernel drivers for --vsh (dmacman, systimer, memlmd_01g,
loadexec_01g, lowio, idstorage, syscon, rtc, wlan, wlanfirm_01g, utility),
ahead of the existing 4 VSH-specific modules.
Only active when g_runningVSH, no effect on normal game boot.

Improve implementations of sceKernelSm1ReferOperations and sceKernelIsIntrContext.

Add some more MMIO stubs (GPIO, SYSCON).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
(cherry picked from commit 7f3168b7df85e47438900016c9ee7d7ef01a0a28)
2026-08-24 10:58:35 +02:00
Henrik Rydgård 1d991557e8 Extend LoadExecForKernel with real VSH loadexec/exit syscalls
Adds SceKernelLoadExecVSHParam and fills in the rest of LoadExecForKernel's
NIDs: real implementations for sceKernelExitVSHVSH/Kernel
(mirrors sceKernelExitGame) and sceKernelLoadExecBufferVSHUsbWlan (loads
an exec from an in-RAM buffer instead of a file, plus UNIMPL stubs.

sceKernelLoadExecBufferVSHUsbWlan needed __KernelLoadExec split into a
file-reading front end and a shared __KernelLoadExecFromPtr back end
that both it and the new buffer-based path call into.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PSNaZnHCjmryS3ziVN9gZU
(cherry picked from commit 4adbedec9e2e3221113c9e5ad00b248bba1153e1)
2026-08-24 10:58:34 +02:00
Henrik RydgårdandClaude Sonnet 5 04a8948ded Add a VSH-only allowlist for real-loading flash0 modules, sceVshCommonUtil stub and sceVshBridge
Load and start VSH's kernel modules before booting vshmain.prx

A few flash0 modules (vshbridge.prx, paf.prx, common_gui.prx,
common_util.prx) should run for real once we know we're
actually booting the VSH rather than a game, since our fakes are unlikely
to be good substitutes for the genuine thing.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PSNaZnHCjmryS3ziVN9gZU
2026-08-24 10:58:34 +02:00
Henrik Rydgård 0dbb79bcd9 Document and wrap the PPSSPP "emulator:" devctls (API for homebrew) 2026-08-24 10:46:04 +02:00
Henrik Rydgård 0e2dfad02d Merge pull request #22136 from hrydgard/nand-mounts
Mount flash from PSP/NAND/flash0, flash1 etc, fix host0: to be more useful for homebrew
2026-08-24 10:45:50 +02:00
Henrik Rydgård b3a2178a5c ImDebugger: Show per-thread current directory in threads view 2026-08-24 10:21:27 +02:00
Henrik Rydgård 8a26e39792 Fix ImDebugger bug 2026-08-24 09:53:40 +02:00
Henrik Rydgård 78ef1eae82 Instead of flash0directory, have a NAND subdirectory under PSP, where you can keep flash0, flash1 etc.
Also make it configurable via command line.
2026-08-24 09:53:37 +02:00
Henrik Rydgård 786a1530de Correct host0: mount 2026-08-24 09:30:44 +02:00
Henrik Rydgård b437ba0fc6 MetaFileSystem: Add subdirectory mounts 2026-08-24 09:30:44 +02:00
Henrik Rydgård 632fa10b23 Use VFS directly to read HLE-sceFont fonts instead of relying on the flash0: mount 2026-08-24 09:30:42 +02:00
Henrik Rydgård e4f66f3809 Merge pull request #22132 from hrydgard/langtool-updates
Langtool updates, translation fixes
2026-08-23 17:29:33 +02:00
Henrik Rydgård 2c932e9a59 Merge pull request #22131 from hrydgard/gameinfo-cache-bundled-update
GameInfoCache: report the firmware updater bundled on a game disc
2026-08-23 17:12:36 +02:00
Henrik RydgårdandClaude Opus 5 396f9e802d GameInfoCache: report the firmware updater bundled on a game disc
Adds GameInfoFlags::BUNDLED_UPDATE_INFO, holding the version, title, size
and timestamp of the updater in PSP_GAME/SYSDIR/UPDATE. It comes from the
PARAM.SFO and the directory entry next to the archive, so it's a couple of
small reads on the ISOFileSystem the worker already has open - no
decryption, and DATA.BIN itself is only sniffed for its magic. Only
computed for ISOs; everything else is marked complete with an empty struct.

ISOFileSystem now parses the date out of the ISO9660 directory record,
stored as Unix UTC seconds and reported as the PSP's atime/ctime/mtime.
Those used to always read back as zero, so games calling sceIoGetstat on a
UMD file saw 1900 where hardware gives the mastering date.

Shown on GameScreen as e.g. "Firmware update on disc: 6.60 (2011-10-05),
25.6 MB".

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0149QcTVgZEXKXbgHyvXF4ZY
2026-08-23 16:44:38 +02:00
Henrik RydgårdandClaude Opus 5 c9ff8f55fe langtool: Mark strings that are deliberately the same as the English one
Plenty of strings are legitimately identical in the target language - "Vsync",
"Status", "Ad Hoc multiplayer" - but the only test we had for "is this
translated" is "does it differ from English", so those got sent to the AI again
on every single run of finish-language-with-ai, costing tokens and giving it
another chance to translate something that shouldn't be.

They now get a "# same as English" comment and are skipped. finish-language-with-ai
writes it when the AI hands the English string back unchanged (which the
validation added earlier was throwing away as an error), and import-single writes
it when a language's line matches the en_US one. Deleting the comment puts the
key back in play.

Also stop comparing values with their comments still attached while working out
what's untranslated, which was leaking "# AI translated" into the context block
of the prompt.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 a1d205539c langtool: Let import-single create the key in en_US.ini too
It skipped the reference file, so adding a new string meant running
add-new-key-value first just to get the key into en_US.ini (and, as a side
effect, the English text into all 46 other files whether they were about to be
translated or not). Now an en_US line in the imported file is treated like any
other language, minus the "AI translated" comment, since that one is the string
the others were translated from rather than a translation.

So the workflow is one command plus validate. Languages left out of the import
simply don't get the key, and fall back to the English string at runtime -
copy-missing-lines is still there if you want the placeholder written out.

Instructions in AGENTS.md and the /add-string command updated to match.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 10bfda59ee Document the translation workflow, and add an /add-string command for it
Translating a UI string well means knowing what it does - what widget it is,
what the placeholders hold, how the neighbouring strings in that language are
phrased. langtool's AI commands can't know any of that, which is why their
prompt has a hand-maintained glossary that grows every time someone spots a bad
translation. An agent working in the repo can just go look.

So: AGENTS.md now describes doing the translating that way and letting langtool
do the file surgery (add-new-key-value, import-single, validate) instead of
hand-editing 47 files, and .claude/commands/add-string.md wraps it as a slash
command. Both say to skip a language rather than guess at it - the English
fallback is fine, a confident wrong translation nobody can proofread is not.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 bb4563e6ca Fix the broken translations that langtool validate found
57 strings across nine language files:

* 33 in km_KH where a space snuck in between the % and the digit ("% 1"), plus
  one where "%0.2f Hz" ended up as "0.2% f Hz".
* fa_IR "Quick chat %1" had the %1 replaced by a Persian numeral, and ko_KR
  "Submitted %1 for %2" lost its %2.
* tr_TR "Earned" had the key translated instead of the value, so the whole
  sentence with its four placeholders was replaced by the word "Earned".
  The new Turkish string needs a look from someone who speaks Turkish.
* 20 strings across five files that were wrapped in quotes. PPSSPP's ini parser
  strips those on load, so nothing changes on screen, but they tripped the
  validator.

Turkish writes percentages as "% 0", so the "% <digit>" fix was only applied
where it makes the placeholders match the English string again.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 47ab1daac2 langtool: Fix losing translations when two keys share an English string
finish-language-with-ai maps the English string the AI translated back to the
key it belongs to, but the map was keyed on the English string, so when two keys
in a section share one - [Graphics] "Texture Filter" and "Texture Filtering",
[Error] "Error reading file" and "The file is not a valid zip file" - only the
last one survived and the other silently never got its translation.

Map to a list of keys instead and update all of them, and stop sending the same
line to the AI twice while we're at it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 398d8238bd langtool: Validate translated strings, and check AI output before writing it
Placeholders like %1 and %d have to survive translation intact, and they don't
always: the new "validate" command finds 37 strings across four language files
where one got dropped, localized into another script, or split with a space.
It also catches empty translations, line breaks and stray quotes, and exits
non-zero so it can be used as a check in a script.

The same checks now run on everything the AI returns, before it's written to a
file - plus a check that it didn't just echo the English string back at us. A
language that fails is skipped instead of aborting the whole run.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 9398629ddf langtool: cargo fmt
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik RydgårdandClaude Opus 5 77e356961d langtool: Add Claude as an alternative to OpenAI for AI translation
New claude.rs talks to the Anthropic Messages API, and ai.rs picks between the
two providers. The provider is chosen with --provider, or guessed from the model
name, or from whichever of ANTHROPIC_API_KEY/OPENAI_API_KEY is set.

Also update the dependencies (and bump the clap requirement to 4.6).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0164UKoXpz9r155TRfQsoc3H
2026-08-23 16:18:07 +02:00
Henrik Rydgård a67ae4e7f6 Merge pull request #22130 from hrydgard/more-ci-libretro
Add CI for libretro's MSYS2-based Windows build
2026-08-23 16:13:11 +02:00
Henrik RydgårdandClaude Opus 5 2396ac7acb Libretro buildfix: drop WebSocket stubs duplicating the inline ones in the header
0ed6b97921 added inline no-op stubs to WebSocket.h under __LIBRETRO__, but the
older non-inline ones in libretro.cpp stayed, so every libretro build has had two
definitions of each. gcc/clang inline the trivial bodies away and emit no symbol,
so it only shows up with MSVC, as LNK2005 against Core.o/Breakpoints.o.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01FzzCUp8y1ahgVueb1Cq92Y
2026-08-23 15:49:15 +02:00
Henrik RydgårdandClaude Opus 5 936ea927f7 CI: Add the libretro Windows MSVC builds that libretro's GitLab CI runs
Builds ppsspp_libretro.dll with make + cl.exe from MSYS2, for x64 and x86,
mirroring the two Windows jobs in .gitlab-ci.yml. That toolchain differs from
both the VS solution and our gcc/clang libretro builds, so it catches errors
neither of them does - multiply-defined symbols in particular, since MSVC emits
inline functions that gcc/clang inline away.

Visual Studio is located here and passed in as VsInstallRoot rather than left to
the Makefile's "cmd //c bash VSWhere.sh", which fails silently if COMSPEC or
ProgramFiles(x86) don't survive into the MSYS2 shell.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01FzzCUp8y1ahgVueb1Cq92Y
2026-08-23 15:49:15 +02:00
Henrik Rydgård c09e6bddaa Merge pull request #22128 from kotcrab/struct-viewer-fixes
Fix struct viewer after memory access cleanup
2026-08-21 21:31:03 +02:00
Henrik Rydgård 3f192a535e Merge pull request #22125 from hrydgard/vsh-prep-2
VSH: More preparations
2026-08-21 11:44:22 +02:00
Henrik Rydgård a0ecf545a6 Implement or stub assorted functions the VSH is calling 2026-08-21 10:56:06 +02:00
Henrik Rydgård 4fb5d5965c MMIO: Simplest possible kernel-mode permission check, start work on supporting in JIT 2026-08-21 10:32:22 +02:00
Henrik Rydgård d94a40931e Log the debugger port at NOTICE, remove fprintf logging of the port 2026-08-21 10:32:21 +02:00
Henrik RydgårdandClaude Opus 5 afb2eb0623 AGENTS.md: document that new entries in an existing HLE module's function
table must go at the end too, not just new module registrations

Also note that a stale binary makes a bisect lie consistently

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-21 10:32:21 +02:00
Henrik Rydgård c6061aebca Debugger: don't persist log.channel.set changes to the saved config
log.channel.set is meant for temporary, session-only diagnostic
tweaks (e.g. quieting a noisy channel while investigating something
over the WebSocket debugger). It was going through the same
SetLogLevel/SetEnabled calls the UI settings use, so a normal app
exit would persist whatever channels/levels the debugger last left
set, silently overwriting the user's real saved log preferences for
future runs - discovered when a later session's default logging
looked "off" for no apparent reason.

LogManager now tracks whether the debugger has touched channel
config this run and skips SaveConfig() entirely if so, leaving
whatever's already on disk untouched.
2026-08-21 10:32:21 +02:00
Henrik Rydgård e64ed141b3 Merge pull request #22124 from hrydgard/psar-unpack
PSAR file format support: Automatic unpacking of firmware updates
2026-08-21 10:31:58 +02:00
Henrik Rydgård 4ecf38f334 Merge pull request #22123 from hrydgard/libretro-cmake-cleanup
libretro: Reduce the amount of files that the libretro CMake build includes
2026-08-21 10:09:08 +02:00
Henrik RydgårdandClaude Opus 5 8f9b2fc1b1 docs: describe the PSAR archive format
Written from reading 43 firmware versions, 1.50 through 6.61, while building the
unpacker - most of it isn't obvious from the code, and several parts only show
up if you look at more than one generation of updater.

Covers the three shapes an updater arrives in, the archive header and the record
layout, the two-step block decryption (KIRK CMD7 demangle, then an ordinary PRX
blob), the entry fields, the compression formats, and the three different naming
schemes with the DES-encrypted file lists that back two of them - including
which key set goes with which firmware, though not the key material itself,
which is already in the source.

Ends with the gotchas that each cost a debugging round: walking by the length in
the header rather than the file size, the slack the decrypter needs but the last
record can't provide, not assuming which entry numbers are file lists, and the
path spelling changing between generations.

Everything in here was checked against an actual updater.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-21 10:01:13 +02:00
Henrik RydgårdandClaude Opus 5 9c71fca33a Loader: identify an encrypted PRX the same as a plain ELF
A firmware dumped off a console has its modules decrypted, while one unpacked
from an updater still has them as ~PSP blobs. IdentifyFile knew about ELF and
PBP but not that, so the loader refused those outright - even though
__KernelLoadELFFromPtr decrypts ~PSP perfectly well once it gets that far, which
is how every encrypted game EBOOT loads.

Checked against a vshmain.prx unpacked from the 6.61 updater: it now gets as far
as "Decrypting ~PSP file" in the module loader instead of stopping at
"CPU_Init didn't recognize file".

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-21 10:01:07 +02:00
Henrik RydgårdandClaude Opus 5 a3a67156da PSAR: fix the last entry of an archive, and paths that aren't on flash
Two small things I'd noticed as counters in a survey and written off as quirks
of the oldest firmwares. Both were bugs here.

The last entry of every 1.x and 2.x archive was dropped. Decoding a record hands
the decrypter 16 bytes of slack past the block, which was read out of the
archive - and the final record ends flush with the end, so there was nothing
there to read and the entry was refused. It copies what's there and zero-fills
only the remainder now. Worth noting the slack isn't decoration: filling it with
zeros unconditionally, which is what I tried first, breaks every archive that
does have those bytes, so the decrypter reads and uses them.

And 2.x names one entry "ipl:/psp_nandipl.bin", where the check for "is this
already a real path" only knew flash0: and flash1:. The file came out fine, but
it was counted unresolved, and - the part that matters - an entry that isn't a
real path is skipped whenever a prefix filter is set. Rather than collect device
names, the test is now for a "<dev>:/" shape, which 3.x's grouped short names
("com:00123") don't have.

Seven sources, firmware 1.50 through 6.61, now unpack with nothing unresolved
and nothing failed: 1.50 and 1.52 gain their missing last file, 2.81 loses its
unresolved count, and 3.95, 6.00, 6.20 and 6.61 are unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-21 10:00:57 +02:00
Henrik RydgårdandClaude Opus 5 da9e0f0898 PSAR: add an entry point for the disc that's already mounted
The font extraction this is for happens while a game is running, when disc0: is
already mounted - so opening the image a second time to read one file out of it
is the wrong shape, and doesn't work at all for the cases that aren't an image,
like a folder-based disc. UnpackUpdaterFromMountedDisc() goes through
pspFileSystem instead, with MountedDiscHasUpdater() to ask cheaply first and
ReadMountedDiscUpdaterVersion() for the version out of the PARAM.SFO.

Testing it turned up that the prefix filter didn't work on 3.x archives at all:
their file lists write paths as "flash0/font/x.pgf" where 6.x writes
"flash0:/font/x.pgf", so a filter of "flash0:/font/" matched nothing and the
unpack quietly produced no files. Both forms are normalized to the 6.x one now,
so a caller only has to know one. The unfiltered output was already identical
either way, which is why the earlier disc tests looked fine.

Verified by booting Crisis Core and pulling flash0:/font out of its 3.95 updater
through the mounted disc: 21 files, nothing else, no failures. Booting homebrew
with no disc0: at all reports no updater rather than failing.

pspautotests 314/314 with --graphics=software, UnitTest 55/55, and the three
file-based sources (6.61 PBP, 3.95 and 6.20 discs) still unpack unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-21 10:00:55 +02:00
Henrik RydgårdandClaude Opus 5 5fa615dc89 PSAR: read the updaters that come on game discs, straight from the ISO
Most UMDs carry a firmware updater in PSP_GAME/SYSDIR/UPDATE, so the fonts can
come from a game the user already has instead of a separate download. Its
DATA.BIN turns out to be exactly the same archive as a downloaded updater's
DATA.PSAR, just without the PBP around it.

UnpackUpdater() replaces UnpackUpdaterPBP() and takes any of the three shapes: a
downloaded EBOOT.PBP, a bare DATA.BIN/PSAR, or a disc image, which it opens with
the block device and ISO filesystem we already have and looks in SYSDIR/UPDATE.
ReadUpdaterVersion() answers the version from the PARAM.SFO next to the archive
without decrypting anything, which is cheap enough to check every disc with.

Testing across eras turned up three things the 6.61 updater alone never showed:

Old archives name entries differently. 3.x groups them by model - "com:00123",
"01g:00005" - with "<group>:00000" as that group's file list, keyed on just the
number, separated by '|' rather than ',' and with paths written "flash0/font/x"
rather than "flash0:/font/x". 1.x skips the indirection and stores real paths.
Both are handled now.

Which numbers are file lists isn't fixed either. 6.61 uses 1-11, but 6.00 has
real files at 00010-00012, which were being taken for corrupt lists and dropped.
A list always decrypts, since the PRX layer under it validates a hash, so a
failure there now just means "this is a file" - which recovered 3 files each on
6.00 and 6.20.

And the walk ran one record past the end. The archive header says how long the
records really are, and both archives have a few bytes of padding after that.

Read from the discs of Coded Arms (1.50), Ace Combat X (2.81), Crisis Core
(3.95), Assassin's Creed Bloodlines (6.00) and BlazBlue (6.20), plus the
downloaded 6.61. Every one gives up its fonts - 17 of them on 1.50, 19 on 2.81,
21 from 3.95 on. All but two are clean: 1.50 has one .rco whose block won't
decrypt, and 2.81 has one name no list claims.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-21 10:00:27 +02:00
Henrik RydgårdandClaude Opus 5 a58f54ba53 PSAR: let the caller pick which PSP model to unpack for
An updater carries one file list per hardware revision, and which one you
resolve names against decides both what a file is called and whether it's part
of that model's firmware at all. That was hardcoded to "first list that names
it", which is right for extracting everything but wrong for reproducing what a
particular console would have installed.

PSARUnpackOptions::model takes a PSPModelGeneration now, and the lists are kept
per model rather than merged. Any (the default) keeps the old behaviour;
anything else uses only that model's list and skips what it doesn't name.
--unpack-updater-model on headless takes "01g".."12g" or "any".

On the 6.61 updater: any gives 411 files, 03g gives 330 with 81 belonging to
other models, 01g gives 313 with 98. The difference is what it should be - 03g
has the _03g.prx variants and arib.pgf, 01g has neither.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-20 22:50:01 +02:00
Henrik RydgårdandClaude Opus 5 d8481eab79 PSAR: resolve the real filenames out of the archive's own name tables
Every entry in an updater is named with a five-digit token. The paths live in
entries 00001-00012 of the same archive, one per PSP model, each a list of
"shortname,realpath" lines - so with those decrypted the walk can name
everything, and the prefix filter this was written for becomes usable.

The tables looked like they needed an unknown cipher, but the tables in the
reference implementations are DES's: a 56-entry PC-1, a 48-entry PC-2, 8x64
4-bit S-boxes and a 32-entry P, with the constants of a textbook IP/FP in the
bit-shuffling. It's DES-CBC decrypt with the key assembled from two words and
the IV alongside it, both per firmware series, and then an ordinary PRX blob
underneath. So this is a plain DES, one bit per byte, since the tables are a few
tens of KB in total and readable permutations matter more than speed here.

Confirmed by decrypting a table and finding a known PRX tag at 0xD0, which a
wrong key would not have produced.

On the 6.61 updater all 411 files now come out under their real paths -
flash0/font (21 files, ltn0-15, jpn0, kr0, arib, gb3s1518, imagefont),
flash0/kd (195), flash0/vsh/module (100), flash0/vsh/resource (67), and the
rest - with nothing unresolved. Checked the prefix filter separately with
"flash0:/font/": 21 files written and nothing else.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-20 22:44:16 +02:00
Henrik RydgårdandClaude Opus 5 322b5122d8 Unpack the firmware out of an official updater EBOOT.PBP
An updater's DATA.PSAR is a flat sequence of records - each is 0x150 bytes of
PRX-style encryption header, a 0x110 byte entry describing one file, and then
its compressed contents. So to get the files, you don't actually have to run it
and let it self-unpack - we can just do it.

Two steps per record. First "demangle": the 0x130 bytes at +0x20 are AES-CBC
encrypted on top of everything else and hide the PRX tag at +0xD0, so a KIRK
CMD7 pass with keyseed 0x55 comes first. Then the record is an ordinary PRX blob
for the decrypter we already have, once it knows the tag - 0x0E000000, which is
new here. Its key needs the kirk7 scramble applied, unlike every other key in
that table, which are stored already scrambled; hence the flag on TAG_INFO.

UnpackPSAR() takes a prefix filter, since the planned main use for this is pulling
flash0:/font out of an updater the user supplies (or from an ISO) rather than
extracting whole firmwares, although that can also be interesting for running
the VSH.

Tested on a 6.61 updater: 436 entries, all 418 files decrypt and decompress,
nothing fails. The contents are what they should be - 295 ~PSP modules, 61 PRF
files, 18 PGF fonts, and the encrypted XMB indices.

Two things it doesn't do yet. Every entry in that archive is named with a
five-digit token rather than a path; the real names live in tables 00001-00012
inside the archive itself, under their own separate encryption, so files come
out under the short name for now and the prefix filter can't match them.
And only the zlib compression format is currently supported.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-20 22:41:58 +02:00
Henrik Rydgård 0ed6b97921 First step towards drawing a cleaner line between what should be in the libretro core and not. 2026-08-20 08:32:11 +02:00
Henrik Rydgård f0a6775792 Merge pull request #22122 from hrydgard/elf-reader-relax
ELF: resize the segment address table instead of rejecting ELFs with many program headers
2026-08-20 02:48:59 +02:00
Henrik Rydgård 2ba816af44 Merge pull request #22121 from hrydgard/wsdbg-improvements
wsdbg improvements
2026-08-20 02:48:43 +02:00
Henrik Rydgård e6dbc9364d Libretro buildfix 2026-08-20 02:13:27 +02:00
Henrik Rydgård 11055aec3c ELF: resize the segment address table instead of rejecting ELFs with many program headers
Fixes cases where EBOOTs have more than 32 program headers. That limit came from
segmentVAddr being a 32-entry array indexed by program header number

Changes the table to a vector, and holds SEGMENT_NOT_LOADED for
headers that aren't PT_LOAD, so a relocation naming one is rejected and logged
rather than quietly relocating against zero. GetSegmentVaddr() still answers 0
for those, as it did when the table was a zero-initialized array.
2026-08-20 02:05:18 +02:00
Henrik RydgårdandClaude Opus 5 7084d6c800 wsdbg: add --launch and --quiet, and stop teaching a redundant wait
--launch starts PPSSPP itself, learns the debugger port from its output,
connects once the socket accepts, and kills it on exit. That replaces
some wrapper scripts, which had to background the emulator,
poll a log file for "Listening on port N", sleep a guessed interval before
connecting, and taskkill afterwards. The polling was also racy - a fixed port
plus a leftover process from an earlier run is a good way to drive the wrong
emulator - and nothing cleaned up, so --timeout's wall-clock budget left
processes alive for hours.

To make that dependable, the port is now also written straight to stderr from
Core/WebServer.cpp, outside the log system entirely. A tool that launches
PPSSPP has to learn the port before it can connect to anything, so that line
must not be losable to a log level or a disabled channel. wsdbg watches both
child streams for it, since which one it lands on depends on how a given build
routes logging.

--quiet sends the broadcast.config.set that every script was hand-writing,
disabling the logger and input broadcasts. The log one is expensive - each line
gets encoded as JSON and pushed down the socket.

The rest is one bug, in the docs rather than the code, which cost a lot of time:
the scripting example paired cpu.runUntilTime with ":wait cpu.stepping". --sync
already waits for the cpu.stepping that follows a resume-family command, so the
explicit :wait waits for a second one that never comes and burns the whole
--sync-timeout. With --sync-timeout 400 that turns a 3-second run into a
7-minute one that looks exactly like a slow boot, because the emulator really
has stopped where it was asked to. Example fixed, and both that and muting the
'stepping' category (same failure, different cause) are called out - wsdbg now
warns when a raw broadcast.config.set disables 'stepping'.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-20 01:42:36 +02:00
Henrik RydgårdandClaude Opus 5 20558bc92b Fix wsdbg losing the last reply
Two things.

wsdbg no longer loses the reply to the last request in a script. A line like
"gpu.displaylist.disasm ..." followed by ":quit" exited before the answer
arrived, and the result was indistinguishable from the request silently doing
nothing - which cost real time in this investigation before it was recognised.
Tickets that were sent and not awaited are tracked now, and :quit (or stdin EOF)
drains them, warning if any are still outstanding after ten seconds. Verified
with exactly the script that used to lose it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-20 01:42:36 +02:00
Henrik Rydgård b663981207 Cleanup 2026-08-20 01:42:36 +02:00
Henrik Rydgård 2abc849c9c Merge pull request #22119 from hrydgard/assorted-fixes
Fix issue with booting extracted ISOs from command line
2026-08-20 01:34:45 +02:00
Henrik Rydgård 31decccb9a Style and comment settings. Surface a setting in the ImDebugger. 2026-08-20 01:05:19 +02:00
Henrik Rydgård 1910be0b47 Fix booting from extracted directories specified on the command line, reported by Kotcrab 2026-08-20 00:54:26 +02:00
Henrik Rydgård acf7effaeb Improve some comments 2026-08-20 00:42:08 +02:00
Henrik RydgårdandClaude Opus 5 15bb7b2866 PSPLoaders: handle the failure cases around mountRoot and boot detection
NormalizePath's Windows branch ignored GetFullPathName failing. A zero return
matched neither of its two branches, so the 512-character buffer was handed back
with whatever was in it.

The content-URI branch then computed its relative path by substr()ing the
decoded FilePath() of the two URIs - but the containment check above it compared
the URIs themselves, which doesn't imply the same prefix relationship for the
decoded paths. When it doesn't hold, substr() throws out_of_range, uncaught.
Checked explicitly now.

More minor fixes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-20 00:42:01 +02:00
Henrik RydgårdandClaude Opus 5 e4613a3487 WaveFile: don't assert-crash when the wav file couldn't be opened
__StartLogAudio ignored WaveFileWriter::Start()'s return value and set m_logAudio
either way, so a wav file that couldn't be opened - read-only or full audio
directory, most likely - left every subsequent mixed block calling
AddStereoSamples on a closed file. That opens with _assert_msg_(file, ...).

Also other minor fixes.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-20 00:40:41 +02:00
Henrik Rydgård 5c6fc2e69f Minor CwCheat correctness fixes 2026-08-20 00:29:00 +02:00
Henrik Rydgård e52fd6d0ef Merge pull request #22118 from hrydgard/claude-elf-fixes
Claude code review: ElfReader
2026-08-19 21:22:56 +02:00
Henrik RydgårdandClaude Opus 5 4c72c13a0d Module loader: clean up properly on the two failure paths that didn't
__KernelLoadELFFromPtr creates its PSPModule and inserts it into loadedModules
before it knows whether the file is loadable, so every failure exit has to
delete the decrypt buffer, Cleanup() the module and Destroy() it. Five of the
seven did. The "unreasonable decrypted size" exit and the decompression-failure
exit just returned - leaking the buffer, and leaving a live kernel object with
its UID stuck in loadedModules for the rest of the session.

While tracing that: the fake-module path frees newptr and then runs for another
sixty lines with ptr still pointing into it. Nothing reads it today - the exits
below use head, which points into the original input rather than the copy - so
there's no use-after-free and no double free, but that's a property of the
current code rather than anything enforced. Both pointers are nulled after the
delete so a future mistake there crashes instead of reading freed heap.

And the function read the magic, and in the ~SCE branch a second word after it,
before anything established the input was that big. The non-PBP caller
guarantees it, but the PBP path computes elfSize from two offsets in the file
and passes whatever comes out, including zero. Checked at the top, before the
module object exists, so that exit needs no cleanup of its own.

pspautotests 314/314 with --graphics=software, and an EBOOT.PBP still boots.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-19 19:19:06 +02:00
Henrik RydgårdandClaude Opus 5 5020647bab PBPReader: detect ELFs by their actual magic, and sanity-check subfile sizes
The "is this an ELF rather than a PBP" test compared against "\nFLE", which is
neither ELF's magic (\x7fELF) nor anything else - most likely a \x7f escape that
swallowed the E when it was written in 2013. Since no real file matches it,
every file that wasn't a PBP was reported as an ELF and the error branch was
unreachable. Compares against the real magic now, so something that's neither is
reported as neither. That error also printed the 4-byte magic with %s, which
isn't NUL-terminated - it's four hex bytes instead.

GetSubFileSize subtracted offsets that come straight out of the file without
checking they're ordered or even inside it, so a corrupt PBP produced a size
from an unsigned underflow - nearly 4GB, which the callers then had to catch by
size limit. It returns 0 for anything that doesn't make sense.

Also &(*out)[0] on a zero-length subfile, which is UB on an empty vector.

Plus one in ParamSFO: GetDataOffset mixed int and u32 for the data offset, so
its bounds check ran in whichever type the promotion landed on. It's size_t
throughout now, matching how ReadSFO does the same arithmetic.

Booted an EBOOT.PBP to check the PBP path end to end - loads, and generates the
same fake disc ID as before. pspautotests 314/314, UnitTest 55/55.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-19 19:19:06 +02:00
Henrik RydgårdandClaude Opus 5 4b282383b2 ELF: harden the accessors and the Rel2 relocation decoder
Nothing here is known to misbehave on a real file - it's the input validation
around the fixes in the preceding commits.

ElfReader's constructor read e_phoff and e_shoff out of the header to find the
segment and section tables, before anything had established there was a header
there; LoadInto's size check only runs later. It leaves header null in that case
now, and the accessors that use it cope.

GetSegmentPtr didn't range-check the segment index at all, and both it and
GetSectionDataPtr accepted an offset exactly at the end of the file, which
addresses no bytes. GetSectionAddr and GetSectionSize took an index on trust.

LoadRelocations2 got most of this commit. Its segment end came from p_filesz
without checking the segment fits in the file, so the whole decode could run off
the end of the buffer. Within it, the flag and type tables are indexed by
bitfields out of each command word and were never checked against the table
sizes (which themselves come from the file); the loop only guaranteed one byte
was left before reading a two-byte command, and the branches that consume a
further two or four bytes checked nothing at all; and the offset segment number
- unlike the address segment number a few lines up - was used to index
segmentVAddr unchecked, though it's wide enough to exceed it. The command read
is byte-wise now too: how far buf has advanced depends on those file-supplied
table sizes, so it isn't necessarily even.

LoadSymbols only checked that a symbol name started inside the file, not that it
was terminated there.

Also dropped the atomic counter and the ParallelLoop.h include left over from
when LoadRelocations ran in parallel.

pspautotests 314/314 with --graphics=software.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-19 19:19:06 +02:00
Henrik RydgårdandClaude Opus 5 90a178aeb8 ParamSFO: make GenerateFakeID independent of char signedness
The fake disc ID homebrew gets when it has no PARAM.SFO is built from the sum of
the bytes of its folder name, summed through a plain char - which is signed on
x86 and unsigned on ARM. So the same homebrew folder produced one ID on Windows
and a different one on Android, quietly splitting its savestates and per-game
config between platforms. Sum through unsigned char, which is what the ARM
builds (Android, iOS, Apple Silicon) already did.

Uppercasing is now explicit and ASCII-only rather than toupper(). Passing a
negative char to toupper() is undefined and trips MSVC's debug CRT assert, so a
folder with a non-ASCII name could stop a debug build dead, and what it did with
bytes above 0x7F otherwise depended on the locale.

ASCII folder names - very nearly all of them - produce exactly the same ID as
before. Non-ASCII ones change on the signed-char platforms, to what the
unsigned-char ones were already generating.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-19 19:19:06 +02:00
Henrik RydgårdandClaude Opus 5 ec364e2cd1 ELF: three fixes in the HI16/LO16 relocation path
addrToHiLo's verification computed (hi<<16) + lo with hi a u16, which promotes
to int - so for any kernel module, loading at 0x88000000, the shift overflowed a
signed int. Undefined behaviour in the one place whose whole job is to check
that a relocation came out right.

A HI16 that found no matching LO16 logged an error and then wrote its zero-
initialized hi into the instruction anyway, blanking the immediate of a lui it
had just admitted it couldn't resolve. It leaves the instruction alone now: we
don't know the right value, and a zeroed lui produces a wrong address far from
here rather than a failure anyone can trace back.

And the candidate LO16's address was computed with the HI16's segment base
rather than its own, which is exactly the mismatch the warning a few lines below
exists to report - so when that warning fired, the IsValidAddress check guarding
the pairing had been applied to an address from the wrong segment.

pspautotests 314/314 with --graphics=software.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-19 19:19:06 +02:00
Henrik RydgårdandClaude Opus 5 12fa56d842 PrxDecrypter: require a whole header before decrypting
Every decrypt type reads the tag at 0xD0, the compressed size at 0xB0 and key
data as far as 0x150, and writes a KIRK header into outbuf at a fixed offset
derived from sizeof(PSP_Header) - all without checking that either buffer is
that big. A PRX declaring a tiny psp_size therefore read past the end of its
input, wrote a 0xE0-byte header past the end of an equally tiny output buffer,
and handed KIRK "size - offset" as an unsigned underflow. The header write sits
behind the SHA-1 check, but the tag keys are compiled in and every hashed input
comes from the file, so that's arithmetic rather than luck. One size check at
the top of pspDecryptPRX covers all five types.

The module loader needed two things to go with it. Its "maybe it just isn't
encrypted" fallback checked for ELF magic at 0x150 of the *output* buffer, which
on the paths where decryption bails early has nothing written to it yet - so it
read uninitialized heap to decide, and then, if psp_size was under 0x150,
memcpy'd a negative length. It reads the input buffer now, which is what it goes
on to copy from anyway, and only when psp_size is big enough to hold what's
being tested.

Second, the returned size is just comp_size out of the file header, checked
against the allocated buffer by a _dbg_assert_ that isn't there in release. That
check is a real one now, folded into the existing sanity test next to it.

pspautotests cpu 11/11.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-19 19:19:06 +02:00
Henrik RydgårdandClaude Opus 5 1a4a1db1b6 PBPReader: don't write the header through a const-stripping cast
header_ was declared const and value-initialized, and then the constructor read
the file into it via (u8 *)&header_. The C-style cast makes that compile, but
modifying a const object is undefined - the compiler is entitled to keep
assuming header_ still holds the zeroes it was initialized with, and fold reads
of it accordingly. It happens to work today; there's no reason to keep relying
on that.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
2026-08-19 19:19:06 +02:00