| name | psp-emulator-debug |
| description | Drive the PPSSPP emulator on macOS to visually debug the PSP runtime (runtime/) โ build an EBOOT for a chosen game, launch it, screenshot the render, and diagnose crashes/hangs/low-FPS without the user's eyes. Use when a PSP build "boots to black", crashes, renders wrong, or runs slow, and you need to SEE the screen or read the emulator's logs yourself. macOS + PPSSPPSDL only. |
Debugging the PSP runtime via PPSSPP (macOS)
This project's runtime/ is a Rust + QuickJS PSP app. You can build an EBOOT for any
game and run it in PPSSPP yourself โ launching it, screenshotting the framebuffer with
macOS screencapture, and reading the emulator's crash reports + logs. Use this to
diagnose "boots to black", crashes, wrong rendering, and low FPS without asking the
user to look.
Build + launch one game
DREAMCART_GAME=outdoor3d.js bun runtime/build.ts
strings runtime/target/mipsel-sony-psp/debug/EBOOT.PBP | grep -c "OUTDOOR 3D"
Two launch modes:
pkill -x PPSSPPSDL; sleep 1
open -a PPSSPPSDL runtime/target/mipsel-sony-psp/debug/EBOOT.PBP
( /Applications/PPSSPPSDL.app/Contents/MacOS/PPSSPPSDL "$ABS_PATH_TO_EBOOT" \
>/tmp/ppsspp.log 2>&1 & )
sleep 16; pkill -x PPSSPPSDL
Always use an absolute path to the EBOOT for the CLI form.
Screenshot the render
sleep N
open -a PPSSPPSDL
sleep 2
screencapture -x -o /tmp/shot.png
Then Read /tmp/shot.png. Other apps constantly steal focus and occlude PPSSPP. The
reliable fix is to HIDE every other app right before capturing (PPSSPP becomes the only
visible window):
osascript -e 'tell application "System Events" to set visible of (every process whose \
background only is false and name is not "PPSSPPSDL") to false' 2>/dev/null
sleep 0.5; screencapture -x -o /tmp/shot.png
Caveat: that can also hide/close PPSSPP itself on some setups โ if the shot is the bare
desktop, open -a PPSSPPSDL to bring it back. Do hide+capture in ONE command so nothing
reactivates in between.
Boot timing (do not mistake "slow" for "hung")
QuickJS evaluating a large bundle + decoding base64 assets + heavy onEnter work is
SLOW on the emulated 333 MHz core. Typical waits before the first frame renders:
- Small 2D / simple 3D bundle (<100 KB): ~6โ8 s
- Textured glTF game (car ~500 KB): ~8โ12 s
- Skinned Fox / terrain-gen scene (~400โ700 KB): 20โ30 s
If pgrep -x PPSSPPSDL still shows it RUNNING, it's booting, not hung. Only treat it
as a crash if the process is gone AND a fresh .ips crash report appeared.
Diagnose a crash
PPSSPP crashing natively writes a macOS crash report:
latest=$(ls -t ~/Library/Logs/DiagnosticReports/PPSSPPSDL-*.ips | head -1)
python3 -c "import json; raw=open('$latest').read(); d=json.loads(raw[raw.find(chr(10))+1:]); \
print(d['exception']); th=d['threads'][d['faultingThread']]; \
imgs=d['usedImages']; \
[print(imgs[f['imageIndex']]['name'] if f.get('imageIndex') is not None else '?', f.get('symbol','')) for f in th['frames'][:8]]"
A fault at host 0x0000000300000000 inside MIPSState::RunLoopUntil (JIT, no symbol)
means the GUEST jumped to its address 0 (PPSSPP maps guest RAM base at 0x300000000) โ
i.e. guest memory corruption / a null write. The CLI stdout log usually names the
real cause; grep it:
grep -iE "too many|Unable to alloc|partition|invalid|jump to|0=sceKernel" /tmp/ppsspp.log
In-game instrumentation
log(msg) (bridge.rs) writes to the PSP debug screen overlay (via psp::dprintln!),
NOT to PPSSPP stdout โ so you can't grep it; you must screenshot the screen, and it
floods. Prefer on-screen HUD you control:
now() (bridge.rs, ยตs from sceKernelGetSystemTimeWide) lets a game compute real
frame time / FPS (the engine dt is a FIXED timestep, not wall-clock). Use the
reusable Fps helper (framework/src/fps.ts: fps.sample() per frame, draw
fps.value). NOTE: PPSSPP's emulation speed depends on the host Mac's spare CPU, so
absolute FPS readings are NOISY (can vary ~2ร between captures) โ trust the relative
before/after and the on-device draw-count, not a single number.
- To isolate a cost, patch the gitignored bundle in
runtime/src/game/<game>.js
directly (sed/python), rebuild, measure; bun run bundle restores it from source.
Toggle one thing at a time (node count, lighting, fog, HUD text) and read the FPS.
Pitfalls (learned the hard way)
osascript ... keystroke / key code opens PPSSPP's pause menu or bounces focus
to another app. To re-focus PPSSPP, use open -a PPSSPPSDL (no file arg) โ it brings
the window forward without sending keys. Avoid sending keystrokes unless you mean to.
- Stale EBOOT: if a screenshot shows the WRONG game, the build didn't relink โ
rebuild explicitly and re-verify with
strings ... | grep "<TITLE>".
- Enable PPSSPP's own FPS counter only as a fallback: set
ShowFPSCounter = 3
under [Graphics] in ~/.config/ppsspp/PSP/SYSTEM/ppsspp.ini while PPSSPP is NOT
running (it rewrites the ini on exit). It renders small in the emulated screen's
corner โ an in-game HUD via now() is more reliable to read.
- Always
pkill -x PPSSPPSDL before relaunching so you measure a fresh boot.