Commit Graph
100 Commits
Author SHA1 Message Date
Henrik RydgårdandClaude Opus 5 e4d940604e Name both versions in the firmware overwrite warning
"Firmware 6.20 is installed. It will be erased and replaced with 6.60." is the
thing worth double-checking before wiping a firmware - installing off whatever
disc is to hand makes going backwards easy to do by accident.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 15:52:53 -06:00
Henrik Rydgård 86fcfdc60a Add some new function names that were cracked from NIDs by zecoxao
He used Claude, never thought of this use case, but it's logical - it
can go reverse engineer the functions to come up with likely names,
making it easier to match the SHA1 hash. (NIDs are truncated SHA1 hashes
of names, except in firmwares later than around 3.50).
2026-09-10 15:52:53 -06:00
Henrik Rydgård 1d7c427eba Merge pull request #22281 from hrydgard/firmware-vsh-boot
Boot to XMB on all (probably) firmware versions
2026-09-10 15:46:14 -06:00
Henrik RydgårdandClaude Opus 5 e82980635b headless: don't fake a framebuffer descriptor for the end-of-run screenshot
SendDebugScreenshot ignores the descriptor entirely and reads the display
framebuffer from the GPU, so filling one in was theatre - and it computed a
pointer from the display address, which is zero whenever the shell has the
display switched off.

Also reset g_screenshotSaved per test, so a test that emits its own screenshot
doesn't stop the next one getting the end-of-run capture, and clear the sceReg
open count on shutdown to match init.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 15:30:03 -06:00
Henrik Rydgård 35d69dd4a1 De-claude a bit. It's so verbose. 2026-09-10 15:25:47 -06:00
Henrik RydgårdandClaude Opus 5 1bc43d2109 docs: all 40 firmware versions reach an interactive XMB
Record the fifth cause (the rejected impose parameter), correct the counts now
that 3.80/3.90 and 2.6x-2.8x are covered, and note that 1.50 through 2.50 never
touch the encrypted XMB index at all.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 14:40:24 -06:00
Henrik RydgårdandClaude Opus 5 fda30ad99c HLE: add the 3.80/3.90 NIDs for the three VSH calls
Same three functions, one more NID each. Those two shells started no plugins at
all and sat on a black screen; both reach an interactive XMB now.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 13:49:08 -06:00
Henrik RydgårdandClaude Opus 5 af5bc1db50 PrxDecrypter: add the 2.6x and 2.8x sceResmgr keys
2.60 and 2.71 tag their flash0:/vsh/etc/index.dat 0x495BE403 rather than with
the 0x0B2Bxxx0 the rest of this family uses; 2.80, 2.81 and 2.82 use 0x0B2B05F0.
All five now reach an interactive XMB.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 13:49:08 -06:00
Henrik RydgårdandClaude Opus 5 f3f5432cb0 sceImpose: accept parameter 0x40000000
Every impose.prx from 1.50 to 5.55 dispatches on it and returns a field of the
impose context - the same field that decides where the backlight brightness
answer comes from. We rejected it as not a parameter at all, and 3.11's shell
read the error back, blanked the display with sceDisplaySetFrameBuf(0, 0, 0)
and never turned it on again.

2.00, 3.03 and 3.11 now reach an interactive XMB.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 13:08:04 -06:00
Henrik RydgårdandClaude Opus 5 a3ce735123 AGENTS.md: spell out that commit messages carry no session marker
The rule was already here in one line and I still added a Claude-Session
trailer to ten commits, because the session's own attribution instructions say
to. Say plainly that this rule wins, and to check git log afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:52:19 -06:00
Henrik RydgårdandClaude Opus 5 ee431b812b docs: re-measure which firmware versions reach an interactive XMB
The old claim of "all of them" measured that the shell renders, which several
versions do while showing only the wallpaper or a full-screen error. Record what
each version actually does, the four causes that were in the way, and how to
find the next one of each kind - including that --timeout is wall-clock, so a
slow-drawing shell can look stuck when it isn't.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:49:06 -06:00
Henrik RydgårdandClaude Opus 5 48a9498729 sceKernelModule: resolve the VSH's kernel drivers on old firmwares too
The per-model naming only starts around 3.50. Older dumps have plain
memlmd.prx and loadexec.prx, name the wlan firmware after the chip revision
(wlanfirm_magpie.prx became wlanfirm_01g.prx), and don't have lowio.prx at all
until about 3.52 - so every one of those was reported as a failed load on
2.00 through 3.30. Try the emulated model, the model in the path, then those
older spellings, and say plainly when the firmware simply doesn't ship one.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:46:18 -06:00
Henrik RydgårdandClaude Opus 5 6b82202a2d PrxDecrypter: add the 3.0x, 3.1x and 3.5x sceResmgr keys too
Those generations predate the per-model split and have a single
flash0:/vsh/etc/index.dat. 3.30 through 3.52 now reach an interactive XMB;
3.30 to 3.51 share 3.52's key.

3.03's mesg_led.prx is older than keys330_1, so its table was confirmed against
keys300_1 and keys280_1 instead - both match byte for byte.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:46:18 -06:00
Henrik RydgårdandClaude Opus 5 f32c015659 PrxDecrypter: add the sceResmgr keys for the 5.0x, 6.0x and 6.3x XMB indexes
flash0:/vsh/etc/index_XXg.dat is the index of what the XMB shows, and each
firmware generation tags it with a key per PSP model. We only had the 6.6x
triple, so 5.03 through 6.39 decrypted nothing, drew no icons, and gave up with
the red error screen.

The keys come out of each firmware's own mesg_led_XXg.prx, whose tag table is
24-byte entries of tag plus 16-byte key. Extracting the 6.6x triple that way
reproduces the three keys already in this file byte for byte, which is what
establishes the layout.

5.03, 5.50, 5.55, 6.00, 6.20, 6.31 and 6.39 now reach an interactive XMB.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:44:47 -06:00
Henrik RydgårdandClaude Opus 5 0054d30b1d HLE: cover the rest of the NIDs the VSH uses across firmware versions
sceKernelLoadModuleVSH, sceKernelGetModel and sceImposeSetStatus each appear
under a different NID in 3.95/4.05, 6.00/6.20 and 6.31/6.39. Without the load
one those shells started no plugins at all and sat on a black screen.

Identified the same way as the 5.xx set: for each firmware, the modulemgr export
with sceKernelLoadModuleVSH's callee set, and the vshbridge export whose body is
the user-level check 6.61 wraps sceKernelGetModel in - which is also the only
SysMemForKernel import those shells actually call.

3.95 and 4.05 now reach an interactive XMB.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:44:47 -06:00
Henrik RydgårdandClaude Opus 5 3bdc4380d8 sceReg: report a registry category_version every VSH accepts
The dumped 0x66 comes from a 6.6x PSP, and 5.50's shell rejects any schema
version above 0x58 as a corrupt registry - it drew its whole XMB and then
replaced it with the "settings are corrupt, press O to repair" dialog.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:44:47 -06:00
Henrik RydgårdandClaude Opus 5 41509a9295 sceReg: don't let a nested registry close drop another opener's categories
We hand every sceRegOpenRegistry the same handle 0, so closing one used to wipe
every open category - including ones a still-open registry handle owns. On
hardware each open gets its own object and they don't interfere.

