Commit Graph
47578 Commits
Author SHA1 Message Date
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) <noreply@anthropic.com>
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årdandClaude Opus 5 95d03449b3 sceMp4: don't reload the modules, read the effective flags, check four bytes
Three from a read-through of the sceMp4 firmware-module path:

Clearing the module UIDs when the game says it's done with the MP4 module
didn't unload anything - it only meant the next load brought in a second copy
of libmp4.prx and mp4msv.prx, some 220KB at the top of user memory each time.
Keep them for the boot instead; __UtilityInit clears them per game, which is
the point at which they really are gone.

The flag test read g_Config directly, so it ignored the very fallback
CheckDisableHLEAvailability computes when the dump is missing - it would go and
try to load modules that aren't there while import resolution had correctly
stayed on HLE. It also ignored a boundary restored from a savestate.

sceKernelGetModuleGPByAddress checked one byte of the pointer it writes four to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 16:59:21 -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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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årdandClaude Opus 5 794263ed6a headless: say when we're waiting at the entry point, and add --debugger-run
--debugger sets startBreak, so the run sits at the entry point until a client
resumes it. A session that forgets to do that looks like a frozen game rather
than a paused CPU - all the way down to "ticks: 0" - so say so on the way up,
and add --debugger-run for the common case of wanting the debugger attached to
a run that just goes.

Also stop headless forcing HLE for the graduated modules. Those come out of the
game's own disc rather than a firmware dump - libpsmfplayer.prx and friends are
user libraries, always present - so forcing them to HLE made headless quietly
disagree with the app about which code a game runs. Tekken 6 plays its movie
through scePsmfPlayer, and headless was faking it, so the movie never reached
sceMpeg at all; now the disc's real psmfplayer drives the real mpeg.prx.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 10:15:02 -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 a333a93a7c sceUtility: forget injected firmware modules between games
Clear them in __UtilityInit, which runs per boot.
2026-09-08 16:57:43 -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 <noreply@anthropic.com>
2026-09-08 15:52:10 -06:00
Henrik Rydgård cc44ba22d0 Android buildfix 2026-09-08 15:45:11 -06:00
Henrik RydgårdandClaude Opus 5 fa577eaa83 Document --re-module in AGENTS.md and docs/reverse-engineering.md
A tool nobody knows about is a tool nobody uses. AGENTS.md gets a short section
pointing at it, plus the two things most likely to be got wrong when reading
the output: that a function's arity can't be inferred from the registers it
reads, since MIPS code passes arguments through untouched, and that a finding
is worth much more when the comment says which module it came from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:32:23 -06:00
Henrik RydgårdandClaude Opus 5 386ff2fc3d headless: add --re-module, a static reverse-engineering dump for one PRX
Loads a single module standalone - no game, no boot - and writes a report:
module header and segments, the export and import tables with NIDs resolved to
names through the HLE tables, one annotated disassembly file per function, and
a call graph as JSON.

Reuses the emulator's own loader rather than parsing PRXes a second time, so
decryption, decompression, relocation and import resolution can't drift from
what actually runs. Only enough of the system is brought up to load a module:
memory map, timing, HLE tables and the kernel allocators. Nothing executes.

Two things beyond a plain disassembly, both aimed at the questions that come up
when reading unfamiliar MIPS:

- lui/addiu (and lui/load) pairs are folded and reported as the address they
  form, which is how every global and constant table gets reached.
- Per function, a register evidence block instead of a guessed signature. A
  MIPS function that takes two arguments and passes the second one down often
  never reads it, so 'never read but live across a call' is reported as
  forwarded rather than quietly dropped from the signature.

SetForceRealModuleLoads() is needed because modules like sceAudiocodec_Driver
have no DisableHLEFlags bit and so can't be turned off the normal way - they'd
fake-load and there would be nothing to look at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:32:14 -06:00
Henrik RydgårdandClaude Opus 5 fb976d4f61 Clamp the module function scan to the text range it already validated
The no-code-sections path validates textStart..textEnd, then derives its
actual scan boundaries from modinfo->libent/libstub without checking those
land inside it. flash0:/kd/sysmem.prx and loadcore.prx from a real firmware
dump put them tens of megabytes past the end of the text, so the scan walked
off into unmapped memory - a debug assert in Read_Instruction, and a pointless
134MB scan in release builds.

For a well-formed module every boundary is already inside the range, so this
is a no-op there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 15:31:29 -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
Nab b2dface40d Add/remove options when switching stereo shaders 2026-09-08 20:58:33 +00:00
Nab e9ae17331d Comment Atrac frame size limit for GTALCS 2026-09-08 20:58:33 +00: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 <noreply@anthropic.com>
2026-09-08 12:25:21 -06:00
Henrik RydgårdandClaude Opus 5 5a7ca5a6a5 HLE: make the HLE boundary part of the machine state, so savestates survive it
Which modules we HLE is decided when each module is loaded, and the syscall
stubs written into memory then are what a savestate captures. But the setting
was read live, so loading a state re-resolved its imports against whatever the
config said now - and if that disagreed with how the state was made, every call
into the module landed on an unresolved stub returning LIBRARY_NOT_YET_LINKED.
Thrillville just retried sceMpegInit forever.

Latch the flags on first use after boot, save them in the state, and restore
them on load. Changing the setting now takes effect on the next boot, which is
the only point it could have taken effect anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
2026-09-08 11:47:44 -06:00
Henrik RydgårdandClaude Opus 5 c0c715ac2e Load injected firmware modules at the top of user memory, not the bottom
The flash0 PRXes we swap in for our HLE took the lowest free block, which sits
right where a game's own EBOOT wants to go. That pushes the game up, shifting
every address in it - invalidating cheats and RetroAchievements - and for a game
whose EBOOT has to load at a fixed low address it fails outright: Tekken 6 wants
0x08804018 and got "block taken", so it didn't boot at all.

Give KernelLoadModule a fromTop flag and use it for the modules we inject. The
game keeps its normal load address and the firmware sits out of the way at the
top.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup
2026-09-08 11:43:49 -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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
2026-09-08 11:15:15 -06:00