OXYGENHUB
# 4c0b050e9a25 +6 retro-remote-debug-controller / SPEC.md
13.4 KiB · Markdown · last changed docs: full sweep to 0.3.0 / four platforms 3 months ago renderedsource blame history raw
1
# Retro Remote Debug Controller — HTTP Control Contract
2
3
**Contract version: 0.3.0** (semver; clients assert on the MAJOR). 0.3 adds
4
memory write (`POST /mem`) and an audio drain (`GET /audio`); 0.2 added input
5
injection (`POST /key`, `POST /reset`). Every minor is purely additive, so older
6
clients keep working and servers advertise the highest level whose callbacks are
7
all present via `/status.contract`.
8
9
A minimal, portable HTTP API that a retro-platform emulator exposes so an
10
external harness can screenshot the screen, read memory and registers, step the
11
machine deterministically, and inject input. The same contract is implemented
12
by the forked FAB Agon Emulator, the Neo6502 emulator, the Commander X16
13
emulator, and the NEC PC-FX (Mednafen) emulator, so **one** pytest harness
14
drives all four ports.
15
16
## Principles
17
18
- **Opt-in.** The server is OFF by default and starts only when the emulator is
19
  launched with a control-port flag. Zero cost to normal use.
20
- **Localhost only.** The server MUST bind `127.0.0.1` (never `0.0.0.0`).
21
  `/mem` and input injection are a memory-read / remote-control surface; they do
22
  not leave the machine.
23
- **The contract is the HTTP surface, not the flag.** Each emulator names its
24
  launch flag idiomatically (x16emu: `-controlport N`; FAB: `--control-port N`).
25
  Only the endpoints below are normative.
26
- **Determinism is a first-class requirement.** These are frame-exact ports.
27
  Every state read (`/mem`, `/regs`, `/screenshot`) MUST observe a consistent
28
  machine state — captured at an instruction/frame boundary on the emulator
29
  thread, never a torn mid-instruction read from the HTTP thread. See
30
  *Consistency* below.
31
32
## Conventions
33
34
- JSON responses are `application/json; charset=utf-8`.
35
- Binary responses use the stated content-type; no chunked encoding required.
36
- Errors: HTTP 4xx/5xx with body `{"error":"<message>"}`.
37
- Numbers in JSON are decimal unless a field is documented as a hex string.
38
- Methods are exactly as listed (`GET`/`POST`). Unknown path → 404. Unknown
39
  method on a known path → 405.
40
41
## Endpoints
42
43
### `GET /status`
44
Liveness + contract negotiation. Always available.
45
```json
46
{
47
  "contract": "0.3.0",
48
  "emulator": "x16emu",
49
  "platform": "x16",
50
  "frame": 12345,
51
  "paused": true,
52
  "running": true
53
}
54
```
55
- `contract` — this document's version. Clients MUST reject a mismatched MAJOR.
56
- `platform` — one of `x16`, `neo6502`, `agon`, `pcfx` (extensible). Tells the
57
  client the memory/register model (see *Per-platform* appendix).
58
- `frame` — monotonic completed-frame counter since boot. The determinism
59
  anchor: a screenshot taken at `frame == N` shows the state after N complete
60
  frames.
61
- `paused` — true if the machine is halted (only advances via `/step`).
62
63
### `GET /screenshot`
64
The live screen as a Portable Pixmap.
65
- Response: `image/x-portable-pixmap`, binary **PPM P6**, 8-bit RGB, no alpha.
66
- Pixels are the most recent COMPLETE frame (never a half-scanned frame). The
67
  emulator keeps a snapshot the render thread fills at frame end; the HTTP
68
  thread serves that snapshot.
69
- Dimensions are the platform's native framebuffer (appendix). Clients read
70
  `width`/`height` from the PPM header — do not hardcode.
71
- Reserved: `?frame=N` (block until frame N) — still reserved, not yet required.
72
73
### `GET /mem?addr=<a>&len=<n>[&bank=<b>]`
74
Raw memory bytes.
75
- `addr` — start address, decimal or `0x`-hex. Interpreted in the platform's
76
  address space (appendix).
