|
| 1 | +# ppsspp-debug |
| 2 | + |
| 3 | +A typed client for [PPSSPP](https://www.ppsspp.org/)'s WebSocket debugger: read and write memory, |
| 4 | +set breakpoints and watchpoints, pause and step the CPU, press buttons. `Client` blocks, |
| 5 | +`AsyncClient` is for asyncio; both have the same methods. Nothing in it is specific to one |
| 6 | +game. |
| 7 | + |
| 8 | +```python |
| 9 | +from ppsspp_debug import Client |
| 10 | + |
| 11 | +with Client.connect() as ppsspp: # finds the PPSSPP running on this machine |
| 12 | + ppsspp.read_u32(address) |
| 13 | + with ppsspp.paused(): # one stop for many reads, all from the same frame |
| 14 | + data = ppsspp.read(address, 0x100) |
| 15 | + regs = ppsspp.registers() |
| 16 | + with ppsspp.watchpoint(address, 2) as hits: # removed when the block ends |
| 17 | + hit = hits.next(timeout=30) # the CPU is now stopped at the writer |
| 18 | + print(hex(hit.pc), ppsspp.registers()["a0"]) |
| 19 | + ppsspp.resume() |
| 20 | +``` |
| 21 | + |
| 22 | +```python |
| 23 | +from ppsspp_debug import AsyncClient |
| 24 | + |
| 25 | +async with AsyncClient.connect(port=12345) as ppsspp: |
| 26 | + async with ppsspp.breakpoint(address, stop=False, log_format="a0={a0}") as calls: |
| 27 | + async for call in calls: # the game keeps running |
| 28 | + print(call.message) |
| 29 | +``` |
| 30 | + |
| 31 | +PPSSPP opens the debugger at startup only with `RemoteDebuggerOnStartup = True` in its |
| 32 | +`ppsspp.ini`, or from Settings > Tools > Developer tools. |
| 33 | + |
| 34 | +## What it handles for you |
| 35 | + |
| 36 | +| PPSSPP (1.20.4) | The client | |
| 37 | +|---|---| |
| 38 | +| Sends its log only to the newest debugger connection, and stops it for all when any connection closes | Log streams get a connection of their own, opened when the stream starts | |
| 39 | +| Does not say why the CPU stopped | Matches a breakpoint stop by its pc; counts as a watchpoint hit any stop this client did not ask for | |
| 40 | +| Answers `cpu.stepping` and `cpu.resume` with a broadcast, not a reply; pausing a paused CPU answers nothing | `pause()` and `resume()` wait for the broadcast, and are fine to call twice | |
| 41 | +| Stops and restarts the CPU for every memory request while the game runs (about 10 ms each) | `paused()` makes each request about 0.3 ms; one `read()` of a whole struct costs one stop | |
| 42 | +| Keeps breakpoints armed after the script that set them has died | `breakpoint()` and `watchpoint()` remove theirs when the block ends, however it ends, and let the game run if a hit arrived that nobody took | |
| 43 | +| Reports errors as events, and an unknown event as an error | Raises `DebuggerError`, or `Unsupported` for an event this PPSSPP does not have | |
| 44 | +| Moves to a random port when its own is busy, and saves that port to `ppsspp.ini` | Reads the port from the PPSSPP process: `find_debuggers()`, `LocalEmulator.port()` | |
| 45 | +| Stalls the WebSocket handshake while it announces itself to report.ppsspp.org at startup | `connect()` keeps trying until its timeout (30 s by default) | |
| 46 | +| Sends input and game events unasked, and buffers the log while it is switched off | Switches on only the kinds an open stream wants, and drops what was held back | |
| 47 | + |
| 48 | +There are no callbacks: events arrive as a stream read in your own code, so nothing the reader |
| 49 | +does can wait on a reply only the reader can deliver. |
| 50 | + |
| 51 | +## Starting PPSSPP |
| 52 | + |
| 53 | +```python |
| 54 | +from ppsspp_debug import Client, DockerEmulator, LocalEmulator |
| 55 | + |
| 56 | +with LocalEmulator("/path/to/PPSSPPSDL", "/path/to/game.iso") as emu: |
| 57 | + with Client.connect(port=emu.port()) as ppsspp: |
| 58 | + ppsspp.wait_for_game() |
| 59 | +``` |
| 60 | + |
| 61 | +`DockerEmulator(container)` drives PPSSPP in the modkit's headless container through its |
| 62 | +`ppsspp-ctl`, and relaunches it when it dies right after reporting ready. A `state=` launch |
| 63 | +skips the cold boot, so PRX plugins do not load. |
| 64 | + |
| 65 | +## Patched PPSSPP |
| 66 | + |
| 67 | +`save_state()`, `load_state()` and `speed()` need the PPSSPP patches that ship with the modkit; |
| 68 | +stock PPSSPP raises `Unsupported`. |
| 69 | + |
| 70 | +`screenshot()` depends on the GPU backend: PPSSPP 1.20.4 with Vulkan answers "Could not |
| 71 | +download output" whether the game runs or is stopped. |
| 72 | + |
| 73 | +## Tests |
| 74 | + |
| 75 | +`ppsspp_debug.testing.FakePPSSPP` is a debugger server with PPSSPP's quirks, for testing code |
| 76 | +that uses this client without an emulator. |
| 77 | + |
| 78 | +The live tests run against a real PPSSPP when `PPSSPP_BINARY` and `PPSSPP_GAME` are set, and |
| 79 | +print what they measure with `-s`: |
| 80 | + |
| 81 | +```bash |
| 82 | +PPSSPP_BINARY=/path/to/PPSSPPSDL PPSSPP_GAME=/path/to/game.iso uv run pytest packages/ppsspp-debug -s |
| 83 | +``` |
0 commit comments