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