The VSH's alarm scan does exactly this: it holds /CONFIG/ALARM open, then opens
and closes the registry once per alarm slot, and found its own category gone by
the end. Count the opens and only clear on the last close.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 12:44:47 -06:00
Henrik RydgårdandClaude Opus 5 56b6c3af90 PrxDecrypter: add the 5.xx sceResmgr key
flash0:/vsh/etc/index_XXg.dat is the index of what the XMB shows, and 5.50 tags
it 0x0B2B11F0, which we had no key for - so the shell decrypted nothing, drew
no icons, and gave up with the red error screen.

The key is read out of that firmware's own mesg_led_02g.prx, whose tag table is
24-byte entries of tag plus 16-byte key. The two neighbouring entries hold
keys330_1 and keys505_a byte for byte, which is how the layout was confirmed.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 10:48:32 -06:00
Henrik RydgårdandClaude Opus 5 1a3f14acd0 HLE: add the 5.xx NIDs the VSH of that era imports
sceKernelLoadModuleVSH, sceKernelGetModel, and four of sceImpose_driver's calls
already have implementations; 5.xx just asks for them under different NIDs, so
the 5.50 shell got nothing back. Each one was identified by disassembling that
firmware's own module and comparing the body against 6.61's, where the same
function is exported under a name - the pairs are instruction-for-instruction
identical apart from context-struct offsets.

GetModel was the one that mattered most: unresolved, vshbridge handed the shell
a garbage model number, and it went looking for PSP-3000 resources on a dump
that has none.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 10:48:32 -06:00
Henrik RydgårdandClaude Opus 5 765d5bfbcf headless: make --nand and --screenshot-save work
--nand had a field and an ApplyToConfig branch but nothing ever parsed it, so it
was silently ignored.

--screenshot-save only fired when a GE replay finished or a test used the
EMIT_SCREENSHOT devctl, and even then only under --compare. Anything else - a
game, or --vsh - ran to the timeout and wrote nothing. Capture the display at
the end of the run when nothing else did, which is what makes it usable for
looking at what a booting system actually has on screen.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-10 10:48:32 -06:00
Henrik Rydgård a52b11aa2e Merge pull request #22275 from hrydgard/scemp4-review-fixes
sceMp4: Read the effective flags (not the config), don't reload the modules by accident
2026-09-09 19:32:10 -06:00
Henrik Rydgård 9d2ebd5a3a Merge pull request #22274 from hrydgard/headless-debugger-run
headless: say when we're waiting at the entry point, and add --debugger-run
2026-09-09 17:13:04 -06:00
Henrik Rydgård 9356a3acbc Merge pull request #22271 from hrydgard/firmware-screen
Add a Tools/PSP Firmware screen, improve compat with older firmwares
2026-09-09 16:50:13 -06:00
Henrik RydgårdandClaude Opus 5 29c5d1baf1 Offer to install the disc's firmware updater from the game context menu
The game screen already reports which updater a disc carries, but there was no
way to act on it - the only route to installing one was picking an updater PBP
out of the browser. The unpacker already handles being pointed at a disc, so
this just wires the menu entry to it.

Shown only when the disc actually has one, and not while a game is running:
installing wipes the NAND that game has mounted.

InstallUpdateScreen takes the archive size now, because for a disc the size of
the file it was handed is the game's and says nothing about the firmware.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 16:25:13 -06:00
Henrik RydgårdandClaude Opus 5 63eb3d5b21 Boot the VSH on every firmware, 1.50 through 6.61
Backwards from 5.01, one release at a time, against all 39 versions that ship
on a disc plus the download-only 6.61. Nothing here is an offset - it's almost
entirely Sony renumbering the kernel *_driver NIDs, which sends an import we
mean to HLE into the real firmware module instead.

Four more NIDs each for sceRtc_driver/sceRtcSetAlarmTick and
sceHprm_driver/sceHprmReadLatch, covering 1.50 up. The rtc one is what parked
every thread on a SceSysconSync semaphore; the hprm one runs once a frame, so
unresolved it was most of the boot log. Also sceImposeGetParam/sceImposeChanges
(1.50 - 2.xx) and sceKernelLoadModuleVSH (1.x, which is how the shell loads its
own plugins - unresolved it got module id 0 and StartModule failed).

sceRtcIsAlarmed had to be implemented too; it returns 0, as in JPCSP. As a null
entry it returned LIBRARY_NOT_YET_LINKED, and the 3.0x-3.5x VSH read that as
"ask the hardware instead" and went back to blocking on syscon.

Two structural findings:

- Up to 4.05, scePaf's heap allocator is a separate heaparea1.prx that paf
  imports as scePafHeaparea. Load it when it's there. Its pool pointer needs
  the same pre-fill paf's does, at gp - 0x7FCC rather than gp - 0x7E88.
- 1.50's vshmain.prx declares no module attributes at all - PSP_MODULE_VSH_MODE
  only appears from 1.52 - so the whole VSH bootstrap was being skipped. Accept
  the module name too.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 14:43:39 -06:00
Henrik RydgårdandClaude Opus 5 480c5e44e2 Boot the VSH on every firmware from 5.01 up
The blocker below 6.60 wasn't offsets, it was that Sony renumbered the kernel
*_driver NIDs between versions. A function we HLE under its 6.6x NID is a
stranger on an older build, so the import lands in the real firmware module
instead - and that's where it goes wrong:

- sceRtc_driver sceRtcSetAlarmTick. Without the HLE the VSH's alarm call ran
  the real rtc.prx, which called on into syscon.prx and blocked forever on a
  SceSysconSync semaphore. That was the whole "stalls with every thread parked"
  symptom; the tell was a fourth SceSysconSync waiter a healthy boot lacks.
- sceHprm_driver sceHprmReadLatch, called once a frame - so before this an
  older firmware's 12-second boot logged ~20000 lines of one unresolved import.

Three extra NIDs each, found by disassembling the module from both firmwares
and matching on the address of the user-mode export whose NID never changed
(sceRtc/0x7D1FBED3, sceHprm/0x40D2F9F0).

5.55 additionally needed two PRX decryption keys we didn't have (0x4C941AF0
and 0x4C941BF0) - without them none of flash0:/kd decrypted and the shell came
up with no drivers behind it at all.

Checked one release at a time against every version that ships on a disc, plus
6.61. 4.05 and below still die on a null write inside vsh_module.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 13:40:37 -06:00
Henrik RydgårdandClaude Opus 5 caf3ed4335 Firmware screen: don't list details a fonts-only install doesn't have
With no kernel modules there's no firmware to speak of - it's the fonts we
pulled off a game's disc - and rows reading "Kernel modules: 0 / XMB: No"
say nothing. Keep the font count and the size.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 12:56:43 -06:00
Henrik RydgårdandClaude Opus 5 b21e40a53f Boot the VSH on firmware 6.60 too, and stop patching builds blind
6.60 ships byte-identical paf.prx and vshmain.prx to 6.61 - all 6338 + 669
functions disassemble the same - and boots to an interactive XMB, so let
FirmwareVersionSupportsVSH accept it. That matters because no UMD carries 6.61
(it was download-only), so 6.60 is the best a disc-installed firmware can be.