77
- `len` — byte count, decimal. Servers MAY cap `len` (recommend ≥ 65536); a
78
  capped request returns what it can with `Content-Length` telling the truth.
79
- `bank` — OPTIONAL bank selector for banked platforms (X16). Omitted → the
80
  machine's currently-selected bank. Ignored by flat-memory platforms.
81
- Response: `application/octet-stream`, exactly the requested bytes.
82
- Reads go through the emulator's debug-read path (no side effects, no I/O
83
  triggers, no access-flag mutation).
84
85
### `POST /mem?addr=<a>[&bank=<b>]`  *(0.3)*
86
Write (poke) memory. The write-sibling of `GET /mem`; same address/bank model.
87
- `addr`, `bank` — as for the read form. `len` is NOT a query param: the byte
88
  count is the request **body** length.
89
- Body — `application/octet-stream`, the raw bytes to write starting at `addr`.
90
  Servers MAY cap the body (recommend ≥ 65536).
91
- Response: `{ "written": <n> }` — bytes actually written. A server MAY write
92
  fewer than requested (e.g. it stops at a ROM/unwritable boundary); the count
93
  tells the truth.
94
- Writes go through the emulator's CPU write path. Intended for RAM/state
95
  fixup (force lives, seed a level, restore a snapshot). Writing an I/O register
96
  MAY trigger device side effects — that is the caller's responsibility.
97
- Determinism: poke while `paused` so the machine can't overwrite between the
98
  write and a subsequent read.
99
100
### `GET /regs`
101
CPU registers as JSON. Keys are platform-specific (appendix); values are decimal
102
unless suffixed `_hex`. Example (x16 / 65C02):
103
```json
104
{ "a":0, "x":16, "y":2, "sp":505, "pc":49152, "status":32,
105
  "ram_bank":1, "rom_bank":0 }
106
```
107
108
### `POST /step?frames=<n>`  *(recommended; required for frame-exact tests)*
109
Advance the machine by exactly `n` complete frames (default 1), then halt.
110
- Only meaningful when the machine is paused (see `/pause`). On a free-running
111
  machine, servers SHOULD pause, run `n` frames, and stay paused.
112
- Response: `{ "frame": <new frame counter> }`.
113
- `POST /step?cycles=<n>` MAY be offered for sub-frame stepping.
114
115
### `POST /pause` / `POST /resume`  *(recommended)*
116
Halt / free-run. `{ "paused": true|false }`. A machine launched with a
117
`--control-pause`-style option SHOULD boot paused so tests are deterministic
118
from frame 0.
119
120
### `POST /key?text=<char> | ?code=<n>  [&down=0|1]`  *(0.2)*
121
Inject a key. Query params (like `/mem` and `/step`), no body:
122
- `text` — a single character, mapped to the platform's keyboard.
123
- `code` — a raw platform key code (e.g. fabgl virtual key on Agon, an SDL/PS2
124
  code on others). `text` and `code` are alternatives; provide one.
125
- `down` — `1` presses (holds), `0` releases; **omitted taps** (press then
126
  release). Holding across `/step` frames is how a program that samples input
127
  each frame (e.g. a game) actually sees the key.
128
- Response: `{ "injected": true }`. Missing/unmapped key → 400.
129
130
Injection plus `/step` retires scripted-autoexec input: press → step → assert →
131
release, deterministically.
132
133
### `POST /reset`  *(0.2)*
134
Soft/cold reset the machine. Response `{ "reset": true }`. The `frame` counter
135
MAY reset here (the one case `/status.frame` is allowed to go backwards).
136
137
### `GET /audio`  *(0.3)*
138
Drain the audio the emulator has synthesised since the last `/audio` call — the
139
audio analogue of `/screenshot`, but a *window* (everything since last drain)
140
rather than a snapshot.
141
- Response: `audio/wav`, a self-describing **PCM WAV** (RIFF, 16-bit signed,
142
  interleaved). The header carries the sample rate and channel count; clients
