A PlayStation Portable emulator written in pure Python.
PySP boots PSP homebrew (ELF, relocatable PRX, EBOOT.PBP) and runs it on an interpreted Allegrex CPU with a software-rendered graphics engine. The whole machine is ordinary Python on top of numpy. That makes it slow by emulator standards, and very easy to read and single-step, which was the point: it started as a way to understand how the hardware fits together and turned into something that actually draws frames.
Paper PSP by Jerome Berg, MIT
licensed homebrew, running from its released EBOOT.PBP at the native 480 by
272. The blue trail is following the d-pad; the other squares are bots. Both
frames came out of --screenshot.
The same game in the window frontend:
- The MIPS core: the integer set with the Allegrex extras (
min/max,ext/ins, bit shuffles), single-precision COP1, and the VFPU with its matrix register file and operand prefixes - Loading: ELF, PRX with relocation, PBP containers with PARAM.SFO
- The kernel: threads with PSP scheduling rules, semaphores, event flags, mutexes, pools, callbacks, alarms-by-delay, and a cycle-accurate event queue
- Graphics: display list execution with a numpy rasteriser. Triangles, sprites, texturing (palettes included), depth, blending, scissor
- I/O: a mounted
ms0:backed by a host directory, ISO 9660 images ondisc0:, controller input, audio mixing into a ring buffer - Save states, a PNG screenshotter, an interactive debugger and a pygame window
There's no JIT, no media decoding (the Atrac3 and MPEG stubs just answer politely) and no network. Retail titles are encrypted and PySP doesn't decrypt them, so in practice this is a homebrew machine.
Python 3.10 or newer.
pip install -e . # emulator + numpy
pip install -e .[gui] # add pygame for the window
pip install -e .[dev] # add pytest and ruff
pysp game.pbp # windowed
pysp game.pbp --headless --frames 300 --screenshot out.png
pysp game.pbp --debug # start in the debugger
pysp game.pbp --slim # 64 MB model
pysp-disasm game.pbp --imports # what does it call?
ms0: maps to ./memstick unless --memory-stick says otherwise. Logging
is per subsystem: --log info,ge=debug turns the graphics engine up without
drowning in everything else.
Keys in the window: arrows for the pad, z/x/a/s for cross, circle,
square and triangle, q/w for the triggers, enter and space for start and
select, ijkl for the analogue stick.
pysp game.pbp --debug drops you at a prompt before the first instruction:
pysp> break sceDisplaySetFrameBuf
pysp> go
pysp> regs
pysp> disasm pc 8
pysp> threads
Breakpoints take addresses or HLE function names, state summarises the
kernel, save/load snapshot the whole machine.
The macos app workflow builds a self-contained PySP.app with PyInstaller
on every tag (or by hand from the Actions tab) and attaches the zip to the
release. Opened from Finder it asks for an executable with a file picker;
from a terminal it takes the same arguments as pysp.
pytest -q # the whole suite
ruff check . # lint
Tests assemble their own PSP executables with the built-in MIPS assembler, so
the suite goes through the real path (stub tables, syscalls, display lists)
without shipping binaries. docs/architecture.md has the tour.
MIT.



