OXYGENHUB
◆ main retro-remote-debug-controller / SPEC.md
29.7 KiB · Markdown · last changed feat: contract 0.6 — GET/POST /perf, the performance metrics (budget, current frame, windows, zones, raw frames since one, a platform section) and their settings, and /status.capabilities; backend callbacks get_perf and set_perf return allocated JSON (#6) 25 days ago renderedsource blame history raw
961b3c61 Andy McCall 1
# Retro Remote Debug Controller — HTTP Control Contract
2
8fd33bc5 Andy McCall ↶ 3
**Contract version: 0.6.0** (semver; clients assert on the MAJOR). 0.6 adds
4
performance metrics (`GET /perf`, `POST /perf`) and `/status.capabilities`; 0.5
5
added a virtual game controller (`POST /pad`, `GET /pad`); 0.4 added pointer injection
398756c0 Andy McCall ↶ 6
(`POST /pointer`, `GET /pointer`); 0.3 added memory write (`POST /mem`) and an
7
audio drain (`GET /audio`); 0.2 added input injection (`POST /key`,
8
`POST /reset`). Every minor is purely additive, so older clients keep working and
9
servers advertise the highest level whose callbacks are all present via
10
`/status.contract`.
961b3c61 Andy McCall 11
12
A minimal, portable HTTP API that a retro-platform emulator exposes so an
13
external harness can screenshot the screen, read memory and registers, step the
d44319b1 Andy McCall ↶ 14
machine deterministically, and inject input. The same contract is implemented
4c0b050e Andy McCall ↶ 15
by the forked FAB Agon Emulator, the Neo6502 emulator, the Commander X16
16
emulator, and the NEC PC-FX (Mednafen) emulator, so **one** pytest harness
17
drives all four ports.
961b3c61 Andy McCall 18
19
## Principles
20
21
- **Opt-in.** The server is OFF by default and starts only when the emulator is
22
  launched with a control-port flag. Zero cost to normal use.
23
- **Localhost only.** The server MUST bind `127.0.0.1` (never `0.0.0.0`).
24
  `/mem` and input injection are a memory-read / remote-control surface; they do
25
  not leave the machine.
26
- **The contract is the HTTP surface, not the flag.** Each emulator names its
27
  launch flag idiomatically (x16emu: `-controlport N`; FAB: `--control-port N`).
28
  Only the endpoints below are normative.
29
- **Determinism is a first-class requirement.** These are frame-exact ports.
30
  Every state read (`/mem`, `/regs`, `/screenshot`) MUST observe a consistent
31
  machine state — captured at an instruction/frame boundary on the emulator
32
  thread, never a torn mid-instruction read from the HTTP thread. See
33
  *Consistency* below.
34
35
## Conventions
36
37
- JSON responses are `application/json; charset=utf-8`.
38
- Binary responses use the stated content-type; no chunked encoding required.
39
- Errors: HTTP 4xx/5xx with body `{"error":"<message>"}`.
40
- Numbers in JSON are decimal unless a field is documented as a hex string.
41
- Methods are exactly as listed (`GET`/`POST`). Unknown path → 404. Unknown
42
  method on a known path → 405.
43
44
## Endpoints
45
46
### `GET /status`
47
Liveness + contract negotiation. Always available.
48
```json
49
{
8fd33bc5 Andy McCall ↶ 50
  "contract": "0.6.0",
961b3c61 Andy McCall 51
  "emulator": "x16emu",
52
  "platform": "x16",
53
  "frame": 12345,
54
  "paused": true,
8fd33bc5 Andy McCall ↶ 55
  "running": true,
56
  "capabilities": ["mem", "regs", "screenshot", "step", "key", "reset",
57
                   "mem-write", "audio", "pointer", "pad", "perf"]
961b3c61 Andy McCall 58
}
59
```
60
- `contract` — this document's version. Clients MUST reject a mismatched MAJOR.
8fd33bc5 Andy McCall ↶ 61
- `capabilities` *(0.6)* — the endpoint groups this server serves, whatever
62
  level `contract` reaches: `mem`, `regs`, `screenshot`, `step` (always), then
63
  `key`, `reset`, `mem-write` (`POST /mem`), `audio`, `pointer`, `pad`, `perf`.
64
  `contract` stays cumulative (0.6.0 means everything up to and including
65
  `/perf`); `capabilities` lets a client ask for one group directly, so a
66
  backend with no pointer can still offer `/perf` without a refusing stub.
67
  Absent on servers older than 0.6: read `contract` there.
4c0b050e Andy McCall ↶ 68
- `platform` — one of `x16`, `neo6502`, `agon`, `pcfx` (extensible). Tells the
69
  client the memory/register model (see *Per-platform* appendix).
961b3c61 Andy McCall 70
- `frame` — monotonic completed-frame counter since boot. The determinism
71
  anchor: a screenshot taken at `frame == N` shows the state after N complete
72
  frames.
73
- `paused` — true if the machine is halted (only advances via `/step`).
74
75
### `GET /screenshot`
76
The live screen as a Portable Pixmap.
77
- Response: `image/x-portable-pixmap`, binary **PPM P6**, 8-bit RGB, no alpha.
78
- Pixels are the most recent COMPLETE frame (never a half-scanned frame). The
79
  emulator keeps a snapshot the render thread fills at frame end; the HTTP
80
  thread serves that snapshot.
81
- Dimensions are the platform's native framebuffer (appendix). Clients read
82
  `width`/`height` from the PPM header — do not hardcode.
4c0b050e Andy McCall ↶ 83
- Reserved: `?frame=N` (block until frame N) — still reserved, not yet required.
961b3c61 Andy McCall 84
85
### `GET /mem?addr=<a>&len=<n>[&bank=<b>]`
86
Raw memory bytes.
87
- `addr` — start address, decimal or `0x`-hex. Interpreted in the platform's
88
  address space (appendix).
89
- `len` — byte count, decimal. Servers MAY cap `len` (recommend ≥ 65536); a
90
  capped request returns what it can with `Content-Length` telling the truth.
91
- `bank` — OPTIONAL bank selector for banked platforms (X16). Omitted → the
92
  machine's currently-selected bank. Ignored by flat-memory platforms.
93
- Response: `application/octet-stream`, exactly the requested bytes.
94
- Reads go through the emulator's debug-read path (no side effects, no I/O
95
  triggers, no access-flag mutation).
