Boot the VSH (XMB) through HLE - #22126
Merged
Merged
Conversation
hrydgard
marked this pull request as draft
August 21, 2026 10:10
hrydgard
force-pushed
the
vsh-hle-boot
branch
2 times, most recently
from
August 21, 2026 12:19
4892a6b to
6b80c6c
Compare
hrydgard
marked this pull request as ready for review
August 21, 2026 12:46
Owner
Author
|
Before I merge, I have to figure out what to do with the flash0: directory mount - assets/flash0 can't be modified on most platforms. I guess we'll look in /PSP/flash0 if it exists... |
…nUtil stub and sceVshBridge Load and start VSH's kernel modules before booting vshmain.prx A few flash0 modules (vshbridge.prx, paf.prx, common_gui.prx, common_util.prx) should run for real once we know we're actually booting the VSH rather than a game, since our fakes are unlikely to be good substitutes for the genuine thing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSNaZnHCjmryS3ziVN9gZU
Adds SceKernelLoadExecVSHParam and fills in the rest of LoadExecForKernel's NIDs: real implementations for sceKernelExitVSHVSH/Kernel (mirrors sceKernelExitGame) and sceKernelLoadExecBufferVSHUsbWlan (loads an exec from an in-RAM buffer instead of a file, plus UNIMPL stubs. sceKernelLoadExecBufferVSHUsbWlan needed __KernelLoadExec split into a file-reading front end and a shared __KernelLoadExecFromPtr back end that both it and the new buffer-based path call into. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSNaZnHCjmryS3ziVN9gZU (cherry picked from commit 4adbedec9e2e3221113c9e5ad00b248bba1153e1)
Extends LoadAndStartVshKernelModules() to load the 11 real kd/*.prx kernel drivers for --vsh (dmacman, systimer, memlmd_01g, loadexec_01g, lowio, idstorage, syscon, rtc, wlan, wlanfirm_01g, utility), ahead of the existing 4 VSH-specific modules. Only active when g_runningVSH, no effect on normal game boot. Improve implementations of sceKernelSm1ReferOperations and sceKernelIsIntrContext. Add some more MMIO stubs (GPIO, SYSCON). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 (cherry picked from commit 7f3168b7df85e47438900016c9ee7d7ef01a0a28)
Extends the SysconSerialMMIO stub added for VSH boot into a real command/ response protocol matching uofw's Syscon_cmd() reference exactly (packet framing, checksum, GPIO4 "response ready" handshake via a new GpioMMIO cross-module hook), with handling for NOP/read-write clock/read-write alarm commands. Still hitting a SIGSEGV though. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9 (cherry picked from commit 11887bf9e1fecd1eac56ec705c01b6fcfac09b2e)
For some reason, a pointer used to allocate the heap for scePaf is not initialized. This hacks aroung that. Additionally zero out the specific 4-byte "category 1 alarm count" address in vsh_module. This gets us much further.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
ModuleMgrForKernel/0xD5DDAB1F is how the VSH loads its own plugins. The XMB's interface lives in flash0:/vsh/module/*_plugin.prx and vshmain pulls those in through this kernel call rather than the user-mode sceKernelLoadModule, so the existing note that vshmain never imports sceKernelLoadModule was true but incomplete - it imports this instead, and it was unresolved. The consequence was quiet: vshmain got no module id back and then called sceKernelStartModule with id 0, which failed with UNKNOWN_MODULE. None of the plugins that populate the XMB ever ran. The scene still had its containers, which is why every frame set up render state per node and drew nothing inside them - the "6x render-state-setup, 0 draws" symptom this investigation has been chasing. Also implements 0xD86DD11B sceKernelSearchModuleByName, the other unresolved ModuleMgrForKernel import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
… we must not) Mostly small stubs: - sceImpose: GetParam/SetParam/Changes/SetStatus, plus the 6.60 alias of sceImposeGetBatteryIconStatus. Also fixes that function's first output - it is a plain "is it charging" boolean, not a BATTICON_ value. We wrote PSP_IMPOSE_BATTICON_NONE (0x80000000) there, which games ignore but which the VSH reads as "no battery" and draws the empty-battery indicator for. These are the bulk of the traffic: the VSH calls sceImposeChanges once a frame, so this alone removes ~10000 trapped calls from a boot. - SysMemForKernel: sceKernelSetRebootKernel, sceKernelSetUmdCacheOn. - scePower_driver: scePowerSetWakeupCondition. - sceHprm_driver, sceUsb: one NID-named call each, as in JPCSP. Three groups are deliberately left unresolved, with comments explaining why, because resolving them lets real flash0 drivers walk into hardware we do not emulate and the boot dies where it used to reach the shell: - ThreadManForKernel mutex/fpl NIDs: the NAND and ID storage drivers use these to init, then poll the NAND controller at 0xbd101300 forever. - InterruptManagerForKernel intr registration: 31 calls, then a stall in GE list execution with no plugin module ever started. 73 unresolved import hits over 37 distinct module/NID pairs remain in a VSH boot, mostly sceSysEventForKernel, sceSuspendForKernel and the various *_driver modules that need real hardware behind them. The sceImpose savestate section goes to v2 for the two new state variables. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
Found the cause of the red error screen the VSH ends on. Every resource load in the boot succeeds - fonts, all the plugin RCOs, topmenu_icon.rco - and then: sceIoOpen(flash0:/vsh/etc/index_02g.dat) -> fd 8 sceIoRead(8, 092a2d40, 496) sceIoClose(8) unresolved import sceResmgr/9dc14891, called from 'vsh_module' sceKernelExitDeleteThread(1) index_02g.dat is the index of what the XMB displays, and it is encrypted (it starts "PSPsysGP"). sceResmgr_9DC14891 decrypts it. There was no sceResmgr module at all, so the call trapped, the index stayed encrypted, and the ScePafJob thread building the top menu exited - a shell with everything loaded and nothing to show. This adds the module and the three tags it needs (0x0B2B90F0/91F0/92F0, keys and code 0x5C) to PrxDecrypter. It is not the whole fix yet: pspDecryptPRX() tries decryption types 0, 1, 2, 5 and 6, and this needs type 9, which JPCSP passes explicitly. So the call is now reached and fails cleanly with a logged error instead of trapping, but does not yet decrypt. Type 9 is a variant of type 2 and is the next job; the notes in docs/VSHBootInvestigation.md say where it is in JPCSP and how to check a port (159 bytes out, starting "release:"). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
pspDecryptPRX() tried types 0, 1, 2, 5 and 6. flash0:/vsh/etc/index_XXg.dat - the index of what the XMB shows, fetched through sceResmgr_9DC14891 - needs type 9, so it failed and the shell had no menu to build. Type 9 is type 6 with three differences, all following from a type 9 file carrying a real ECDSA signature at 0x104..0x12C where a type 6 file has nothing: - The "must be empty" header check stops at 0x104 instead of 0x10C. The index's signature starts there, so 8 of its bytes were failing type 6's check - the original failure. - The signature is left out of the hashed header rather than fed into it. JPCSP zeroes buf2[0x34..0x5C), which is that same range once its header rearrangement is undone, so PRXType9 just leaves the field zero. - ecdsa_hash in the KIRK CMD1 header stays 0. Type 6/7 set it, but the branch type 9 takes writes only the mode word, and setting it made KIRK reject the block. Tried last in the chain: its header check is a subset of type 6's, so a genuine type 6 PRX would pass it and then fail on the hash, and trying it earlier would shadow the real answer. False positives are not really possible either way - the SHA1 check inside has to match before anything is decrypted. Verified end to end: 496 bytes in, 159 out (the comp_size in the header), starting "release:". sceResmgr checks that prefix and says so in its log line, since a wrong-but-plausible decrypt would otherwise look like success here and fail much later as an unreadable index. The VSH now draws something different - the per-frame display list settles at 24 stall points rather than 38 - but what it shows is not visually confirmed; framebuffer readback doesn't work under headless on either backend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
sceIoDevctl 0x01E18030 asks whether the disc's region matches the console's. Unusually it answers through the return value rather than an output buffer - 1 matches, 0 doesn't - so leaving it unimplemented read as a mismatch, and the VSH opened on "This disc cannot be started. The region code is not correct.", asking 12 times. PPSSPP has no region-locked discs; anything it can load is something it should run, so this always matches. The call now happens once. Behind it is a second thing, not fixed here: the VSH believes a disc is inserted at all because nothing in PPSSPP models an empty drive. sceUmd reports PSP_UMD_PRESENT | PSP_UMD_READY unconditionally, and devctl 0x01F20001 always answers "game disc". JPCSP answers "no disc" when no ISO is loaded, which is why it never reaches the region question. Giving those two a notion of "no disc" would be the real fix - low risk for games, which always have one, but it is on a path every game uses, so it is written up in docs/VSHBootInvestigation.md rather than done as a drive-by. With this the shell reaches the interactive XMB: the error is dismissable with circle and the menu behind it works. The per-frame display list stops settling into one repeated frame and alternates between 45 and 48 stall points, which is the headless-visible sign of a live menu rather than a static dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://www.ppsspp.org/docs/getting-started/how-to-run-the-vsh/
An almost weeklong collaboration between me and Claude Sonnet/Opus to boot the VSH in PPSSPP. The VSH, visual shell, is more known as the XMB or the Xross Media Bar - the main user interface of the PSP. This took a lot of investigation and fixes, and careful steering of Claude - but it did do a lot of difficult investigation for me. It would have taken way, way longer without it.
Currently only works in the plain interpreter, since we had to simulate a little bit of hardware at a low level, and the JIT doesn't understand memory-mapped I/O.
Functionality will be added soon to automatically extract the VSH from any game's built-in updater (at least if the game has a new enough updater).
Unfortunately, it's mostly useful for nostalgia - almost none of the actual functionality works at this point, but you can at least navigate around the interface and hear the bootup sound. It's very fragile, and only kinda works because some drivers that we can't support hang.
NOTE: Only supported with firmware 6.61 right now.
Credits go to the JPCSP project for some critical information. The path taken here is not identical to what JPCSP does, but it's along the same lines.