OXYGENHUB
# 69f777699a51 retro-remote-debug-controller / SPEC.md
7.2 KiB · Markdown · last changed HTTP control contract, portable server core, and shared client 3 months ago renderedsource blame history raw
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
---
154
155
## Reference implementations in this repo
156
157
- `core/retro_control.h` — the portable backend vtable each fork implements.
158
- `core/retro_control.c` — the shared server (socket loop, HTTP parse, PPM/JSON
159
  encode, command marshalling). Forks vendor/submodule this and call
160
  `retro_control_start(port, &backend)`. *(server .c lands next.)*
161
- `client/emu_control.py` — the shared pytest client.
162
- `conformance/` — tests an emulator runs to prove it conforms. *(next.)*