96
674b8447 Andy McCall ↶ 97
### `POST /mem?addr=<a>[&bank=<b>]`  *(0.3)*
98
Write (poke) memory. The write-sibling of `GET /mem`; same address/bank model.
99
- `addr`, `bank` — as for the read form. `len` is NOT a query param: the byte
100
  count is the request **body** length.
101
- Body — `application/octet-stream`, the raw bytes to write starting at `addr`.
102
  Servers MAY cap the body (recommend ≥ 65536).
103
- Response: `{ "written": <n> }` — bytes actually written. A server MAY write
104
  fewer than requested (e.g. it stops at a ROM/unwritable boundary); the count
105
  tells the truth.
106
- Writes go through the emulator's CPU write path. Intended for RAM/state
107
  fixup (force lives, seed a level, restore a snapshot). Writing an I/O register
108
  MAY trigger device side effects — that is the caller's responsibility.
109
- Determinism: poke while `paused` so the machine can't overwrite between the
110
  write and a subsequent read.
111
961b3c61 Andy McCall 112
### `GET /regs`
113
CPU registers as JSON. Keys are platform-specific (appendix); values are decimal
114
unless suffixed `_hex`. Example (x16 / 65C02):
115
```json
116
{ "a":0, "x":16, "y":2, "sp":505, "pc":49152, "status":32,
117
  "ram_bank":1, "rom_bank":0 }
118
```
119
120
### `POST /step?frames=<n>`  *(recommended; required for frame-exact tests)*
121
Advance the machine by exactly `n` complete frames (default 1), then halt.
122
- Only meaningful when the machine is paused (see `/pause`). On a free-running
123
  machine, servers SHOULD pause, run `n` frames, and stay paused.
124
- Response: `{ "frame": <new frame counter> }`.
125
- `POST /step?cycles=<n>` MAY be offered for sub-frame stepping.
126
127
### `POST /pause` / `POST /resume`  *(recommended)*
128
Halt / free-run. `{ "paused": true|false }`. A machine launched with a
129
`--control-pause`-style option SHOULD boot paused so tests are deterministic
130
from frame 0.
131
d44319b1 Andy McCall ↶ 132
### `POST /key?text=<char> | ?code=<n>  [&down=0|1]`  *(0.2)*
133
Inject a key. Query params (like `/mem` and `/step`), no body:
134
- `text` — a single character, mapped to the platform's keyboard.
135
- `code` — a raw platform key code (e.g. fabgl virtual key on Agon, an SDL/PS2
136
  code on others). `text` and `code` are alternatives; provide one.
137
- `down` — `1` presses (holds), `0` releases; **omitted taps** (press then
138
  release). Holding across `/step` frames is how a program that samples input
139
  each frame (e.g. a game) actually sees the key.
