Retro Remote Debug Controller
A tiny, portable HTTP control API for retro-platform emulators, and the shared tooling that speaks it. One contract → one test harness across every Doomsday One port (Commander X16, Neo6502, Agon, PC-FX — and whatever's next).
An emulator launched with a control-port flag exposes a localhost HTTP server:
| Endpoint | Method | Returns |
|---|---|---|
/status | GET | contract version, platform, frame counter, paused state, (0.6) capabilities |
/screenshot | GET | PPM image of the live screen |
/mem?addr=&len=&bank= | GET | raw memory bytes |
/regs | GET | CPU registers (JSON) |
/step?frames= | POST | advance N frames deterministically |
/pause, /resume | POST | halt / free-run |
/key, /reset | POST | (0.2) inject a key; reset the machine |
/mem?addr=&bank= | POST | (0.3) write (poke) memory |
/audio | GET | (0.3) the sound since the last call, as a WAV |
/pointer | POST / GET | (0.4) move and click the pointer; read it back |
/pad?index= | POST / GET | (0.5) present a virtual game controller; read it back |
/perf | GET / POST | (0.6) performance metrics: how much of each frame the program uses, by phase; their settings |
How it fits together
flowchart LR
subgraph clients ["Clients"]
py["pytest + emu_control.py"]
curl["curl, scripts"]
agent["an agent, an IDE"]
end
subgraph emu ["The emulator process"]
subgraph srv ["server thread (retro_control.c)"]
http["127.0.0.1 socket loop<br/>HTTP parse · PPM / JSON / WAV encode"]
end
subgraph emut ["emulator thread"]
svc["retro_control_service()<br/>once a loop iteration"]
be["the backend's callbacks<br/>read_mem · get_framebuffer · get_regs_json<br/>inject_key · set_pad · get_perf …"]
mach["the machine"]
svc --> be --> mach
end
http -- "queued request" --> svc
svc -- "reply" --> http
end
py & curl & agent -- "HTTP" --> http
The server never touches machine state on its own thread: every read and
write runs inside retro_control_service() on the emulator's thread, so
what a client sees is one consistent instant of the machine.
The contract grows in levels, and a server advertises the highest one its
backend serves in full. /status.capabilities (0.6) lists each group it
serves, whatever the level:
flowchart LR
l1["0.1<br/>status · screenshot · mem · regs<br/>step · pause · resume"] --> l2["0.2<br/>key · reset"]
l2 --> l3["0.3<br/>mem writes · audio"] --> l4["0.4<br/>pointer"] --> l5["0.5<br/>pad"] --> l6["0.6<br/>perf · capabilities"]
Why this exists
Every Doomsday One retro port chases frame-exact arcade fidelity, and each one grew its own fragile way to inspect state headlessly — write to the SD card, dump 64 KB of RAM on a magic jump, grep stdout (which lies). This replaces all of that with one contract:
GET /mem→ assert game state (sprite table, actor positions).GET /screenshot→ Pillow → assert pixels, or just catch "nothing rendered" (the exact class of bug that otherwise costs an afternoon).POST /step→ deterministic, frame-addressable capture, so CI diffs don't flake against a free-running 60 Hz loop.GET /perf→ how much of each frame the program spends, and on what: a speed regression becomes a failing test instead of a player's complaint.
The payoff is uniformity: the same emu_control.py client and the same pytest
patterns drive all three emulators.
Pages
- HTTP Contract — the endpoints in detail (the normative
spec is
SPEC.mdin the repo). - Adopting in an Emulator — implement the backend and wire the loop hooks.
- Client & Testing — the shared pytest client and how to write reliable tests with it.
- Performance Metrics — (0.6)
/perf: profiling a program's frames from a test, and graphing them. - Per-Platform Notes — x16 / Neo6502 / Agon / PC-FX specifics.
- Reference Implementation (x16emu) — the worked example, with build and run recipes.
Repo
Source: doomsdayone/retro-remote-debug-controller.
Contract version 0.6.0. Reference implementations: the Commander X16 fork
(0.5.0) and the enhanced X16 emulator (0.6.0, with /perf).