| 961b3c61 Andy McCall |
1 |
# retro-remote-debug-controller |
| 2 |
|
| 3 |
A tiny, portable **HTTP control API for retro-platform emulators**, and the |
| 4 |
shared tooling that speaks it. One contract → one test harness across every |
| 5 |
Doomsday One port. |
| 6 |
|
| 7 |
An emulator launched with a control-port flag exposes a localhost HTTP server: |
| 8 |
|
| 67065eaa Andy McCall ↶ |
9 |
|
| 10 |
|
| 11 |
| `GET /status` | contract version, platform, frame counter, paused/running | 0.1 | |
| 12 |
| `GET /screenshot` | PPM image of the live screen | 0.1 | |
| 13 |
| `GET /mem?addr=&len=&bank=` | raw memory bytes | 0.1 | |
| 14 |
| `GET /regs` | CPU registers (JSON) | 0.1 | |
| 15 |
| `POST /step?frames=` | advance N frames deterministically | 0.1 | |
| 16 |
| `POST /pause` / `POST /resume` | halt / resume the machine | 0.1 | |
| 17 |
| `POST /key`, `POST /reset` | inject input / reset | 0.2 | |
| 18 |
| `POST /mem?addr=&bank=` | write (poke) memory | 0.3 | |
| 19 |
| `GET /audio` | drain synthesised PCM as a WAV | 0.3 | |
| 346ca3ee Andy McCall ↶ |
20 |
| `POST /pointer?x=&y=&buttons=` | inject a pointer move/click | 0.4 | |
| 21 |
| `GET /pointer` | read back pointer position + buttons | 0.4 | |
| bed0f1e5 Andy McCall ↶ |
22 |
| `POST /pad?index=&buttons=&connected=` | present a virtual controller, set what is held | 0.5 | |
| 23 |
| `GET /pad?index=` | read back a pad's presence + held buttons | 0.5 | |
| 8fd33bc5 Andy McCall ↶ |
24 |
| `GET /perf?windows=&frames=&since=` | performance metrics: the frame budget, work/idle/IRQ cycles, statistics windows, zones, raw frames | 0.6 | |
| 25 |
| `POST /perf` | performance settings (JSON body): target FPS, idle mode, history, zones and marker names | 0.6 | |
| 961b3c61 Andy McCall |
26 |
|
| 8fd33bc5 Andy McCall ↶ |
27 |
`/status` also lists `capabilities` from 0.6: the groups a server serves, |
| 28 |
whatever level it reaches. |
| 29 |
|
| 30 |
Current contract: **0.6.0**. Minors are purely additive (a missing capability |
| 67065eaa Andy McCall ↶ |
31 |
returns 501 and lowers the advertised contract), so a client reads |
| 32 |
`/status.contract` and fails fast only on a MAJOR mismatch. See [SPEC.md](SPEC.md). |
| 33 |
|
| 961b3c61 Andy McCall |
34 |
## Why |
| 35 |
|
| 67065eaa Andy McCall ↶ |
36 |
Every one of these ports (Pac-Man on X16, Neo6502, Agon, and NEC PC-FX; and |
| 37 |
whatever's next) is chasing **frame-exact** arcade fidelity, and every one had |
| 38 |
its own fragile, platform-specific way to check state headless (write to the SD |
| 39 |
card, dump 64 KB of RAM on a magic jump, grep stdout — which lies). This |
| 40 |
replaces all of that with one contract: |
| 961b3c61 Andy McCall |
41 |
|
| 67065eaa Andy McCall ↶ |
42 |
- **`GET /mem`** → assert game state (sprite table, actor positions); |
| 43 |
**`POST /mem`** pokes it back for setup. |
| 961b3c61 Andy McCall |
44 |
- **`GET /screenshot`** → Pillow → assert pixels, or just catch "nothing |
| 45 |
rendered" (the exact class of bug that costs an afternoon otherwise). |
| 46 |
- **`POST /step`** → deterministic, frame-addressable capture, so CI diffs don't |
| 47 |
flake against a free-running 60 Hz loop. |
| 67065eaa Andy McCall ↶ |
48 |
- **`POST /key`** → drive input; **`GET /audio`** → assert the sound a stepped |
| 49 |
window produced. |
| 961b3c61 Andy McCall |
50 |
|
| 51 |
The payoff is uniformity: the same `emu_control.py` and the same pytest patterns |
| 67065eaa Andy McCall ↶ |
52 |
drive the X16, Neo6502, Agon, and PC-FX emulators. |
| 961b3c61 Andy McCall |
53 |
|
| 54 |
## Layout |
| 55 |
|
| 56 |
``` |
| 57 |
SPEC.md the normative HTTP contract (start here) |
| 58 |
core/retro_control.h portable backend vtable each emulator implements |
| 67065eaa Andy McCall ↶ |
59 |
core/retro_control.c shared server: sockets, HTTP, PPM/JSON, marshalling |
| 60 |
client/emu_control.py shared pytest client (stdlib; Pillow optional) |
| 61 |
conformance/ a platform-agnostic pytest that proves an emulator conforms |
| 62 |
CHANGELOG.md the additive contract history |
| 961b3c61 Andy McCall |
63 |
``` |
| 67065eaa Andy McCall ↶ |
64 |
|
| 65 |
## Adopting it in an emulator |
| 66 |
|
| 67 |
Two first-class paths (both normative in [SPEC.md](SPEC.md#adoption-paths)): |
| 961b3c61 Andy McCall |
68 |
|
| 67065eaa Andy McCall ↶ |
69 |
- **Vendor the shared core** — for C/C++ emulators. Add this repo as a git |
| 70 |
submodule, compile `core/retro_control.c`, implement the backend callbacks, |
| 71 |
and wire three loop hooks. The socket loop, HTTP parsing, PPM/JSON encoding |
| 72 |
and pause/step control come for free. |
| 73 |
- **Conform only** — for emulators in other languages (the FAB Agon emulator is |
| 74 |
Rust). Keep your own HTTP server; just match the contract and pass the |
| 75 |
conformance suite. Sharing the C core is an optimization, not a requirement. |
| 961b3c61 Andy McCall |
76 |
|
| 67065eaa Andy McCall ↶ |
77 |
The vendor path is one backend struct plus three hooks: |
| 961b3c61 Andy McCall |
78 |
|
| 79 |
```c |
| 80 |
#include "retro_control.h" |
| 81 |
|
| 346ca3ee Andy McCall ↶ |
82 |
/* Four callbacks satisfy 0.1; add inject_key/reset for 0.2, |
| bed0f1e5 Andy McCall ↶ |
83 |
write_mem/capture_audio for 0.3, set_pointer/get_pointer for 0.4, |
| 8fd33bc5 Andy McCall ↶ |
84 |
set_pad/get_pad for 0.5, get_perf/set_perf for 0.6. |
| 346ca3ee Andy McCall ↶ |
85 |
A NULL callback 501s its endpoint. */ |
| 67065eaa Andy McCall ↶ |
86 |
static const retro_control_backend_t backend = { |
| 87 |
.platform = "x16", |
| 88 |
.emulator = "x16emu", |
| 89 |
.read_mem = x16_read_mem, /* debug-read, no side effects */ |
| 90 |
.get_regs_json = x16_get_regs_json, |
| 91 |
.get_framebuffer = x16_get_framebuffer, |
| 92 |
.get_frame_count = x16_get_frame_count, |
| 93 |
.inject_key = x16_inject_key, /* 0.2 */ |
| 94 |
.reset = x16_reset, /* 0.2 */ |
| 95 |
.write_mem = x16_write_mem, /* 0.3 */ |
| 96 |
.capture_audio = x16_capture_audio, /* 0.3 */ |
| 346ca3ee Andy McCall ↶ |
97 |
.set_pointer = x16_set_pointer, /* 0.4 */ |
| bed0f1e5 Andy McCall ↶ |
98 |
.set_pad = x16_set_pad, /* 0.5 */ |
| 346ca3ee Andy McCall ↶ |
99 |
.get_pointer = x16_get_pointer, /* 0.4 */ |
| 8fd33bc5 Andy McCall ↶ |
100 |
.get_perf = x16_get_perf, /* 0.6: malloc'd JSON */ |
| 101 |
.set_perf = x16_set_perf, /* 0.6 */ |
| 961b3c61 Andy McCall |
102 |
}; |
| 103 |
|
| 67065eaa Andy McCall ↶ |
104 |
if (control_port) /* set by -controlport / --control-port / env */ |
| 961b3c61 Andy McCall |
105 |
retro_control_start(control_port, &backend); |
| 106 |
``` |
| 107 |
|
| 67065eaa Andy McCall ↶ |
108 |
Stepping and pausing are driven cooperatively from the emulator's main loop, not |
| 109 |
by a callback — three hooks at a frame boundary: |
| 110 |
|
| 111 |
```c |
| 112 |
retro_control_service(); /* run a pending request on the emu thread */ |
| 113 |
if (!retro_control_running()) { idle; } /* gate advancing the machine (pause/step) */ |
| 114 |
... run one frame ... |
| 115 |
if (frame_completed) retro_control_on_frame(); /* tick the /step budget */ |
| 116 |
``` |
| 961b3c61 Andy McCall |
117 |
|
| 118 |
## Status |
| 119 |
|
| 398756c0 Andy McCall ↶ |
120 |
Contract **0.5.0**, implemented across **four** emulators. The shared server and |
| 121 |
the conformance suite (22 tests) are complete; tests gated *above* the server's |
| 122 |
advertised level skip, so a 0.1-only backend still passes. |
| 123 |
|
| 124 |
Reference implementation: the Commander X16 fork (`x16-emulator`), which vendors |
| 125 |
`core/` as a git submodule; the NEC PC-FX fork (Mednafen) does the same, and the |
| 126 |
FAB Agon emulator conforms directly in Rust. |
| 127 |
|
| 128 |
> **Known gap — cumulative levels vs. absent devices.** A level is advertised |
| 129 |
> only when *all* its callbacks are present, so a platform that lacks a device |
| 130 |
> cannot advertise past it without supplying a stub. The PC-FX has no pointing |
| 131 |
> device: it stubs `/pointer` to 400 in order to advertise **0.5.0** for the |
| 132 |
> `/pad` support it genuinely has — but the suite gates the pointer tests on |
| 133 |
> `minor >= 4`, so they then *run* and fail. Measured against the PC-FX fork: |
| 134 |
> **19 passed, 3 failed**, the three being pointer tests. A single cumulative |
| 135 |
> integer cannot express "has pad, has no pointer"; this needs either a |
| 136 |
> per-capability declaration in `/status` or a conformance rule that treats a |
| 137 |
> documented "no such device" 400 as a skip. |