140
- Response: `{ "injected": true }`. Missing/unmapped key → 400.
961b3c61 Andy McCall 141
d44319b1 Andy McCall ↶ 142
Injection plus `/step` retires scripted-autoexec input: press → step → assert →
143
release, deterministically.
144
346ca3ee Andy McCall ↶ 145
### `POST /pointer?x=<n>&y=<n> | ?dx=<n>&dy=<n>  [&buttons=<mask>]`  *(0.4)*
146
Inject a pointer (mouse) move and/or click through the platform's native pointer
147
path — the mouse analogue of `/key`. Query params (no body):
148
- `x`, `y` — **absolute** pointer position, in the platform's pointer coordinate
149
  space (framebuffer pixels; appendix). Provide both.
150
- `dx`, `dy` — **relative** motion applied to the current position; either may be
151
  omitted (treated as 0). `x`/`y` and `dx`/`dy` are alternatives; provide one
152
  pair. Absolute is preferred for tests ("put the cursor on the button" beats
153
  dead-reckoning).
154
- `buttons` — OPTIONAL bitmask, **bit0 = primary, bit1 = secondary**. Omitted →
155
  the button state is left unchanged (a pure move). Provide `buttons=0` to
156
  release. Hold a button across `/step` frames (re-assert it each move) so a
157
  program that samples the pointer per frame sees the press.
158
- Response: `{ "injected": true }`. No pointer on the platform → 400.
159
160
Device-level, not driver-state: the injection MUST exercise the same path a real
161
mouse takes (e.g. the KERNAL/VERA mouse on the X16, the firmware mouse on the
162
Neo6502), so what the program reads is what a physical mouse would produce.
163
Determinism: inject under `/pause`, then `/step` — the move lands at the frame
164
boundary, so screenshot-diffing a hovered/clicked UI is reproducible.
165
166
### `GET /pointer`  *(0.4)*
167
Read back the current pointer state. Response:
168
```json
169
{ "x": 42, "y": 24, "buttons": 1 }
170
```
171
- `x`, `y` — current position in the same coordinate space as `POST /pointer`.
172
- `buttons` — the current button bitmask (same bit assignment).
173
- Lets a test distinguish "the input never reached the platform" from "it
174
  arrived but the program drew the wrong thing". No pointer on the platform → 400.
175
bed0f1e5 Andy McCall ↶ 176
### `POST /pad?index=<n>  [&buttons=<mask>] [&connected=0|1]`  *(0.5)*
177
Inject a game controller — the pad analogue of `/key` and `/pointer`. Query
178
params (no body):
179
- `index` — which pad, 0-based. OPTIONAL, default 0.
180
- `buttons` — bitmask of what is HELD, in the canonical layout below. Omitted →
181
  the button state is left unchanged. `buttons=0` releases everything while
182
  leaving the pad connected.
183
- `connected` — OPTIONAL. `1` (the default whenever `buttons` is given) presents
184
  a **virtual pad** at `index`; `0` unplugs it. Unplugging matters: a program
185
  that asks "is a controller present" before offering two-player must be
186
  testable in both states, and on most platforms the count is what it asks.
187
- Response: `{ "injected": true }`. No controller support on the platform → 400.
188
189
Canonical button layout, low bit first:
190
191
| bit | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
192
|---|---|---|---|---|---|---|---|---|---|---|---|---|
193
| | LEFT | RIGHT | UP | DOWN | A | B | X | Y | START | SELECT | L | R |
194
195
This is the SNES/Neo6502 native order, chosen for the same reason `/pointer`
196
fixed bit0 = primary: **one layout, defined once, so a test written against one
197
platform reads the same on another.** A backend whose hardware disagrees — the
198
X16 KERNAL reports RIGHT/LEFT/DOWN/UP — converts in its own floor. A test never
199
learns which machine it is on, which is the entire point.
200
201
Device-level, not driver-state: the injection MUST exercise the same path a real
202
pad takes, so what the program reads is what physical hardware would produce. On
203
an emulator that reads SDL joysticks, that means presenting a virtual pad the
204
existing read path picks up — **not** driving SDL, which would need a joystick
205
to exist and defeats the purpose. Headless CI has no joystick; that is the case
206
this endpoint is for.
207
208
Hold across frames: a pad is a LEVEL, not an event. `buttons` stays as set until
209
changed, so a press survives any number of `/step` frames without re-asserting —
210
unlike `/key`, whose TAP is deliberately momentary. Release explicitly.
211
212
### `GET /pad?index=<n>`  *(0.5)*
213
Read back a pad. Response:
214
```json
215
{ "index": 0, "connected": true, "buttons": 48 }
216
```
217
- `connected` — whether anything is present at that index, real or virtual.
218
- `buttons` — the current mask in the layout above.
219
- Same job as `GET /pointer`: distinguishes "the input never reached the
220
  platform" from "it arrived and the program ignored it". No controller support
221
  on the platform → 400.