The two module patches were hardcoded offsets from the module base applied to
any module of the right name, which is quietly wrong on any other build:

- The scePaf heap arena slot moves with every build (0x18CCD8 on 6.00 through
  0x18D728 on 6.60/6.61) but sits at gp - 0x7E88 in all of them, so find it
  that way. On its own this turns an immediate SIGSEGV inside scePaf into a
  clean stall on 6.00 through 6.39 - they still don't reach an XMB, they get
  stuck in sceVshBridge_Driver instead.
- The vsh_module alarm-category offset has no such anchor, so check the word
  there is the one the patch was derived from. On 6.20 and 6.00 it's ASCII
  string data - the unconditional write was corrupting a string table.

Also resolve the per-model kernel drivers (memlmd, loadexec, wlanfirm) to the
model being emulated. They were asked for as _01g, which a firmware unpacked
for a single model doesn't have - and our own updater unpack defaults to 02g.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 12:56:34 -06:00
Henrik RydgårdandClaude Opus 5 bb96802f72 Fix a heap overflow decrypting a PRX whose size we had to guess
KIRK CMD1 writes header + data_offset + align16(data_size) bytes into outbuf,
and all three come out of the header the decrypter just decrypted, not from the
caller. The SHA1 check doesn't bound them - it only covers the header, so it
passes just as happily for a block that's been cut short.

The PSAR walker has to guess how long an updater's second block is (nothing
records it, so it tries the sizes real updaters use), and a wrong guess sent
KIRK off the end of the buffer: unpacking a firmware crashed roughly half the
time, on every version and disc I tried, depending on the heap layout.

Bound the write against the size the caller gave us, in all six decrypt types.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 12:56:17 -06:00
Henrik Rydgård 304eef9b16 Merge pull request #22270 from hrydgard/reverb-tuning
Reverb effect tuning
2026-09-09 12:26:43 -06:00
Henrik RydgårdandClaude Opus 5 57e96b2d81 Add a PSP Firmware screen under Settings > Tools
Shows what's actually in PSP/NAND - nothing, a fonts-only partial install, or
a full firmware with its version, build date and region - read from
flash0:/vsh/etc/version.txt, which is present both in a PSAR-unpacked install
and a NAND dumped off hardware.

Also offers to install an updater, erase the NAND, and launch the XMB, the
last one gated on FirmwareVersionSupportsVSH() since the module patches that
get vshmain.prx running are tied to 6.61's offsets.

Installing a firmware now erases flash0/flash1/ipl first - two firmwares can't
be merged, a file the new one doesn't have would linger and still get loaded.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 12:07:28 -06:00
Henrik Rydgård 6d945404d1 sceSas: Fix send/return levels for the reverb effects
The two fudge factors in the reverb path cancelled, which is why the
overall level felt roughly right.

- The send is accumulator * 0x20 >> 16, i.e. sample >> 2. We used >> 1,
  driving the reverb 6dB hot.
- The return is (evol * out) >> 11. We used >> 12, i.e. 6dB quiet.

Net level is therefore unchanged, but the reverb now runs at the level
the presets were designed around. That matters because the filter clamps
internally, so a 6dB hot input changes how the feedback path saturates -
worst on the presets with heavy feedback.

Also adds a slider in the imgui.
2026-09-09 11:05:40 -06:00
Henrik Rydgård 5402579a44 sceSas: correct the reverb presets
The reverb presets came from nocash's PS1 table. Six of the nine are
identical on the PSP, but three are not.

Also correct our linear interpolation expression: we were close but
our formula can produce an off by 1 at times.
2026-09-09 10:48:53 -06:00
Henrik Rydgård e5b98b07e8 Merge pull request #22269 from hrydgard/me-re-tools
Headless RE tools: Add some options to disassemble ME images
2026-09-09 09:41:53 -06:00
Henrik Rydgård 280a5df557 headless: split the second stream out of an ME image, and accept a bare one
An ME image is two KL4E streams back to back: the code that runs at
0x88300000, then a blob the image decompresses to ME local RAM at
0x00101000, which is where its data segment lives. Decompressing only the
first left every global unaccounted for.

