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