222
d44319b1 Andy McCall ↶ 223
### `POST /reset`  *(0.2)*
224
Soft/cold reset the machine. Response `{ "reset": true }`. The `frame` counter
225
MAY reset here (the one case `/status.frame` is allowed to go backwards).
674b8447 Andy McCall ↶ 226
227
### `GET /audio`  *(0.3)*
228
Drain the audio the emulator has synthesised since the last `/audio` call — the
229
audio analogue of `/screenshot`, but a *window* (everything since last drain)
230
rather than a snapshot.
231
- Response: `audio/wav`, a self-describing **PCM WAV** (RIFF, 16-bit signed,
232
  interleaved). The header carries the sample rate and channel count; clients
233
  read them from the WAV — do not hardcode (appendix lists native rates).
234
- The server keeps a bounded ring of mixed output samples, teed from the same
235
  buffer that feeds the sound device. Each call empties the ring and returns it;
236
  an empty ring returns a valid zero-frame WAV.
237
- Determinism: under `/pause` + `/step`, the drain covers exactly the audio of
238
  the stepped frames, so a test can `pause → step(n) → GET /audio` and get the
239
  sound of those `n` frames reproducibly.
240
- `X-Rrdc-Audio-Dropped: <n>` — samples the ring dropped (oldest-first) to
241
  overflow since the last drain. Non-zero means drain more often. `0` when clean.
d44319b1 Andy McCall ↶ 242
8fd33bc5 Andy McCall ↶ 243
### `GET /perf  [?windows=<list>] [&zones=0|1] [&frames=<n>] [&since=<frame>]`  *(0.6)*
244
How much of each frame the program uses: the measure a profiler gives, read by
245
a test the way `/mem` is. Answered on the emulator thread (see *Consistency*),
246
so it describes whole frames.
247
248
Query params, all OPTIONAL:
249
- `windows` — comma-separated statistics windows, each a number of seconds or
250
  `session` (the whole history). Default `1,10,30,60,session`.
251
- `zones` — `0` leaves `zones` out. Default `1`.
252
- `frames` — return the last `n` completed frames' raw records in `frames`
253
  (oldest first). Default `0`: none.
254
- `since` — return the raw records of every frame after `<frame>` still in the
255
  history (oldest first; `frames` then caps how many, the newest kept). Lets a
256
  harness poll and never miss a frame: pass the last `frame` it saw.
257
- A platform MAY accept more (the X16: `lines=1`, below); unknown params are
258
  ignored.
259
260
Every count is in **guest CPU cycles** unless its name says otherwise (`…Us`:
261
host microseconds; `…Pct`: percent). The shape:
262
263
```json
264
{
265
  "enabled": true,
266
  "budget": {
267
    "targetFps": 60, "vsyncHz": 59.52, "machineKhz": 8000,
268
    "cyclesPerFrame": 134400, "scanlinesPerFrame": 525, "cyclesPerScanline": 256,
269
    "budgetCycles": 134400, "framesPerBudget": 1,
270
    "historyFrames": 3600, "totalFrames": 7200, "idleMode": "auto"
271
  },
272
  "current": {
273
    "frame": 7199, "totalCycles": 134400, "workCycles": 31012,
274
    "idleCycles": 103388, "irqCycles": 3297, "mainCycles": 27715,
275
    "budgetWorkCycles": 31012, "instructions": 9120, "hostUs": 412,
276
    "utilizationPct": 23.1, "overBudget": false
277
  },
278
  "windows": [
279
    { "name": "1s", "seconds": 1, "frames": 60, "budgetCycles": 134400,
280
      "work":       {"min": 0, "mean": 0, "p50": 0, "p95": 0, "p99": 0, "max": 0},
281
      "budgetWork": {"min": 0, "mean": 0, "p50": 0, "p95": 0, "p99": 0, "max": 0},
282
      "total": {"…": 0}, "irq": {"…": 0}, "hostUs": {"…": 0},
283
      "overruns": 0, "overrunPct": 0.0, "worstOverrunCycles": 0, "worstFrame": 0,
284
      "meanUtilizationPct": 23.1, "p95UtilizationPct": 31.0, "maxUtilizationPct": 40.2 }
285
  ],
286
  "zones": [
287
    { "id": 0, "name": "sound", "kind": "marker", "markerId": 7, "idle": false,
288
      "current": 11520,
289
      "windows": { "1s": {"min": 0, "mean": 0, "p50": 0, "p95": 0, "p99": 0, "max": 0} } },
290
    { "id": 1, "name": "vblank wait", "kind": "address",
291
      "start": 49152, "end": 49408, "bank": -1, "idle": true, "current": 0, "windows": {} }
292
  ],
293
  "frames": [
294
    { "frame": 7198, "totalCycles": 134400, "workCycles": 30544,
295
      "idleCycles": 103856, "irqCycles": 3297, "instructions": 9001,
296
      "hostUs": 398, "budgetWorkCycles": 30544, "zones": [11520, 0] }
297
  ],
298
  "platform": { }
299
}
300
```
301
302
- `enabled` — whether the backend is measuring. Off, every count is 0 and the
303
  history is empty; `POST /perf {"enabled":true}` turns it on. A backend MAY