--re-decrypt now reports how much of the plaintext the first stream used
and writes the remainder to <out>.tail, and accepts an already-plain
KL4E/KL3E file so that tail can be fed straight back in.
2026-09-09 09:00:29 -06:00
Henrik Rydgård 1a1430e8ca headless: add --re-decrypt and --re-raw-base to the RE tool
--re-decrypt runs pspDecryptPRX() over a file and unpacks the KL4E/KL3E
stream behind it. This opens up flash0:/kd/resource/*.img, the images the
Media Engine actually runs: they are ordinary tagged containers (tag
862648D1, which PrxDecrypter already has a key for) with the ~PSP
signature blanked, so the normal module loader never touches them.

--re-raw-base analyzes --re-module as a flat code image at a given
address rather than as a PRX. The decrypted ME images are raw MIPS with
no ELF around them; the address they were linked for is recoverable from
their own jal targets (0x08300000 for meimg.img).

Also makes PrxDecrypter.h self-contained - PSP_Header is built from _le
types, so it needs Common/Swap.h rather than relying on the includer.
2026-09-09 09:00:29 -06:00
Henrik Rydgård d163551823 Merge pull request #22262 from NABN00B/stereo-settings
Show stereo shader options dynamically
2026-09-09 08:53:46 -06:00
Henrik Rydgård 3d59ef6f09 Merge pull request #22268 from hrydgard/revert-mipstracer
Revert exposing MIPSTracer to websocket
2026-09-09 08:46:19 -06:00
Henrik Rydgård f161e7f9f9 Revert "Expose the MIPSTracer over the WebSocket debugger"
This reverts commit ccdaa94f53.
2026-09-09 08:07:19 -06:00
Henrik Rydgård 25812b70fb Merge pull request #22266 from hrydgard/firmware-module-reset
sceUtility: forget injected firmware modules between games
2026-09-08 17:08:09 -06:00
Henrik Rydgård 32a70d0d3d Merge pull request #22265 from hrydgard/vaudio-mp3-sample-counts
sceVaudioChReserve: accept the MP3 frame sizes
2026-09-08 16:14:13 -06:00
Henrik Rydgård 47c17b5589 Merge pull request #22264 from hrydgard/re-module-dump
Module dumper for reverse engineering
2026-09-08 16:07:37 -06:00
Henrik Rydgård c192ddcaa1 Merge pull request #22263 from hrydgard/hardware-verified-hle-fixes
Hardware verified HLE fixes
2026-09-08 15:53:35 -06:00
Henrik RydgårdandClaude Opus 5 ed9078b055 sceVaudioChReserve: accept the MP3 frame sizes
The channel took only 256, 1024 and 2048 samples, so a game that hands it MP3
frames got SCE_KERNEL_ERROR_INVALID_SIZE and no music. Dead or Alive Paradise
does exactly that from its music player: sceVaudioChReserve(1152, 44100, 2),
1152 being the MPEG-1 Layer III frame size.

The format check moves below the sample count check to match: the module
returns 0x80000104 for a bad count before it ever looks at the format, so a
call with both wrong got the wrong error out of us. The two error codes we
already returned are the ones it uses.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 15:52:10 -06:00
Henrik Rydgård cc44ba22d0 Android buildfix 2026-09-08 15:45:11 -06:00
Henrik Rydgård 1e2eaa1690 MacOS: Add shortcut to load the VSH 2026-09-08 15:26:26 -06:00
Henrik Rydgård 41144e3b35 Give the memory partitions the caller's privilege, not the syscall's
PPSSPP decided whether a caller was privileged with hleIsKernelMode(), which reports whether the
syscall being executed is itself a kernel-only export. That's a different question from the one
the hardware answers: on a PSP the privilege belongs to the calling module, and a kernel module
reaches sceKernelCreateTlspl through the ordinary ThreadManForUser NID like anything else. So a
kernel module asking for partition 1, 3 or 4 got ILLEGAL_PERM where a real PSP hands it over,
which the new threads/tls/kernel/partition test shows directly.

BlockAllocatorFromID now also accepts a caller whose thread belongs to a kernel module, via a new
__KernelCurThreadIsKernelMode(). It checks the thread's own attribute first and then the owning
module, because a kernel module's main thread isn't necessarily flagged kernel - the attribute
comes from PSP_MAIN_THREAD_ATTR, which needn't set it. That mirrors how sceKernelCreateThread
already works out allowKernel.

This only ever widens access, and only for threads belonging to kernel modules, so games are
unaffected - they run in user modules and see exactly what they saw before.
2026-09-08 15:18:02 -06:00
Henrik Rydgård a676eecba7 Tlspl partitions: 1-6 in both privilege levels, and document kernel-mode tests
threads/tls/kernel/partition now records the sweep from a kernel module, which settles the range
question the user-mode recording couldn't: privilege changes the permission check, not the range.
Partitions 1, 3 and 4 are ILLEGAL_PERM from user mode and fine from kernel mode, while 7 and up
are ILLEGAL_ARGUMENT either way. So the check goes back to a plain 1-6 for both, and the
kernel-mode carve-out from the last commit - which would have let 8 and 9 through - is gone.

The hardware doc gains a section on kernel-mode tests: what COMMON_KERNEL does, why the stock
crt0 makes a kernel PRX unloadable, which libraries can't be imported, and how much room there
actually is in the kernel partition.
2026-09-08 15:18:02 -06:00
Henrik Rydgård 705ea7eb02 Keep the SHA-1 context in game memory too, and scope the Tlspl partition range to user mode
sceKernelUtilsSha1Block* had the same single global context that MD5 did, so it gets the same
treatment: state, counters and block buffer now live at ctxAddr in the layout hash/sha1ctx
records off hardware. Unlike MD5, SHA-1 does not stream whole blocks through buf, which happens
to be what our sha1_update already does - so no fill-in step is needed there.

The Tlspl partition range from the last commit was too broad a cut. Hardware says only 1-6 exist,
but that recording is from user mode, and BlockAllocatorFromID deliberately maps 8 and 10 to the
user partition for a kernel-mode caller - rejecting them outright would have taken that away.
The tightened range now applies to user mode only and kernel mode keeps what it had.
threads/tls/partition also shows the answer doesn't depend on the compiled SDK version, checked
across 1.00 through 6.06, and that partition 5 is accepted - which no test had covered.
2026-09-08 15:18:02 -06:00
Henrik Rydgård 8a02d1ee0f Keep the MD5 context in game memory, and make MT19937 actually be MT19937
Three fixes, all of them things the new hardware tests turned up.

sceMd5Block* and sceKernelUtilsMd5Block* shared one static md5_context and ignored the context
pointer the caller passed in, with a TODO saying it would do "unless games do several MD5
concurrently". hash/md5ctx shows a real PSP keeps everything in the caller's 96 bytes and happily
runs two digests at once, so do that instead: the state, the counters and the block buffer now
live at ctxAddr in the game's own memory, in the layout the test pins down. Two interleaved
digests come out right, and a context that gets copied mid-digest carries on correctly. As a
side effect the state is now covered by savestates, which a file-static never was.

MersenneTwister masked both halves with 0x80000000 where the low half needs 0x7FFFFFFF, so
sceMt19937UInt and sceKernelUtilsMt19937UInt were returning a sequence that isn't MT19937 at
all - every number differed from hardware from the first draw. hash/mt19937ctx computes the
reference sequence itself and confirms the PSP is plain MT19937; with the mask fixed we match it
for both seeds tested. Init also twists the array immediately, as hardware does, so a context
that has been seeded but not drawn from now holds what a real one would.

sceKernelCreateTlspl accepted partitions up to 9 before falling through to the permission check.
Hardware draws the line at 6 - threads/tls/create records 7, 8, 9 and 10 all returning
ILLEGAL_ARGUMENT - so 8 and 9 were coming back ILLEGAL_PERM. Note this is genuinely different
from sceKernelCreateVpl right above it, which does let 8 and 9 through to ILLEGAL_PERM; the two
had been sharing a check that was only ever right for Vpl.

Risk worth naming: the MT19937 change alters the numbers any game gets from these calls. That's
the point - they were wrong - but a savestate taken mid-sequence will resume with a generator
that behaves differently from the one that made it.
2026-09-08 15:18:01 -06:00
Henrik Rydgård 4d795f5130 Merge pull request #22248 from hrydgard/fat-short-names
sceIo: generate and resolve FAT 8.3 short names
2026-09-08 14:42:18 -06:00
Henrik Rydgård e706d27e14 Merge pull request #22258 from hrydgard/pkg-research
PKG install support
2026-09-08 13:44:42 -06:00
Henrik Rydgård 2845de3a4d Merge pull request #22259 from hrydgard/firmware-modules-load-high
Firmware modules: Load to high memory
2026-09-08 13:44:14 -06:00
Henrik Rydgård 11265049c0 Merge pull request #22260 from hrydgard/iso-trailing-partial-sector
Don't drop an ISO's trailing partial sector
2026-09-08 13:43:04 -06:00
Henrik Rydgård 08336e8c22 pspautotests: pick up the toolchain build fixes
Every test directory builds under pspdev GCC 15 again, so gentest.py no longer
aborts on a neighbour's compile error before it reaches the PSP. No .prx was
regenerated, so the suite behaves exactly as before - 319/319 still pass.

Also updates the hardware doc: the "several tests don't build" workaround is
gone, and it now records what rebuilding a .prx actually costs (64-bit time_t
changes what rtc/convert tests), that host0: differs per host OS, and that
PRXs stay resident so you need a reset between runs.
2026-09-08 12:43:40 -06:00
Henrik RydgårdandClaude Opus 5 51ec045947 Don't drop an ISO's trailing partial sector
Not every disc image is a whole number of 2048-byte sectors - tools that build
pre-patched ISOs write images that stop partway through their last one, with a
file legitimately ending there. Two things then conspired to lose that tail.

FileBlockDevice::GetNumBlocks() rounds down, so the partial sector isn't
counted, and the file size clamp in ISOFileSystem measured what the image holds
in whole blocks. A file running to the last byte of such an image got clamped
short - by up to a sector - before anything read it.

FileBlockDevice::ReadBlock() then returned false for a short read of that
sector, and ISOFileSystem::ReadFile substitutes an all-zero sector when a read
fails, so even the bytes that were there came back as zeroes.

Measure the clamp in bytes via GetUncompressedSize() instead of blocks, and
treat a short read at the end of the image as a success with the rest of the
sector zeroed. GetUncompressedSize() defaults to the block-based value and is
only overridden by FileBlockDevice, so nothing else changes behaviour.

Also report why a module was rejected. "Failed to load module" named the file
and nothing else, and the truncation check logged only the byte count, which
points at the executable when the real cause is that the loader was handed
fewer bytes than the file has. ElfReader now keeps the reason for a failed
LoadInto, __KernelLoadELFFromPtr puts it in the error string that reaches the
user, and both messages say which header table overran and by how much.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 12:25:21 -06:00
Henrik Rydgård 87bb9dd965 docs: how to write a pspautotest and run it on a real PSP
We had a doc for running the existing tests against headless, but nothing on
the other half - bringing up PSPLink and usbhostfs_pc, what gentest.py does,
and how to get an .expected out of real hardware. Write that down, including
the parts that cost time to rediscover: usbhostfs_pc's working directory is
host0:/ so it has to start in the pspautotests root, gentest.py makes the
whole test directory and several old tests no longer build under pspdev's
GCC 15 (use -k), rebuilding a .prx with a newer toolchain balloons it, and
host0: is not FAT so anything testing FAT semantics needs ms0:.

Also adds the io/shortname test the doc uses as its worked example. It stays
in tests_next: hardware preserves the case of d_name where we uppercase it,
and appends ~1 to the short name of anything that isn't already valid
uppercase 8.3 where we only do that on a collision.

threads/tls/create moves to tests_next as well. It's collateral from the
submodule bump - upstream 1dcefeb regenerated its .expected on a PSP with
less free memory, so allocations at 1MB and above now expect failure, and
partitions 8 and 9 now expect 800200D2 where we return 800200D1.
2026-09-08 11:43:38 -06:00
Henrik RydgårdandClaude Opus 5 f9bd5682db libkirk: let C++ callers include its headers directly
kirk_engine.h and amctrl.h guard their declarations, but AES.h and SHA1.h
never did, and kirk_engine.h includes them from outside its own guard. So the
AES_* and SHA1* functions got C++ linkage in any C++ file that reached them
through there, and only linked for callers that happened to wrap the whole
header in an extern "C" of their own. Nothing had called AES_* from C++
before, so it stayed hidden until something did.

Guarding the two headers instead lets every caller include them plainly, and
the wrappers scattered around the tree come out. Both are pure declarations
over kirk_common.h's typedefs with no system headers behind them, so there's
nothing in there that shouldn't be wrapped.

kirk_engine.h also uses size_t without including anything that defines it,
which only held together because its includers happened to have it already.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 11:15:15 -06:00
Henrik RydgårdandClaude Opus 5 19723f59eb Decrypt the NPDRM modules a PKG game update installs
A .sprx from one of these packages is an NPDRM "\0PSPEDAT" container: a
0x90-byte header, then an ordinary ~PSP PRX. The loader only ever saw the
EDAT magic and gave up with SCE_KERNEL_ERROR_UNSUPPORTED_PRX_TYPE.

Step over the header, then derive the key the PRX inside is really
encrypted against: sceNpDrmGetFixedKey() over the content ID, XOR in the
licensee key the game handed us through sceNpDrmSetLicenseeKey(), then AES
under a module key that had to be added. Both halves of that were already
lying around unused - sceNpDrmGetFixedKey() had no callers at all, and the
licensee key was being kept and never read.

The rest of it is a fixed XOR that the PRX header's decrypt_mode selects
rather than its tag, so it's applied on the mode the way JPCSP does it and
the tag table is left alone - tag 0x407810F0 carries no seed of its own
there either, so ours was never wrong about it. pspDecryptType5() already
had a slot for both XORs; no new decryption logic was needed.

Decryption is only half of it: these modules are KL4E-compressed rather
than gzipped, so they also need Core/Util/KL4E.cpp, which is already there
for the firmware modules that use the same compression. With both halves
Shiren 4 Plus loads its one big .sprx and runs. God Eater 2 needed one
further fix that isn't in this commit - the type-B relocation bug in
ElfReader::LoadRelocations2, issue #8075 - and then plays.

docs/pkg_notes.md has the container layout and the key derivation.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 11:15:15 -06:00
Henrik RydgårdandClaude Opus 5 f0dafdee10 Show and remove installed game updates from the game info screen
An installed update silently replaces what the game boots, so the info
pane now says when there is one - version, size and where it lives - and
the context menu offers to remove it again.

Removing takes the whole PSP/GAME/<DISC_ID> folder when the update is all
that's in it. When a digital game shares the folder, only PBOOT.PBP goes,
since deleting the folder would take the game with it and nothing records
what the install wrote. The confirmation names the exact path either way.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_018izZ1mGTWhz2RqeudqsDQR
2026-09-08 11:15:15 -06:00
Henrik RydgårdandClaude Opus 5 3a70c6b149 Install PKG game updates, and boot them
Opening a .pkg now offers to install it, the way a .zip does - see the new
InstallPkgScreen, which shows what the update patches, what it'll take up
on disk (exact, since package contents aren't compressed) and where it's
going. The package's PS3-style USRDIR/CONTENT wrapping is stripped so the
files land where the PSP expects them, in PSP/GAME/<DISC_ID>.

Booting a disc then looks for PSP/GAME/<DISC_ID>/PBOOT.PBP and boots that
instead of the disc's own EBOOT, leaving the disc mounted - so the update
overrides the files it ships and the disc supplies the rest. The update's
DISC_ID has to match; a DISC_VERSION mismatch only warns, since updates do
get used with slightly different dumps in practice.

Verified against the whole corpus: every one installs, and the
digital NP* update/base-image pairs that could be assembled all boot the
patch rather than the disc's executable. That includes Super Robot Taisen
Operation Extend from a real NPUMDIMG EBOOT.PBP, which settles that
ISO.BIN.EDAT does not re-key the PBOOT - a digital title's patched EBOOT is
encrypted exactly like a UMD one. On the UMD side, the patched
LittleBigPlanet reads PATCH.ARC out of the install alongside the disc's own
archive.

The DISC_VERSION warning turns out to be load-bearing: many of the pairs
mismatch, because the dumps in circulation are later disc revisions than the
updates were built against. docs/pkg_notes.md has the numbers, and the one
thing that doesn't work - PGD-wrapped .sprx modules, which
sceKernelLoadModuleNpDrm can't load.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-08 11:15:15 -06:00
Henrik RydgårdandClaude Opus 5 5089caf2a0 Add a reader for PKG game update packages
PSP game updates were distributed as NPDRM .pkg files holding a patched
EBOOT (PBOOT.PBP) plus the data files the patch replaces. PkgUnpack reads
one: header, item table, both PARAM.SFOs, and the AES-128-CTR that covers
everything past the header - including the per-item key split, where an
item's pspType byte picks between the PSP and PS3 keys.

Nothing new is needed to decrypt these. All packages checked use PRX
tag 0x2E5E10F0 for their PBOOT, which PrxDecrypter already has a key for.

Also adds "PPSSPPHeadless --install-pkg=DIR", a sibling of --unpack-updater,
which installs without any UI. All packages install through it byte
-identically to a reference implementation.

Format notes are in docs/pkg_notes.md.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-08 11:15:11 -06:00
Henrik RydgårdandClaude Opus 5 ddc673916d Tools/pkg.py: read PSP update packages, and notes on the format
A standalone Python reader for the .pkg containers Sony shipped PSP game
updates in: it prints a package's header, metadata, both PARAM.SFOs and its
item table, and extracts the payload. Nothing in PPSSPP calls it - it exists to
work the format out and to have a second implementation to check the C++ one
against, the way Tools/ already holds a few other one-off analysis scripts.

docs/pkg_notes.md is what it was written from: the header and item table
layout, the two AES-CTR keys a single package mixes, and the detail that trips
up a first attempt - which key applies is per item, not per package, so a
reader that picks one produces garbage filenames for most of a package while a
few entries decode perfectly.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 11:14:46 -06:00
Henrik Rydgård 1fc823681f Merge pull request #22252 from hrydgard/scemp4-real-prx
Run the real libmp4.prx/mp4msv.prx instead of our sceMp4 HLE, optionally. Fixes Speedball
2026-09-08 10:54:50 -06:00
Henrik Rydgård d11175bb88 Merge pull request #22254 from hrydgard/elf-relocation2-hi16-pairing
Fix type-B relocations losing the shared lo16 between two HI16s
2026-09-08 09:35:26 -06:00
Henrik Rydgård 3f5ddf9096 Merge pull request #22256 from hrydgard/log-callback-list
External log callback: Allow multiple handlers, fix bug in LogBroadcaster
2026-09-08 09:35:15 -06:00
Henrik Rydgård 0905d5d6d4 Merge pull request #22257 from hrydgard/websocket-mips-tracer
Expose the MIPSTracer over the WebSocket debugger
2026-09-08 09:31:52 -06:00
Henrik RydgårdandClaude Opus 5 fbcc8ee987 Fix type-B relocations losing the shared lo16 between two HI16s
LoadRelocations2 declared last_type, initialised it to -1, read it once - and
never assigned it. So the (flag & 0x38) == 0x08 case, which means "reuse the
lo16 the previous relocation carried", always saw last_type != 4 and reset
lo16 to 0 instead.

That matters because R_MIPS_HI16 computes ((op << 16) + lo16) + relocate_to and
then adds 0x10000 if bit 15 of the result is set, to pre-compensate the sign
extension the paired addiu will do. With lo16 wrongly 0 the carry decision is
made on the load address alone, so for any base whose low half has bit 15 set
the high half comes out one too high and the pointer lands 0x10000 past what it
should be.

A compiler emits exactly this pattern around a branch-likely: one lui in the
delay slot, another on the fall-through path, both for the same symbol, sharing
a single addiu after the paths converge. Only the second lui is adjacent to a
HI16, so the first one silently got the wrong high half.

last_type is assigned where JPCSP assigns its R_TYPE_OLD: at the end of the
branch that actually relocates something, so the commands that only move the
base around don't count as "the previous relocation" and a HI16/HI16/LO16 group
still pairs up across them. R_MIPS_NONE stops continuing the loop for the same
reason - it has to clear last_type, or a HI16 after it would reuse a lo16 that
isn't its own.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 08:55:57 -06:00
Henrik RydgårdandClaude Opus 5 ccdaa94f53 Expose the MIPSTracer over the WebSocket debugger
The tracer records every basic block the CPU executes and can write the
instruction stream out to a file, which is the tool you want when something
corrupts state and the question is "what actually ran just before". It was
only reachable from Developer Tools in the UI, so a scripted session had no
way to turn it on, and reconstructing the same thing from log-only breakpoints
means guessing what to watch before you know what happened.

Five events: cpu.tracer.start/stop/flush/clear/status. start takes the two
buffer sizes and clears the JIT cache by default, because blocks compiled
before tracing was on don't carry the LogIRBlock instruction the tracer feeds
on - without that a hot loop compiled earlier simply never appears. The trace
ring is cyclic, so a finished recording holds the last maxTraceSize blocks:
start it, run into a crash, and the tail of the file is the instructions that
led there.

Only the IR cores drive the tracer, so start refuses on the others and says
which core is loaded rather than recording nothing; status reports the same
thing as `supported` so a client can tell that apart from "nothing executed".
Everything that mutates tracer state goes through Core_RunOnCPUThread, per
docs/DebuggerThreading.md.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 18:43:38 -06:00
Henrik RydgårdandClaude Opus 5 4e24c195ac Fix the debugger log stream going out of step after 1024 messages
DebuggerLogListener keeps a ring of the last BUFFER_SIZE messages. read_ and
count_ count messages ever read/written, while messages_ is indexed modulo
BUFFER_SIZE - but the non-overflow path used read_ directly as the index. Once
a session had logged BUFFER_SIZE messages that index was past the end of the
array, so the first copy loop (bounded by BUFFER_SIZE) ran zero times and the
second one handed back messages_[0..readCount-1] instead: real log messages,
just the wrong ones, and every later poll stayed the same distance out of step.

It looks like the tail of the log going missing rather than being wrong, which
is a bad way to find out. A log-only breakpoint in a hot loop reaches 1024
messages in seconds, and then "the last thing logged before the breakpoint hit"
- exactly what such a breakpoint is for - names an event thousands of messages
old. Confirmed against a case with an independently known answer: a log-only
breakpoint recording a register in a loop that runs ~20k times now ends with
the value that register actually held at the final hit, where before it ended
several hundred iterations short of it.

The overflow path was already correct - it starts from nextMessage_, which is
an index - so only the one line changes.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 18:42:39 -06:00
Henrik Rydgård 98e70c8ca3 Merge pull request #22251 from hrydgard/log-callback-list
Let more than one thing receive the log stream at a time
2026-09-07 15:51:23 -06:00
Henrik RydgårdandClaude Opus 5 e26e2d28a0 Let more than one thing receive the log stream at a time
The log manager had a single external-callback slot, but the WebSocket
debugger registers one per *connection* - LogBroadcaster is a local in the
per-connection handler. So with two clients attached (the bundled JS debugger
in a browser and Tools/wsdbg, say) the second one to connect silently took the
log stream away from the first, and then whichever disconnected first cleared
the slot and stopped delivery to the other as well. A one-shot wsdbg command is
enough to do it: connect, take the stream, exit, and the long-lived listener
that was watching the log goes quiet with nothing to say why.

Make it a list with add/remove by handle. The dispatch loop holds the lock
across the callbacks so a listener can't be freed while one is running - which
is what lets LogBroadcaster delete its listener straight after removing it.
Enabling and disabling LogOutput::ExternalCallback belongs to the list now, and
disabling only happens when the last callback goes away.

libretro registers one of these too, and never removes it; it just moves to the
new call. It can't actually collide with the debugger - the libretro build
doesn't compile Core/Debugger/WebSocket at all - but there's no reason for it
to keep using an API that only has room for one caller.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 15:23:50 -06:00
Henrik RydgårdandClaude Opus 5 9799085c3a Let more than one thing receive the log stream at a time
The log manager had a single external-callback slot, but the WebSocket
debugger registers one per *connection* - LogBroadcaster is a local in the
per-connection handler. So with two clients attached (the bundled JS debugger
in a browser and Tools/wsdbg, say) the second one to connect silently took the
log stream away from the first, and then whichever disconnected first cleared
the slot and stopped delivery to the other as well. A one-shot wsdbg command is
enough to do it: connect, take the stream, exit, and the long-lived listener
that was watching the log goes quiet with nothing to say why.

Make it a list with add/remove by handle. The dispatch loop holds the lock
across the callbacks so a listener can't be freed while one is running - which
is what lets LogBroadcaster delete its listener straight after removing it.
Enabling and disabling LogOutput::ExternalCallback belongs to the list now, and
disabling only happens when the last callback goes away.

libretro registers one of these too, and never removes it; it just moves to the
new call. It can't actually collide with the debugger - the libretro build
doesn't compile Core/Debugger/WebSocket at all - but there's no reason for it
to keep using an API that only has room for one caller.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 15:15:10 -06:00
Henrik Rydgård fdd9baaba1 Merge pull request #22250 from hrydgard/audiocodec-field-names
sceAudiocodec: Use more information for decoding, describe more fields
2026-09-07 13:56:11 -06:00
Henrik Rydgård 8206ed88f7 Merge pull request #22249 from hrydgard/thread-run-clocks
sceKernelThread: actually accumulate runForClocks
2026-09-07 13:51:28 -06:00
Henrik Rydgård 1da84afbb0 Merge pull request #22243 from 4RH1T3CT0R7/fix/debugger-vfpu-register-view
Win32 debugger: show VFPU values, make the register list scrollable
2026-09-07 13:48:54 -06:00
Henrik Rydgård 8559ed971f Merge pull request #22240 from acts-1631/security/fix-infra-dns-json-validation
Validate infra DNS JSON responses
2026-09-07 13:42:54 -06:00
Henrik Rydgård b0af7a8dfe Merge pull request #22239 from acts-1631/security/fix-png-replacement-limits
Bound replacement PNG dimensions safely
2026-09-07 13:10:57 -06:00
Henrik RydgårdandClaude Opus 5 a91448b318 sceKernelThread: actually accumulate runForClocks
nt.runForClocks was zeroed when a thread was created and copied out by
sceKernelReferThreadStatus, but nothing ever added to it, so every thread
reported having run for zero time forever.

Crazy Taxi: Fare Wars uses it as a liveness check. Its music state machine
samples the mp3 thread's run time once every 60 frames and compares it with
the previous two samples; when it doesn't move it concludes playback is
wedged, sets the stop bit, and the thread tears itself down and exits. The
game restarts it, and about a second later decides it's wedged again - custom
soundtracks restarted roughly once a second forever, whatever the file.

Bill the time since the previous switch to the outgoing thread, which is
exactly the thread that was running for it. The field is already part of the
serialized thread struct, so savestates don't change format; the timestamp
itself is re-based on load rather than saved, and only on load - saving runs
a measure pass and a write pass, and re-basing in those would discard the
time the running thread had accumulated since the last switch, letting a save
change what the game can observe.

Risk: this runs on every context switch, the hottest path in the scheduler.
It adds one CoreTiming read and a 64-bit add. Games that poll thread run
times will now see them move, which is correct but is new behavior.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 12:48:22 -06:00
Henrik Rydgård d1f33c9fd5 Merge pull request #22247 from hrydgard/kl4e
Implement KL4E/KL3E decompression, so firmware modules that use it can load
2026-09-07 12:22:16 -06:00
Henrik Rydgård c0e09f8ef0 Merge pull request #22244 from hrydgard/raintegration-elevated-install
Don't load RAIntegration from an install we can't write to
2026-09-07 11:22:08 -06:00
Henrik Rydgård cae47b3df5 Merge pull request #22246 from hrydgard/more-misc-changes
sceFont: let glyphs draw past bytesPerLine, like the hardware does
2026-09-07 11:21:34 -06:00
Henrik RydgårdandClaude Opus 5 fb8c99ad49 sceIo: generate and resolve FAT 8.3 short names
sceIoDread hands back a dirent whose d_private holds the 8.3 short name
ahead of the long name, and we never wrote the short name at all - the game
got whatever was on the stack there. Crazy Taxi: Fare Wars reads it rather
than d_name, so it rejected every file in ms0:/MUSIC, ended up with an empty
playlist and never even reserved an mp3 handle: custom soundtracks were
silently dead, with the game spinning on InitResource/SetLoopNum forever.

Generate the names from the directory listing, and resolve them back in
DirectoryFileSystem so a game can open a file by the short name it was given.
Both sides come from the same function, so they agree.

We can't lean on the host for any of this. Linux, macOS and Android have no
8.3 names at all, and while Windows does keep aliases it generates them by a
different rule - it counts to ~4 and then switches to a hash - so resolution
runs before the literal path is tried rather than as a fallback, or on
Windows we'd quietly open a different file than the one we handed the game.

The exact names a real PSP produces are still unverified - no pspautotest
covers d_private - so this implements the ordinary FAT rule and the new
FatShortNames unit test pins that down until hardware can settle it.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 10:20:49 -06:00
Henrik Rydgård 284472ae13 Merge pull request #22245 from hrydgard/mp3-stream-buffer
sceMp3: Stream buffering fixes
2026-09-07 10:20:22 -06:00
Henrik RydgårdandClaude Opus 5 27ca0f80fc sceFont: let glyphs draw past bytesPerLine, like the hardware does
SetFontPixel refused to write any pixel whose x fell outside
bytesPerLine, so a glyph drawn into a buffer with a bytesPerLine
narrower than its rows came out mostly blank. The hardware doesn't
bound it that way - it works out an address and writes, so the rows
overlap and the glyph smears across them. The declared bufWidth and
bufHeight, plus the address check, are what keep it in bounds.

Cache invalidation now covers the wider of bytesPerLine * bufHeight and
where the last row actually ends, since those are no longer the same
thing when the rows overlap.

Fixes font/charglyphimage and font/charglyphimageclip, moved from
tests_next to tests_good. The other font tests are unaffected, so
whatever ails them is something else.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-07 09:27:13 -06:00
Henrik RydgårdandClaude Opus 5 5a0006efe1 sceMp3: take the lowest free handle, not the map size
sceMp3ReserveMp3Handle derived the new handle from g_mp3Map.size(), which
collides as soon as handles are released out of order: with 0 and 1 open,
releasing 0 leaves size at 1, so the next reserve returns 1 again. That
replaced the live context in the map without deleting it, leaking it and
handing the game a handle aliasing a stream it was still playing.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 09:18:02 -06:00
Henrik RydgårdandClaude Opus 5 b855cec6d4 sceMp3: stop decoding at endPos instead of running off the buffer
A game can notify more data than the file actually had - audio/mp3/stream
asks for 3360 bytes and notifies all of them even when the read came up
short - so the tail of the buffer holds stale bytes from the previous half.
We happily decoded those, six frames past the end of the stream, because the
end flag only suppressed the zero fill and never stopped the decoder.

Check it before decoding too. The post-decode check stays where it was: the
hardware rewinds in the same call that decodes the last frame, so the sum
reads back as zero right after it, which is what audio/mp3/getsumdecoded
records. Moving the whole thing up front breaks that test.

Fixes audio/mp3/stream, added to tests_good - it walks 27 refills end to end,
so it also covers the half-buffer handout.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-07 09:18:02 -06:00
Henrik Rydgård 24e8d931e0 sceMp3: hand out the stream buffer in halves, like the hardware does
The area after the 0x5c0 workarea is double buffered - a half only becomes
writable again once the decoder has consumed past its end, so decoding a
single frame usually frees nothing at all. We instead reported every byte a
decode had just consumed, which made sceMp3CheckStreamDataNeeded() answer
"yes" after every single frame.

Beats sleeps 50ms whenever that call says the file thread is behind, so it
slept once per decoded frame and delivered audio at 46% of realtime - the
badly stuttering custom soundtracks. It now decodes 3-4 frames per 3360 byte
refill, with the write pointer alternating between the two halves exactly as
audio/mp3/stream records from hardware, and keeps up.

AuGetInfoToAddStreamData/AuNotifyAddStreamData now derive the write position
from how much has been added rather than from how much is still buffered,
since the write pointer walks the halves in turn and doesn't follow the
decoder.

Fixes audio/mp3/notifyadd, moved to tests_good, and the "after decode" case
in audio/mp3/checkneeded.

sceMp3: note that the half-buffer split is only verified at 8192 bytes
2026-09-07 09:17:48 -06:00
Henrik Rydgård 0ab672f87f Merge pull request #22238 from hrydgard/sdl-prefer-wayland
SDL: Prefer Wayland when we're in a Wayland session
2026-09-07 09:10:33 -06:00
Henrik RydgårdandClaude Opus 5 3af6080075 add-string: check the arguments before translating anything
The command splits $1/$2/$3 off the invocation positionally and then states them
as fact, so calling it with a sentence rather than `/add-string <Section> "<Key>"`
yields three arbitrary words - and nothing downstream notices. Ask for them to be
checked against en_US.ini first, and to re-derive the real section and key from
the request if they don't hold up.

Also spell out, in both the command and docs/translations.md, that the en_US line
in the scratch file overwrites en_US.ini like any other language, so for an
existing key it has to match the current English text exactly - otherwise it
quietly rewords the string every other language was translated from.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
2026-09-07 09:04:26 -06:00
Henrik RydgårdandClaude Opus 5 2b16e107b9 Translate "RAIntegrationNotWritable"
36 languages, following each file's existing handling of the sibling RAIntegration
string - most keep the name as-is, pl_PL uses "Integracja RA", tr_TR "RA Entegrasyonu",
ja_JP "RAインテグレーション" - and its formality. The rest are left to fall back to
English rather than guessed at.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
2026-09-07 09:02:19 -06:00
Henrik Rydgård 856f627b74 Merge pull request #22242 from Kethen/cmake_cleanup
cleanup cmake aemu_postoffice building, allow connecting to 127.0.0.1 adhoc server outside of PPSSPP process
2026-09-06 14:30:22 -06:00
Henrik RydgårdandClaude Opus 5 afeab27186 Don't load RAIntegration from an install we can't write to
RAIntegration keeps its cache and local achievement data next to the
executable. If PPSSPP is installed somewhere that needs elevation to write -
Program Files being the obvious case - that write fails and takes the emulator
down as soon as a set or code notes are loaded, with no log to show for it since
the log can't be written either.

Check whether the exe directory is writable before handing the DLL to rcheevos,
and if it isn't, say so and point at the portable .zip instead. Achievements
themselves still work, so carry on to the normal login.

Fixes #21260

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
2026-09-05 16:15:45 -06:00
Henrik RydgårdandClaude Opus 5 463bd7acf9 SDL: Prefer Wayland when we're in a Wayland session
SDL doesn't reliably pick Wayland on its own - on a machine with a
working Wayland compositor but no XDG_SESSION_TYPE it still chose x11,
putting us on XWayland. Ask for Wayland when WAYLAND_DISPLAY is set,
respect SDL_VIDEO_DRIVER if the user set it, and fall back to letting
SDL choose if Wayland then fails to initialize.

Also log the video driver we ended up with, which should help triage
the Wayland/X11 reports.

Fixes #21080

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-05 14:09:24 -06:00
Henrik Rydgård fefdf21db5 Merge pull request #22236 from hrydgard/savestate-hardcore-race
Fix savestate loads slipping past hardcore mode during boot
2026-09-05 14:07:40 -06:00
Henrik RydgårdandClaude Opus 5 124b0a43ae sceMp3: point the game at the end of the buffered data, not the start
sceMp3GetInfoToAddStreamData always handed back the start of the work
area, so the pointer never moved as data was added - the hardware walks
it forward past what's already buffered. AuNotifyAddStreamData now
takes the new bytes from where the game was actually told to write, and
checks that range fits the buffer rather than just comparing the size.

Also compare readPos against endPos as signed. readPos is an int and a
game can notify a negative size, which made it promote to a huge u64
and look like the end of the stream, so we reported nothing left to
write where the hardware still wanted 6721 bytes.

Fixes audio/mp3/infotoadd, moved to tests_good. audio/mp3/notifyadd
gets both of its value differences fixed but still fails: after a
decode the hardware reports no space at all, while we free what the
decode consumed, so we do one round more than it does.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
2026-09-05 13:50:49 -06:00
Henrik Rydgård c1b822ad07 Merge pull request #22190 from hrydgard/texture-bounds-fixes
TextureCache: fix two host-memory overruns from GE texture state
2026-09-05 13:48:41 -06:00
Henrik Rydgård 5d77d5daff Merge pull request #22229 from hrydgard/psp-file-attrs
Report PSP file attributes rather than the host's
2026-09-05 13:48:15 -06:00
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) <[email protected]>
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) <[email protected]>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
2026-09-05 13:29:50 -06:00