| 1 |
# Retro Remote Debug Controller — HTTP Control Contract |
| 2 |
|
| 3 |
**Contract version: 0.1.0** (semver; clients assert on the MAJOR). |
| 4 |
|
| 5 |
A minimal, portable HTTP API that a retro-platform emulator exposes so an |
| 6 |
external harness can screenshot the screen, read memory and registers, step the |
| 7 |
machine deterministically, and (later) inject input. The same contract is |
| 8 |
implemented by the forked FAB Agon Emulator, the Neo6502 emulator, and the |
| 9 |
Commander X16 emulator, so **one** pytest harness drives all three ports. |
| 10 |
|
| 11 |
## Principles |
| 12 |
|
| 13 |
- **Opt-in.** The server is OFF by default and starts only when the emulator is |
| 14 |
launched with a control-port flag. Zero cost to normal use. |
| 15 |
- **Localhost only.** The server MUST bind `127.0.0.1` (never `0.0.0.0`). |
| 16 |
`/mem` and input injection are a memory-read / remote-control surface; they do |
| 17 |
not leave the machine. |
| 18 |
- **The contract is the HTTP surface, not the flag.** Each emulator names its |
| 19 |
launch flag idiomatically (x16emu: `-controlport N`; FAB: `--control-port N`). |
| 20 |
Only the endpoints below are normative. |
| 21 |
- **Determinism is a first-class requirement.** These are frame-exact ports. |
| 22 |
Every state read (`/mem`, `/regs`, `/screenshot`) MUST observe a consistent |
| 23 |
machine state — captured at an instruction/frame boundary on the emulator |
| 24 |
thread, never a torn mid-instruction read from the HTTP thread. See |
| 25 |
*Consistency* below. |
| 26 |
|
| 27 |
## Conventions |
| 28 |
|
| 29 |
- JSON responses are `application/json; charset=utf-8`. |
| 30 |
- Binary responses use the stated content-type; no chunked encoding required. |
| 31 |
- Errors: HTTP 4xx/5xx with body `{"error":"<message>"}`. |
| 32 |
- Numbers in JSON are decimal unless a field is documented as a hex string. |
| 33 |
- Methods are exactly as listed (`GET`/`POST`). Unknown path → 404. Unknown |
| 34 |
method on a known path → 405. |
| 35 |
|
| 36 |
## Endpoints |
| 37 |
|
| 38 |
### `GET /status` |
| 39 |
Liveness + contract negotiation. Always available. |
| 40 |
```json |
| 41 |
{ |
| 42 |
"contract": "0.1.0", |
| 43 |
"emulator": "x16emu", |
| 44 |
"platform": "x16", |
| 45 |
"frame": 12345, |
| 46 |
"paused": true, |
| 47 |
"running": true |
| 48 |
} |
| 49 |
``` |
| 50 |
- `contract` — this document's version. Clients MUST reject a mismatched MAJOR. |
| 51 |
- `platform` — one of `x16`, `neo6502`, `agon` (extensible). Tells the client |
| 52 |
the memory/register model (see *Per-platform* appendix). |
| 53 |
- `frame` — monotonic completed-frame counter since boot. The determinism |
| 54 |
anchor: a screenshot taken at `frame == N` shows the state after N complete |
| 55 |
frames. |
| 56 |
- `paused` — true if the machine is halted (only advances via `/step`). |
| 57 |
|
| 58 |
### `GET /screenshot` |
| 59 |
The live screen as a Portable Pixmap. |
| 60 |
- Response: `image/x-portable-pixmap`, binary **PPM P6**, 8-bit RGB, no alpha. |
| 61 |
- Pixels are the most recent COMPLETE frame (never a half-scanned frame). The |
| 62 |
emulator keeps a snapshot the render thread fills at frame end; the HTTP |
| 63 |
thread serves that snapshot. |
| 64 |
- Dimensions are the platform's native framebuffer (appendix). Clients read |
| 65 |
`width`/`height` from the PPM header — do not hardcode. |
| 66 |
- Reserved: `?frame=N` (block until frame N) — not required in 0.1. |
| 67 |
|
| 68 |
### `GET /mem?addr=<a>&len=<n>[&bank=<b>]` |
| 69 |
Raw memory bytes. |
| 70 |
- `addr` — start address, decimal or `0x`-hex. Interpreted in the platform's |
| 71 |
address space (appendix). |
| 72 |
- `len` — byte count, decimal. Servers MAY cap `len` (recommend ≥ 65536); a |
| 73 |
capped request returns what it can with `Content-Length` telling the truth. |
| 74 |
- `bank` — OPTIONAL bank selector for banked platforms (X16). Omitted → the |
| 75 |
machine's currently-selected bank. Ignored by flat-memory platforms. |
| 76 |
- Response: `application/octet-stream`, exactly the requested bytes. |
| 77 |
- Reads go through the emulator's debug-read path (no side effects, no I/O |
| 78 |
triggers, no access-flag mutation). |
| 79 |
|
| 80 |
### `GET /regs` |
| 81 |
CPU registers as JSON. Keys are platform-specific (appendix); values are decimal |
| 82 |
unless suffixed `_hex`. Example (x16 / 65C02): |
| 83 |
```json |
| 84 |
{ "a":0, "x":16, "y":2, "sp":505, "pc":49152, "status":32, |
| 85 |
"ram_bank":1, "rom_bank":0 } |
| 86 |
``` |
| 87 |
|
| 88 |
### `POST /step?frames=<n>` *(recommended; required for frame-exact tests)* |
| 89 |
Advance the machine by exactly `n` complete frames (default 1), then halt. |
| 90 |
- Only meaningful when the machine is paused (see `/pause`). On a free-running |
| 91 |
machine, servers SHOULD pause, run `n` frames, and stay paused. |
| 92 |
- Response: `{ "frame": <new frame counter> }`. |
| 93 |
- `POST /step?cycles=<n>` MAY be offered for sub-frame stepping. |
| 94 |
|
| 95 |
### `POST /pause` / `POST /resume` *(recommended)* |
| 96 |
Halt / free-run. `{ "paused": true|false }`. A machine launched with a |
| 97 |
`--control-pause`-style option SHOULD boot paused so tests are deterministic |
| 98 |
from frame 0. |
| 99 |
|
| 100 |
### `POST /key` / `POST /reset` *(reserved for 0.2)* |
| 101 |
Input injection. Proposed shape — `POST /key` body |
| 102 |
`{ "code": <keycode>, "down": true|false }` or `{ "text": "..." }`; `/reset` |
| 103 |
performs a cold reset. Not required in 0.1, but reserve the paths. Injection |
| 104 |
plus `/step` is what retires scripted-autoexec input. |
| 105 |
|
| 106 |
## Consistency (normative) |
| 107 |
|
| 108 |
- `/mem`, `/regs`, and `/step` MUST be serviced on the emulator thread at an |
| 109 |
instruction boundary — e.g. the HTTP handler enqueues a command that the main |
| 110 |
loop drains between instructions/frames and signals back. The HTTP thread MUST |
| 111 |
NOT read CPU/RAM state directly while the CPU is executing. |
| 112 |
- `/screenshot` MUST serve a complete-frame snapshot (double-buffer: render |
| 113 |
thread writes the back buffer, swaps under a lock at frame end; HTTP thread |
| 114 |
reads the front buffer). |
| 115 |
- `frame` in `/status` MUST increment once per completed video frame and never |
| 116 |
go backwards (except across `/reset`). |
| 117 |
|
| 118 |
## Versioning |
| 119 |
|
| 120 |
- Semantic. MAJOR bump = breaking change to an existing endpoint's |
| 121 |
request/response. MINOR = additive (new endpoint / optional field). PATCH = |
| 122 |
clarification. |
| 123 |
- Clients read `/status.contract` and fail fast on a MAJOR mismatch. |
| 124 |
|
| 125 |
--- |
| 126 |
|
| 127 |
## Per-platform appendix |
| 128 |
|
| 129 |
### Commander X16 — `x16emu` |
| 130 |
- Flag: `-controlport <N>` (single-dash, matching x16emu's `-dump` style). |
| 131 |
- Framebuffer: **640×480**, native order BGRA8888 (`framebuffer[i*4+2]=R`, |
| 132 |
`+1=G`, `+0=B`); the server swizzles to PPM RGB. |
| 133 |
- Address space: 16-bit CPU (`0x0000–0xFFFF`). The banked window `0xA000–0xBFFF` |
| 134 |
is selected by `bank` → the emulator's `read6502(addr, bank)` / banked RAM |
| 135 |
(`BRAM`). `addr < 0xA000` or `≥ 0xC000` ignores `bank`. |
| 136 |
- Registers: `a x y` (8), `sp pc` (16), `status` (8), plus `ram_bank`, |
| 137 |
`rom_bank`. Source: the `regs` struct + `memory_get_ram_bank/rom_bank()`. |
| 138 |
- Frame source: `video.c` `frame_count`. |
| 139 |
|
| 140 |
### Neo6502 — neo6502 emulator |
| 141 |
- Flag: `--control-port <N>`. |
| 142 |
- Address space: flat 64 KB; `bank` ignored. |
| 143 |
- Registers: 6502 `a x y sp pc status`. |
| 144 |
- Framebuffer: native resolution per the emulator's video mode. |
| 145 |
|
| 146 |
### Agon (Console8 / Light 2) — FAB Agon Emulator |
| 147 |
- Flag: `--control-port <N>`. |
| 148 |
- Address space: eZ80 24-bit, flat 16 MB (`addr` up to `0xFFFFFF`); `bank` |
| 149 |
ignored. Program load base `0x040000`. |
| 150 |
- Registers (ADL): `af bc de hl ix iy sp pc mbase` + ADL-mode flag. |
| 151 |
- Framebuffer: from `vgabuf` (the VDP output), server swizzles to PPM RGB. |
| 152 |
|
| 153 |
### NEC PC-FX — `mednafen` (Doomsday One fork) |
| 154 |
- **Adoption:** vendors the shared core as a git submodule |
| 155 |
(`src/extern/retro-remote-debug-controller`), built in with `./configure |
| 156 |
--enable-rrdc`. Dispatched per running console, so PC Engine and Saturn can |
| 157 |
reuse the same core once their backends land. |
| 158 |
- Control port: the `MEDNAFEN_CONTROLPORT=<N>` environment variable (Mednafen |
| 159 |
exposes no free-form CLI flag for a driver add-on). Unset ⇒ server off. |
| 160 |
- Address space: V810, flat **2 MB** main RAM (`0x000000–0x1FFFFF`); `addr` |
| 161 |
wraps in that space and `bank` is ignored. |
| 162 |
- Registers: `pc` + `r0`–`r31` (33 × 32-bit), from the V810 core. |
| 163 |
- Framebuffer: the active display rect (typically 256×240), decoded to RGB888 |
| 164 |
by the fork with the surface's channel shifts, so it is correct regardless of |
| 165 |
Mednafen's display-dependent pixel order. |
| 166 |
- Audio: stereo, at Mednafen's configured output rate (`espec.SoundRate`), |
| 167 |
teed from the per-frame `SoundBuf`. |
| 168 |
- Input: the PC-FX has a gamepad, not a keyboard, so `/key?text=<c>` and |
| 169 |
`/key?code=<n>` are both read as a character over a WASD-style pad map — |
| 170 |
`w/a/s/d` = up/left/down/right, space or Enter = RUN (start), `c` = SELECT, |
| 171 |
`1`–`6` = buttons I–VI. A held button is re-asserted each frame (the frontend |
| 172 |
rewrites the pad from physical input every frame), so holds persist across |
| 173 |
`/step`. `/reset` power-cycles the machine. |
| 174 |
|
| 175 |
--- |
| 176 |
|
| 177 |
## Reference implementations in this repo |
| 178 |
|
| 179 |
- `core/retro_control.h` — the portable backend vtable each fork implements. |
| 180 |
- `core/retro_control.c` — the shared server (socket loop, HTTP parse, PPM/JSON |
| 181 |
encode, command marshalling). Forks vendor/submodule this and call |
| 182 |
`retro_control_start(port, &backend)`. *(server .c lands next.)* |
| 183 |
- `client/emu_control.py` — the shared pytest client. |
| 184 |
- `conformance/` — tests an emulator runs to prove it conforms. *(next.)* |