304
  start measuring on the first `/perf` request.
305
- `budget` — the frame and its budget. `budgetCycles` is the cycles the program
306
  may use per budget period of `framesPerBudget` frames (a 30 fps target on a
307
  60 Hz machine: 2 frames). `cyclesPerScanline` converts a count to scanlines.
308
- `current` — the last **completed** frame. `workCycles` = total − idle;
309
  `mainCycles` = work − irq. `idleCycles` are cycles the backend classifies as
310
  waiting (see *Idle*). `budgetWorkCycles` is the work over the trailing budget
311
  period, which `utilizationPct` and `overBudget` measure against `budgetCycles`.
312
- `windows` — per requested window: the frames it covers and `{min, mean, p50,
313
  p95, p99, max}` of each series (`budgetWork` per budget period, the rest per
314
  frame); `overruns` (periods over budget), their share, and the worst.
315
- `zones` — the program's own breakdown (see *Zones*). `current` is the zone's
316
  cycles in the last completed frame; `windows` its `{min…max}` per window, where
317
  the backend keeps a zone's history (`{}` where it does not). A cycle belongs
318
  to one zone at most: the innermost open marker region, else the address zone
319
  the PC is in.
320
- `frames` — raw per-frame records, `zones` indexed by zone `id`.
321
- `platform` — anything platform-specific, described in the *Per-platform
322
  appendix* (the X16's VERA bus bandwidth and sprite render time). A client
323
  that does not know the platform ignores it.
324
325
**Idle.** A backend counts as idle what the CPU spends waiting: a halt
326
instruction (`WAI`, `HALT`), a recognised spin loop, an address zone marked
327
idle, or what the guest marks so. `idleMode` says which: `auto` (all of them),
328
`markers` (only what the guest marks), `none`.
329
330
**Zones.** Two kinds, both optional:
331
- *address* zones — a PC range `[start, end)` (and `bank`, `-1` any), set by
332
  `POST /perf`.
333
- *marker* regions — the guest program writes markers to a platform-defined
334
  register as it enters and leaves a region of its own (the X16: `$9FBC`, see
335
  the appendix). A marker region exists once the guest first enters it, named
336
  `region <n>` unless `POST /perf` has named it.
337
338
### `POST /perf`  *(0.6)*
339
Settings. The body is a JSON object; every field is OPTIONAL, and a field left
340
out is left as it is:
341
```json
342
{
343
  "enabled": true,
344
  "targetFps": 60,
345
  "idleMode": "auto",
346
  "historyFrames": 3600,
347
  "reset": false,
348
  "zones": [
349
    { "name": "game", "marker": 1 },
350
    { "name": "vblank wait", "start": 49152, "end": 49408, "bank": -1, "idle": true }
351
  ]
352
}
353
```
354
- `enabled` — start or stop measuring. Stopping MAY discard the history.
355
- `targetFps` — the rate the program aims for, which sets `budget`.
356
- `idleMode` — `auto`, `markers` or `none` (see *Idle*).
357
- `historyFrames` — how many frames of history to keep.
358
- `reset` — `true` empties the history and the statistics.
359
- `zones` — **replaces** the zone list. An entry with `marker` names the guest's
360
  marker region `<n>`; one with `start`/`end` is an address zone.
361
- A platform MAY accept more (the X16: `sprites`, below); unknown fields are
362
  ignored.
363
- Response: `200` and the `GET /perf` body without `windows`, `zones` or
364
  `frames`, i.e. the settings as they now stand. A body that is not a JSON
365
  object, or a field of the wrong type, is `400`.
366
961b3c61 Andy McCall 367
## Consistency (normative)
368
369
- `/mem`, `/regs`, and `/step` MUST be serviced on the emulator thread at an
370
  instruction boundary — e.g. the HTTP handler enqueues a command that the main
371
  loop drains between instructions/frames and signals back. The HTTP thread MUST
372
  NOT read CPU/RAM state directly while the CPU is executing.
8fd33bc5 Andy McCall ↶ 373
- `/perf` MUST be answered on the emulator thread, and describe completed
374
  frames only (`current` is the last whole frame, never the one in progress).
961b3c61 Andy McCall 375
- `/screenshot` MUST serve a complete-frame snapshot (double-buffer: render
376
  thread writes the back buffer, swaps under a lock at frame end; HTTP thread
377
  reads the front buffer).
378
- `frame` in `/status` MUST increment once per completed video frame and never
379
  go backwards (except across `/reset`).
380
381
## Versioning
382
383
- Semantic. MAJOR bump = breaking change to an existing endpoint's
384
  request/response. MINOR = additive (new endpoint / optional field). PATCH =
385
  clarification.
386
- Clients read `/status.contract` and fail fast on a MAJOR mismatch.
387
388
---
389
390
## Per-platform appendix
391
392
### Commander X16 — `x16emu`
393
- Flag: `-controlport <N>` (single-dash, matching x16emu's `-dump` style).
394
- Framebuffer: **640×480**, native order BGRA8888 (`framebuffer[i*4+2]=R`,
395
  `+1=G`, `+0=B`); the server swizzles to PPM RGB.
396
- Address space: 16-bit CPU (`0x0000–0xFFFF`). The banked window `0xA000–0xBFFF`
397
  is selected by `bank` → the emulator's `read6502(addr, bank)` / banked RAM
398
  (`BRAM`). `addr < 0xA000` or `≥ 0xC000` ignores `bank`.
399
- Registers: `a x y` (8), `sp pc` (16), `status` (8), plus `ram_bank`,
400
  `rom_bank`. Source: the `regs` struct + `memory_get_ram_bank/rom_bank()`.
401
- Frame source: `video.c` `frame_count`.
674b8447 Andy McCall ↶ 402
- Audio: stereo (2 ch), native rate `25000000/512` ≈ **48828 Hz**, teed from the
403
  mixed `audio_render()` output.
346ca3ee Andy McCall ↶ 404
- Pointer: `/pointer` maps to the KERNAL/VERA mouse — absolute/relative position
405
  and buttons fed through the same path the host mouse uses, so `MOUSE_GET`
398756c0 Andy McCall ↶ 406
  (and VERA sprite 0) see it. Coordinates are screen pixels.
407
- Pad: `/pad` presents a virtual controller through the emulator's own joystick
408
  read path. The fork implements the full 0.5 callback set and advertises
409
  **0.5.0**.
8fd33bc5 Andy McCall ↶ 410
- Perf *(0.6)*: the **enhanced X16 emulator** (`enhanced-x16-emulator`, the one
411
  with the ImGui Performance panel) implements `/perf` from the same
412
  measurements the panel shows, and advertises **0.6.0**; the plain fork does
413
  not measure, and stays at 0.5.0. The budget is the X16's: 8 MHz, 525
414
  scanlines a frame at 59.52 Hz, 134,400 cycles, 256 a scanline. Idle: `WAI`,
415
  a short store-free backward branch taken twice, or markers.
416
  - Markers: the guest writes one byte to **`$9FBC`** (the emulator's register
417
    12; reading it returns `1`, the marker protocol, so a program can tell it
418
    is there): `$01`–`$3F` enter region n, `$80|n` leave region n, `$00` leave
419
    the innermost, `$F0` the frame's work begins, `$F1` the frame's work ends
420
    (the rest of the frame is idle), `$FE` reset the statistics. Up to 8
421
    regions nest; cycles go to the innermost. A write to a stock X16 or the
422
    plain fork does nothing.
423
  - Zones: up to 16; the first 8 keep a history (`windows`), the rest report
424
    `current` only.
425
  - `platform`:
426
    ```json
427
    { "bandwidth": { "frame": 7198, "lineClocks": 800, "linesActive": 480,
428
        "peakLine": 12, "peakLineClocks": 480, "p95LineClocks": 480,
429
        "meanLineClocks": 320, "linesOverBudget": 0,
430
        "layers": [ {"fetches": 57600, "bytes": 230400, "peakLine": 0, "peakFetches": 120},
431
                    {"fetches": 57600, "bytes": 230400, "peakLine": 0, "peakFetches": 120} ],
432
        "port": {"readBytes": 0, "writeBytes": 64, "accesses": 64, "vramBytes": 64},
433
        "lines": [320, 320, "…480 entries"] },
434
      "sprites": { "enabled": true, "frame": 7198, "lineBudget": 798,
435
        "peakLine": 40, "peakBudget": 612, "peakDemand": 612,
436
        "linesExhausted": 0, "spritesDropped": 0, "budgetUsedTotal": 51200,
437
        "slotsUsed": 24, "lines": [0, 0, "…480 entries"] } }
438
    ```
439
    `bandwidth` is VERA's VRAM bus in **bus clocks** (800 a scanline; a layer
440
    fetch or a CPU data-port access takes 2), for the last completed frame;
441
    `lines` (only with `?lines=1`) is each active line's clocks.
442
    `sprites` is the sprite renderer's time per line against its 798-clock
443
    budget, for the last frame; it is off until `POST /perf {"sprites":true}`
444
    (it costs the emulator time), `{"enabled":false}` until then.
961b3c61 Andy McCall 445
446
### Neo6502 — neo6502 emulator
447
- Flag: `--control-port <N>`.
448
- Address space: flat 64 KB; `bank` ignored.
449
- Registers: 6502 `a x y sp pc status`.
450
- Framebuffer: native resolution per the emulator's video mode.
346ca3ee Andy McCall ↶ 451
- Pointer: `/pointer` drives the firmware mouse — absolute → `MSESetPosition`,
452
  relative → `MSEOffsetPosition`, buttons → `MSEUpdateButtonState`; `GET /pointer`
453
  reads `MSEGetState`. Coordinates are firmware graphics pixels (the same space a
454
  program reads). Buttons: bit0 = left, bit1 = right (the firmware's native
455
  masks), matching the contract's primary/secondary. The emulator enables the
456
  mouse at reset, so injection works from boot.
961b3c61 Andy McCall 457
458
### Agon (Console8 / Light 2) — FAB Agon Emulator
42c217d9 Andy McCall ↶ 459
- **Adoption:** conform-only (Rust emulator — keeps its own HTTP server, matches
460
  this contract rather than vendoring the C core).
961b3c61 Andy McCall 461
- Flag: `--control-port <N>`.
462
- Address space: eZ80 24-bit, flat 16 MB (`addr` up to `0xFFFFFF`); `bank`
42c217d9 Andy McCall ↶ 463
  tolerated and ignored. Program load base `0x040000`.
464
- Registers (ADL) — `/regs` example (16-bit regs; `mbase` 8-bit; `adl` 0/1):
465
  ```json
466
  { "af":0, "bc":0, "de":0, "hl":0, "ix":0, "iy":0,
467
    "sp":0, "pc":262144, "mbase":0, "adl":1 }
468
  ```
469
- Framebuffer: from `vgabuf` (the VDP output); the offscreen path already keeps
470
  video alive (the FAB equivalent of x16's `SDL_VIDEODRIVER=dummy`).
67065eaa Andy McCall ↶ 471
- Audio: the VDP mixes 8-bit unsigned mono at 16384 Hz; `/audio` returns it as a
472
  16-bit signed PCM WAV. The SDL audio callback (the sole `getAudioSamples`
473
  consumer) tees each block into the ring; headless capture needs
474
  `SDL_AUDIODRIVER=dummy` so that callback runs.
d44319b1 Andy McCall ↶ 475
- Input: `/key?text=c` maps the char to a fabgl virtual key (`ascii2vk`) and
476
  delivers it via `sendVKeyEventToFabgl`; `?code=<vk>` passes a raw fabgl vkey.
477
  The control thread queues events; the render thread delivers them (same
478
  thread as normal keyboard input). `/reset` triggers the soft-reset atomic.
79333fa1 Andy McCall ↶ 479
- Pointer: `/pointer` feeds PS/2 mouse packets through `sendHostMouseEventToFabgl`
480
  — the same device path host mouse motion uses, so it drives the real fabgl/VDP
481
  mouse rather than a shortcut. PS/2 motion is relative, so the control server
482
  tracks the absolute position itself: `x`/`y` become a delta from that tracked
483
  point (split across packets past the ±127 int9 range), `dx`/`dy` apply directly,
484
  and `GET /pointer` reports it exactly. Coordinates are screen space (x right,
485
  y down; y is negated into the PS/2 up-positive convention). `buttons` bit0/bit1
486
  map to the PS/2 left/right (primary/secondary) button bits. Like `/key`, packets
487
  are queued by the control thread and delivered by the render thread at the frame
488
  boundary, so `pause → pointer → step(1) → screenshot` is reproducible.
961b3c61 Andy McCall 489
c17f852c Andy McCall ↶ 490
### NEC PC-FX — `mednafen` (Doomsday One fork)
491
- **Adoption:** vendors the shared core as a git submodule
492
  (`src/extern/retro-remote-debug-controller`), built in with `./configure
493
  --enable-rrdc`. Dispatched per running console, so PC Engine and Saturn can
494
  reuse the same core once their backends land.
495
- Control port: the `MEDNAFEN_CONTROLPORT=<N>` environment variable (Mednafen
496
  exposes no free-form CLI flag for a driver add-on). Unset ⇒ server off.
497
- Address space: V810, flat **2 MB** main RAM (`0x000000–0x1FFFFF`); `addr`
498
  wraps in that space and `bank` is ignored.
499
- Registers: `pc` + `r0`–`r31` (33 × 32-bit), from the V810 core.
4c0b050e Andy McCall ↶ 500
- Framebuffer: the active display, decoded to RGB888 by the fork with the
501
  surface's channel shifts (so it is correct regardless of Mednafen's
502
  display-dependent pixel order). The PC-FX varies horizontal resolution per
503
  scene (256 for the maze, up to 1024 for high-res text); the fork normalizes
504
  each scanline by Mednafen's per-line `LineWidths`, so the PPM is centred and
505
  complete at whatever width the scene uses. Height is the visible line count
506
  (~232). Clients read `width`/`height` from the PPM header.
c17f852c Andy McCall ↶ 507
- Audio: stereo, at Mednafen's configured output rate (`espec.SoundRate`),
508
  teed from the per-frame `SoundBuf`.
509
- Input: the PC-FX has a gamepad, not a keyboard, so `/key?text=<c>` and
510
  `/key?code=<n>` are both read as a character over a WASD-style pad map —
511
  `w/a/s/d` = up/left/down/right, space or Enter = RUN (start), `c` = SELECT,
512
  `1`–`6` = buttons I–VI. A held button is re-asserted each frame (the frontend
513
  rewrites the pad from physical input every frame), so holds persist across
514
  `/step`. `/reset` power-cycles the machine.
398756c0 Andy McCall ↶ 515
- Pad: `/pad` remaps the canonical mask to the FX pad word (`I=0 II=1 III=2 IV=3
516
  V=4 VI=5 SELECT=6 RUN=7 UP=8 RIGHT=9 DOWN=10 LEFT=11`; A/B → I/II, X/Y →
517
  III/IV, L/R → V/VI, START → RUN) and OR-merges it into the pad read every
518
  frame, as a level. **Trap:** that layout is implicit in the *order* of
519
  Mednafen's `PCFX_GamepadIDII` array — the third `IDIIS_Button` argument is
520
  `ConfigOrder`, not a bit offset. Using those numbers as bits binds every button
521
  wrongly and is a defect currently present in the fork.
522
- Pointer: the PC-FX has no pointing device. Because 0.5.0 advertises
523
  cumulatively, the fork supplies refusing stubs (`/pointer` → **400**) rather
524
  than NULL callbacks, so it can honestly advertise **0.5.0** for `/pad`.
525
- Audio: `/audio` is fed by the mixer, which only runs when sound is enabled.
526
  Launching with `-sound 0` yields a valid but **header-only (44-byte)** WAV, not
527
  an error — use `-sound 1 -sound.driver dummy` for headless audio capture.
c17f852c Andy McCall ↶ 528
961b3c61 Andy McCall 529
---
530
42c217d9 Andy McCall ↶ 531
## Adoption paths
961b3c61 Andy McCall 532
42c217d9 Andy McCall ↶ 533
An emulator conforms by exposing exactly the HTTP surface above — nothing else
534
is required. Two ways to get there:
535
536
- **Vendor the shared core** (`core/retro_control.c` + `.h`) — for C/C++
674b8447 Andy McCall ↶ 537
  emulators. Implement the backend callbacks (four are enough for 0.1; add
398756c0 Andy McCall ↶ 538
  `inject_key`/`reset` for 0.2, `write_mem`/`capture_audio` for 0.3,
8fd33bc5 Andy McCall ↶ 539
  `set_pointer`/`get_pointer` for 0.4, `set_pad`/`get_pad` for 0.5, and
540
  `get_perf`/`set_perf` (which return allocated JSON) for 0.6 — a NULL
398756c0 Andy McCall ↶ 541
  callback makes its endpoint return 501 and lowers the advertised contract, so a
542
  platform lacking a device should supply a stub that returns 400 instead), wire
543
  three loop hooks, call
346ca3ee Andy McCall ↶ 544
  `retro_control_start(port, &backend)`; the socket loop, HTTP parsing, PPM/JSON
545
  encoding and pause/step control come for free. The Commander X16 fork does
546
  this.
42c217d9 Andy McCall ↶ 547
- **Conform directly** — for emulators in other languages (the FAB Agon emulator
548
  is Rust). Keep your own HTTP server; just match the contract and pass the
549
  conformance suite. Sharing the C core is an optimization, not a requirement —
550
  the contract is the asset.
551
552
Either way, `conformance/` is the arbiter: pass it and you conform.
553
554
## In this repo
555
556
- `core/retro_control.{c,h}` — portable server + backend vtable (vendor path).
961b3c61 Andy McCall 557
- `client/emu_control.py` — the shared pytest client.
42c217d9 Andy McCall ↶ 558
- `conformance/` — a platform-agnostic pytest that proves an emulator conforms.