143
  read them from the WAV — do not hardcode (appendix lists native rates).
144
- The server keeps a bounded ring of mixed output samples, teed from the same
145
  buffer that feeds the sound device. Each call empties the ring and returns it;
146
  an empty ring returns a valid zero-frame WAV.
147
- Determinism: under `/pause` + `/step`, the drain covers exactly the audio of
148
  the stepped frames, so a test can `pause → step(n) → GET /audio` and get the
149
  sound of those `n` frames reproducibly.
150
- `X-Rrdc-Audio-Dropped: <n>` — samples the ring dropped (oldest-first) to
151
  overflow since the last drain. Non-zero means drain more often. `0` when clean.
152
153
## Consistency (normative)
154
155
- `/mem`, `/regs`, and `/step` MUST be serviced on the emulator thread at an
156
  instruction boundary — e.g. the HTTP handler enqueues a command that the main
157
  loop drains between instructions/frames and signals back. The HTTP thread MUST
158
  NOT read CPU/RAM state directly while the CPU is executing.
159
- `/screenshot` MUST serve a complete-frame snapshot (double-buffer: render
160
  thread writes the back buffer, swaps under a lock at frame end; HTTP thread
161
  reads the front buffer).
162
- `frame` in `/status` MUST increment once per completed video frame and never
163
  go backwards (except across `/reset`).
164
165
## Versioning
166
167
- Semantic. MAJOR bump = breaking change to an existing endpoint's
168
  request/response. MINOR = additive (new endpoint / optional field). PATCH =
169
  clarification.
