diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 881f8dcbc0..b4be647332 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1154,7 +1154,7 @@ static void LoadAndStartVshKernelModules() { // declare their own smaller module_start_thread_stacksize, so __KernelStartModule's // generic 0x40000 (256KB) default applies to every one of them. 11 of // them at 256KB each (2.75MB) is a lot relative to the 4MB kernel memory pool. - // + // // If we run out of kernel memory for some reason, we can force these to a smaller stack size. static const char *const vshSmallKernelModulePaths[] = { "flash0:/kd/dmacman.prx", @@ -2079,6 +2079,13 @@ static bool __KernelLoadExecFromPtr(MIPSState * mips, const u8 *data, size_t siz option.stacksize = module->nm.module_start_thread_stacksize; if (g_runningVSH) { + // NOTE: JPCSP's --vsh handler additionally forces the root thread into kernel mode + // with the lowest priority (0x7E) after loading, to mirror real hardware. Tried here + // and reverted (see docs/VSHBootInvestigation.md, Attempt 17) - it changes thread + // scheduling order enough that sceVshBridge_Driver's thread runs before some + // precondition it needs is ready, regressing all the way back to the very first + // crash this investigation fixed (an immediate `break` in sceVshBridge_Driver). Left + // as the default module-declared attr/priority instead. LoadAndStartVshKernelModules(); } diff --git a/docs/VSHBootInvestigation.md b/docs/VSHBootInvestigation.md new file mode 100644 index 0000000000..952eaa8da8 --- /dev/null +++ b/docs/VSHBootInvestigation.md @@ -0,0 +1,280 @@ +# VSH Boot Investigation + +Notes on booting the PSP's Visual Shell (VSH / `vshmain.prx`) in PPSSPP via the direct HLE boot +path (`--vsh`), using a real Sony firmware dump (not checked into the repo - placed locally under +`assets/flash0/`, with `assets/flash1/` alongside it if you have one). This is exploratory +reverse-engineering of Sony's shipped, undocumented System Software, not a supported PPSSPP +feature - treat conclusions here as best-effort, not verified against hardware. + +## Start here + +One command boots VSH, runs to a fixed point, and tells you where it got. It starts the emulator, +finds the debugger port, connects, and cleans up after itself - no wrapper script needed: + +```bash +printf 'cpu.runUntilTime us=6000000\ncpu.status\n:quit\n' | \ + Tools/wsdbg/target/release/wsdbg.exe --sync --compact --quiet \ + --launch Windows/x64/Debug/PPSSPPHeadless.exe --vsh -i --graphics=vulkan --loglevel=3 2>emu.log +``` + +That takes a few seconds. Protocol replies come back on stdout, the emulator's log on stderr. +Put more commands in the script to inspect state once it stops. Three things to know: + +1. **Don't add `:wait cpu.stepping` after `cpu.runUntilTime`** (or any resume/step command). + `--sync` already waits for it; a second one never arrives and burns the whole timeout. This is + the single most likely reason a run seems to take minutes - emulation itself is ~real time. +2. **`-i` (interpreter)** is what this path is known to work with, and breakpoints are most + reliable there. +3. Boot needs a real firmware dump in `assets/flash0/` (and optionally `assets/flash1/`), neither + of which is in the repo. + +## Why it opens on a disc error + +The shell reaches the interactive XMB, but first shows "This disc cannot be started. The region +code is not correct." Cancel it with circle and the menu is there and usable. + +Two separate things: + +- **It thinks a disc is inserted**, because nothing in PPSSPP models an empty drive. + `sceUmd.cpp` reports `PSP_UMD_PRESENT | PSP_UMD_READY` unconditionally, and `sceIoDevctl` + `0x01F20001` (get disc type) always answers `0x10`, "game disc". JPCSP answers `0` when no ISO is + loaded, which is why it never asks the next question. Fixing this properly means giving those two + a notion of "no disc" - low risk for games, which always have one, but it is on a path every game + uses. +- **It then asked whether the region matched and we didn't answer.** `sceIoDevctl` `0x01E18030` is + the region check, and unusually it answers through the *return value* (1 matches, 0 doesn't) + rather than an output buffer. It was unimplemented, so the VSH read the failure as a mismatch and + retried 12 times. Implemented now - PPSSPP has no region-locked discs, so anything it can load is + something it should run - and the call drops to one. + +## Current status + +`--vsh` boots VSH and renders real XMB frames. Measured on the GE command stream after 25 +emulated seconds: 8 draw commands (two `DRAW SPLINE 16x9`, a `DRAW BEZIER 4x4`, four textured +`DRAW PRIM TRI_STRIP` - the wave background and icon quads), 41 texture binds, and full +PROJECTION/World/VIEW transform setup. `opening_plugin_module`, `sceMpegVsh_library` and +`impose_plugin_module` load and start. + +**It reaches the interactive XMB.** Up to 2026-08-19 it showed the logo and wave animation and then +a full-screen red error screen, which turned out to be the XMB index failing to decrypt (below). +With that fixed, and with the disc region check answered (above), the shell opens on a dismissable +disc error and the menu behind it works. + +Note that framebuffer readback does not work under headless on either backend - +`gpu.buffer.screenshot` and `gpu.buffer.renderColor` both answer "Could not download output" - so +checking what is actually on screen means running the app build. From headless, the useful proxy is +the per-frame display list signature (see the red error screen section): a screen that is finished +changing repeats byte-identically, and a live menu does not. + +## The red error screen + +What is known, all measured from a 40-emulated-second `--vsh` boot: + +- **Nothing fails to load.** Every `sceIoOpen` in the whole boot succeeds: `flash0:/font/*.pgf`, + `opening_plugin.rco`, `system_plugin.rco` / `_bg` / `_fg`, `topmenu_plugin.rco`, + `topmenu_icon.rco`, `impose_plugin.rco`, and `flash0:/vsh/etc/index_02g.dat`. The only I/O + failure in the entire trace is `flash1:/registry/init.dat`, which is correct (see above). So VSH + genuinely gets as far as building the top menu before giving up. +- **Where it freezes.** Grouping `sceGeListUpdateStallAddr` by display list id gives a per-frame + signature of what is being drawn. It grows 6 -> 10 -> 11 -> 15 -> 19 -> 33 -> 34 -> 38 stall + points (the dialog and its text being composed) and then repeats **byte-identically for the + remaining ~2016 frames**. That is the error screen locking in; everything before it is animation. +- **Nothing logs an error in the run-up.** The window before the freeze is just the startup jingle + decoding (`sceAtracDecodeData` + `sceAudioOutputPannedBlocking`), `vshCtrlReadBufferPositive`, + `scePowerIsSuspendRequired`, `sceKernelGetUserLevel` and `sceImposeChanges`. So the trigger is a + *wrong return value* we hand back, not a missing call - which makes it much harder to spot than + the `sceKernelLoadModuleVSH` bug was. +- **`sceImposeChanges` is not a symptom of the error.** It runs once a frame regardless; the + ~10000 calls per boot are just the indicator refresh, and they start *after* the frame loop is + already steady. Don't read into it. + +### What actually triggers it: the XMB index can't be decrypted + +Found by raising log channels for a narrow window around the transition (5.8s to 6.7s emulated, +~12K lines - `log.channel.set`, not a full-boot log). The frame loop's calls before and after the +transition differ sharply: before it there is resource loading, module starting and registry +access, after it only the clock and battery indicators. Inside that window: + +``` +sceIoOpen(flash0:/vsh/etc/index_02g.dat) -> fd 8 +sceIoRead(8, 092a2d40, 496) -> the whole file +sceIoClose(8) +unresolved import sceResmgr/9dc14891, called from 'vsh_module' <- traps here +sceKernelExitDeleteThread(1) <- the ScePafJob building the menu gives up +``` + +`index_02g.dat` is the index of what the XMB shows, and it is encrypted (it starts `PSPsysGP`, not +the `release:` of an already-plaintext one). `sceResmgr_9DC14891` is what decrypts it. With that +import unresolved the call trapped, the index stayed encrypted, and the job thread quit - hence a +shell that has loaded every resource successfully and still has nothing to show. + +**Fixed.** `Core/HLE/sceResmgr.cpp` implements the call, the three tags it needs +(`0x0B2B90F0/91F0/92F0`, keys and code `0x5C` from JPCSP) are in `Core/ELF/PrxDecrypter.cpp`, and +`pspDecryptType9()` there decrypts it - 496 bytes in, 159 out, starting `release:`, which is the +check `sceResmgr` logs on every call. + +Type 9 turned out to be type 6 with three differences, all because a type 9 file carries a real +ECDSA signature at `0x104..0x12C` where type 6 has nothing: + +- The "this region must be empty" check has to stop at `0x104` rather than `0x10C`, or the first + 8 bytes of the signature fail it (this was the original `-2`). +- The signature is excluded from the hashed header rather than fed into it - JPCSP zeroes + `buf2[0x34..0x5C)`, which is the same range after its header rearrangement, so `PRXType9` simply + leaves that field zero. +- `ecdsa_hash` in the KIRK CMD1 header stays 0. Type 6/7 set it; the branch type 9 takes writes + only the mode word. Setting it was the final `-4`. + +The SHA1 check inside the decrypt is a good progress signal while porting: reaching `-4` means the +header reconstruction and hash inputs were already right and only the KIRK call was wrong. + +## The root cause of the long-standing blank screen + +**`ModuleMgrForKernel/0xD5DDAB1F` (`sceKernelLoadModuleVSH`) was unimplemented.** That is how VSH +loads its own plugins: the XMB's UI lives in `flash0:/vsh/module/*_plugin.prx` and `vshmain` +pulls those in through this kernel call, not the user-mode `sceKernelLoadModule`. The import went +unresolved, vshmain got no module id back, and the `sceKernelStartModule` that followed was called +with **id 0** and failed with `0x8002012E UNKNOWN_MODULE`. No plugin ever ran, so the scene had +its containers but nothing drawable in them - which is exactly the "render state set up 6 times +per frame, zero draws" symptom this investigation chased for a long time. + +Implemented along with `0xD86DD11B` `sceKernelSearchModuleByName`, the other unresolved +`ModuleMgrForKernel` import. + +Found by scanning a whole boot for *any* HLE call returning an error. Exactly one mattered, and it +had been missed on two earlier passes because it logs as `: error 8002012e` rather than as a +`8xxxxxxx=sceSomething(` return value. **Grep both shapes.** + +## Solved along the way + +- **Real kernel driver modules running as genuine MIPS code** needed scratchpad RAM kernel-mode + address mirrors (`Core/MemMap.cpp`), dummy COP0 instructions (`Core/MIPS/Interpreter.cpp`), and + a GPIO + Syscon serial MMIO model (`Core/HW/GpioMMIO.{h,cpp}`) so `kd/syscon.prx`'s real + handshake completes instead of spinning. +- **`vsh_module` alarm-task SIGSEGV**: a targeted data patch zeroing an "alarm category count" + field nothing else initializes (`__KernelLoadELFFromPtr`). Symptom patch, not a principled fix - + why real firmware never reaches that path was never established. +- **`SCE_KERNEL_ERROR_NO_MEMORY` in the app build**: the 4MB kernel pool was exhausted by PPGe's + ~2MB overlay texture (now skipped when booting VSH) plus 11 driver modules each asking for a + 256KB thread stack (now given 32KB). +- **`sceHprm_driver/sceHprmReadLatch` (0xE9B776BE)** implemented and its output struct actually + zeroed, matching JPCSP. Correct, but did not affect the blank screen. +- **flash1 is now mounted** when a dump sits next to the flash0 one. `sceReg` serves a compiled-in + snapshot of some real PSP's registry (`Core/HLE/sceReg.cpp`) rather than reading the volume, and + a genuine dump has only `registry/system.dreg` and `.ireg`. **`flash1:/registry/init.dat` must + stay absent** - an earlier note here said creating one "changes nothing", which is wrong. VSH + stats it twice during startup, and *finding* it means "reinitialize the registry": it then calls + `sceRegRemoveRegistry` and `sceIoDevctl("flashfat1:", 0x5802)`, both `UNIMPL`, and the boot dies + without starting a single plugin module. Its absence is the healthy path, and the resulting + `FILE NOT FOUND` in the log is expected, not a lead. +- **A diagnostic-tooling bug that produced a phantom lead.** The "unresolved import" reporter + encoded identity in a 55-slot ring buffer, but a VSH boot registers ~1000 unresolved imports, so + the ring wrapped ~18 times and every report named whatever most recently reused that slot. It + made `sceMgVideo_driver` look like one massively hot call when it was the misattributed sum of + many. Fixed by recovering `stubAddr` via `g_lastSyscallPC` and looking identity up in + `module->importedFuncs`, which already tracked exactly this (interpreter-only, matching the + original limitation). **Lesson: when identity has to be recovered later by address, check + whether something already tracks it before adding a cache.** Breakpoints were never at fault - + if one doesn't fire, suspect the address first. + +## Imports that must stay unresolved + +Counterintuitive but repeatedly measured: implementing more of what the VSH imports can make the +boot *worse*, because a correct implementation lets a real flash0 driver finish its init and walk +on into hardware PPSSPP does not emulate. Leaving the import unresolved makes the driver fail early +and the boot continues. All three are commented in the source at the point where they'd be added: + +- **`ThreadManForKernel` mutex/fpl NIDs** (`0xB7D098C6` `sceKernelCreateMutex`, `0xB011B11F` + `sceKernelLockMutex`, `0x6B30100F` `sceKernelUnlockMutex`, `0xD979E9BF` `sceKernelAllocateFpl`) - + we implement all four for user mode already. Resolve them and the NAND and ID storage drivers + init successfully, then `sceIdStorage_Service` polls the NAND controller at `0xbd101300` forever. + No module starts. +- **`InterruptManagerForKernel` interrupt registration** (`0x58DD8978`, `0xF987B1F0`, `0x4D6E7305`, + `0xD774BA45`) - JPCSP can honour these because it emulates the interrupt controller as MMIO. We + dispatch the few interrupts we emulate ourselves (`__RegisterIntrHandler`), so success is a lie + the drivers act on. Measured: 31 such calls (interrupts 4, 12, 15-18, 20-24, 31), then a stall in + GE list execution with no plugin module started. + +73 unresolved import hits over 37 distinct module/NID pairs remain in a boot. Most of the rest are +`sceSysEventForKernel`, `sceSuspendForKernel` and `*_driver` modules that would need real hardware +behind them. Note the *runtime* ones log as `Unknown syscall (run) at ...: unresolved import ...` +(`HLE.cpp:905`) - a different string from the load-time `Unknown syscall from known HLE module`, +so grep for both or you will undercount badly. + +## How this boot path works + +Real hardware boots VSH through the kernel's own module bootstrap, launched from +`flash0:/reboot.bin`. PPSSPP doesn't emulate that chain. `--vsh` loads +`flash0:/vsh/module/vshmain.prx` through the normal PRX loader, and +`LoadAndStartVshKernelModules()` (`Core/HLE/sceKernelModule.cpp`) approximates the missing +bootstrap by loading and starting `vshbridge.prx`, `paf.prx`, `common_gui.prx`, `common_util.prx` +and 11 real `kd/` drivers first, in the order JPCSP's own `--vsh` shortcut uses. + +JPCSP's `--vsh` turns out to use the same shortcut rather than a real reboot.bin LLE path, so this +is a legitimate if approximate route to a working VSH. + +Known differences from JPCSP's `--vsh` that have **not** been tried: it preloads +`PSP_MODULE_AV_VAUDIO`, `AV_ATRAC3PLUS` and `AV_AVCODEC`, and sets the io filepath to +`ms0:/PSP/GAME`. Its trick of forcing the root thread to kernel mode at priority 0x7E **was** +tried and reverted - it reorders scheduling enough that `sceVshBridge_Driver` runs before a +precondition is ready. Firmware version already matches (660 both sides). + +## Thread structure + +There is no `vsh_main` thread and there shouldn't be. `vshmain.prx`'s `module_start` runs on the +`root` thread, creates and starts `SCE_VSH_GRAPHICS` (entry `08818d14`) and returns from module - +visible in the log as `Context switch: root -> ... (returned from module)` right after the +`sceKernelStartThread`. **`SCE_VSH_GRAPHICS` is VSH's main thread**, and its entry is the +outermost frame of every render backtrace. + +Steady state is 8 threads: `SCE_VSH_GRAPHICS`, `ScePafThread`, `SceWaveMain`, `SceWlanMac`, +`sceRTC_Service`, `sceSYSCON_Driver`, `idle0`, `idle1`, plus transient `ScePafJob` threads that +load the `.rco` resources and exit. The frame loop is healthy: poll `ScePafSyncCall`, +`vshCtrlReadBufferPositive`, render, `sceDisplaySetFrameBuf`, wait `SceVblankSync`. + +## The render call chain + +Still worth having if individual widgets misbehave. Traced with a **write memcheck on a display +list word** (`memory.breakpoint.add address=... write=true`), which stops with a structured hit +and lets `hle.backtrace` name the whole chain - minutes of work, and the technique to reach for +first next time: + +``` +08818d14 SCE_VSH_GRAPHICS thread entry + 08895544 6-iteration loop + 08963cfc container: sets base render state, walks children at [obj+0x550], count [obj+0x554] + 08950754 per-child render + 089a1cdc / 089a36e4 / 089a38c0 ... GE state helpers +``` + +`08950754` gates on `z_un_089501cc(child+0x450) == 1` and on `[child+0x438] != 0`, then computes +the scissor by intersecting `[child+0x43c..+0x448]` with a parent clip rect. + +## Tools and methodology + +- Everything useful works on a live session: `hle.thread.list`, `hle.eventflag.list`, + `hle.backtrace`, `memory.disasm`, `memory.readString`, `gpu.displaylist.disasm`, `cpu.evaluate` + (expressions like `[s0+0x438]` read guest memory), and memory breakpoints. See + `docs/WebSocketDebugger.md` for the catalog. +- `cpu.runUntilTime us=N` is what makes a script reproducible - it stops on the requested emulated + microsecond, so every run reaches the same instruction. Ask for the time you need and no more; + the red screen locks in around 6 emulated seconds. +- Raise log channels for a *short* window only - `sceGe`/`G3D` at debug produce ~200K lines in + three seconds of boot. `log.channel.set` is session-only, never persisted. `--loglevel=3` is a + good default for the whole run: bare `--log` on a VSH boot writes ~2.3GB, level 3 writes ~66KB. +- Diffing against JPCSP (`../JPCSP`, Java source, NIDs searchable by hex) repeatedly beat guessing + at missing HLE functions. `assets/flash0` has the decrypted PRXs if a NID needs disassembling. +- Headless is preferred over the app build - the app's Debug config blocks on a Windows crash + dialog with no console output. + +**Emulation is not the slow part.** A VSH boot runs at roughly real time even in a Debug build +(1 emulated second in a 2.5s run that includes process startup). If a scripted run takes minutes, +it is waiting, not emulating - and the usual cause is an explicit `:wait cpu.stepping` after a +`cpu.runUntil*`/`cpu.resume`/`cpu.step*`, which `--sync` already waits for. The redundant one waits +for a second `cpu.stepping` that never comes and burns the whole `--sync-timeout`. See +`Tools/wsdbg/README.md`. + +## Not included on this branch + +The separate `--vsh-reboot` experiment (loading `flash0:/reboot.bin` for a real LLE kernel boot, +with NAND/KIRK/system-control MMIO emulation) lives on `vsh-experiments`. The direct HLE path here +reaches much further without any of it.