170
- Clients read `/status.contract` and fail fast on a MAJOR mismatch.
171
172
---
173
174
## Per-platform appendix
175
176
### Commander X16 — `x16emu`
177
- Flag: `-controlport <N>` (single-dash, matching x16emu's `-dump` style).
178
- Framebuffer: **640×480**, native order BGRA8888 (`framebuffer[i*4+2]=R`,
179
  `+1=G`, `+0=B`); the server swizzles to PPM RGB.
180
- Address space: 16-bit CPU (`0x0000–0xFFFF`). The banked window `0xA000–0xBFFF`
181
  is selected by `bank` → the emulator's `read6502(addr, bank)` / banked RAM
182
  (`BRAM`). `addr < 0xA000` or `≥ 0xC000` ignores `bank`.
183
- Registers: `a x y` (8), `sp pc` (16), `status` (8), plus `ram_bank`,
184
  `rom_bank`. Source: the `regs` struct + `memory_get_ram_bank/rom_bank()`.
185
- Frame source: `video.c` `frame_count`.
186
- Audio: stereo (2 ch), native rate `25000000/512` ≈ **48828 Hz**, teed from the
187
  mixed `audio_render()` output.
188
189
### Neo6502 — neo6502 emulator
190
- Flag: `--control-port <N>`.
191
- Address space: flat 64 KB; `bank` ignored.
192
- Registers: 6502 `a x y sp pc status`.
193
- Framebuffer: native resolution per the emulator's video mode.
194
195
### Agon (Console8 / Light 2) — FAB Agon Emulator
196
- **Adoption:** conform-only (Rust emulator — keeps its own HTTP server, matches
197
  this contract rather than vendoring the C core).
198
- Flag: `--control-port <N>`.
199
- Address space: eZ80 24-bit, flat 16 MB (`addr` up to `0xFFFFFF`); `bank`
200
  tolerated and ignored. Program load base `0x040000`.
201
- Registers (ADL) — `/regs` example (16-bit regs; `mbase` 8-bit; `adl` 0/1):
202
  ```json
203
  { "af":0, "bc":0, "de":0, "hl":0, "ix":0, "iy":0,
204
    "sp":0, "pc":262144, "mbase":0, "adl":1 }
205
  ```
206
- Framebuffer: from `vgabuf` (the VDP output); the offscreen path already keeps
207
  video alive (the FAB equivalent of x16's `SDL_VIDEODRIVER=dummy`).
208
- Audio: the VDP mixes 8-bit unsigned mono at 16384 Hz; `/audio` returns it as a
209
  16-bit signed PCM WAV. The SDL audio callback (the sole `getAudioSamples`
210
  consumer) tees each block into the ring; headless capture needs
211
  `SDL_AUDIODRIVER=dummy` so that callback runs.
212
- Input: `/key?text=c` maps the char to a fabgl virtual key (`ascii2vk`) and
213
  delivers it via `sendVKeyEventToFabgl`; `?code=<vk>` passes a raw fabgl vkey.
214
  The control thread queues events; the render thread delivers them (same
215
  thread as normal keyboard input). `/reset` triggers the soft-reset atomic.
216
217
### NEC PC-FX — `mednafen` (Doomsday One fork)
218
- **Adoption:** vendors the shared core as a git submodule
219
  (`src/extern/retro-remote-debug-controller`), built in with `./configure
220
  --enable-rrdc`. Dispatched per running console, so PC Engine and Saturn can
221
  reuse the same core once their backends land.
222
- Control port: the `MEDNAFEN_CONTROLPORT=<N>` environment variable (Mednafen
223
  exposes no free-form CLI flag for a driver add-on). Unset ⇒ server off.
224
- Address space: V810, flat **2 MB** main RAM (`0x000000–0x1FFFFF`); `addr`
225
  wraps in that space and `bank` is ignored.
226
- Registers: `pc` + `r0`–`r31` (33 × 32-bit), from the V810 core.
227
- Framebuffer: the active display, decoded to RGB888 by the fork with the
228
  surface's channel shifts (so it is correct regardless of Mednafen's
229
  display-dependent pixel order). The PC-FX varies horizontal resolution per
230
  scene (256 for the maze, up to 1024 for high-res text); the fork normalizes
231
  each scanline by Mednafen's per-line `LineWidths`, so the PPM is centred and
232
  complete at whatever width the scene uses. Height is the visible line count
233
  (~232). Clients read `width`/`height` from the PPM header.
234
- Audio: stereo, at Mednafen's configured output rate (`espec.SoundRate`),
235
  teed from the per-frame `SoundBuf`.
236
- Input: the PC-FX has a gamepad, not a keyboard, so `/key?text=<c>` and
237
  `/key?code=<n>` are both read as a character over a WASD-style pad map —
238
  `w/a/s/d` = up/left/down/right, space or Enter = RUN (start), `c` = SELECT,
239
  `1`–`6` = buttons I–VI. A held button is re-asserted each frame (the frontend
240
  rewrites the pad from physical input every frame), so holds persist across
241
  `/step`. `/reset` power-cycles the machine.
242
243
---
244
245
## Adoption paths
246
247
An emulator conforms by exposing exactly the HTTP surface above — nothing else
248
is required. Two ways to get there:
249
250
- **Vendor the shared core** (`core/retro_control.c` + `.h`) — for C/C++
251
  emulators. Implement the backend callbacks (four are enough for 0.1; add
252
  `inject_key`/`reset` for 0.2 and `write_mem`/`capture_audio` for 0.3 — a NULL
253
  callback makes its endpoint return 501 and lowers the advertised contract),
254
  wire three loop hooks, call `retro_control_start(port, &backend)`; the socket
255
  loop, HTTP parsing, PPM/JSON encoding and pause/step control come for free.
256
  The Commander X16 fork does this.
257
- **Conform directly** — for emulators in other languages (the FAB Agon emulator
258
  is Rust). Keep your own HTTP server; just match the contract and pass the
259
  conformance suite. Sharing the C core is an optimization, not a requirement —
260
  the contract is the asset.
261
262
Either way, `conformance/` is the arbiter: pass it and you conform.
263
264
## In this repo
265
266
- `core/retro_control.{c,h}` — portable server + backend vtable (vendor path).
267
- `client/emu_control.py` — the shared pytest client.
268
- `conformance/` — a platform-agnostic pytest that proves an emulator conforms.