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
1
# Retro Remote Debug Controller — HTTP Control Contract
2
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
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`.
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
14
machine deterministically, and inject input. The same contract is implemented
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.
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
{
50
  "contract": "0.6.0",
51
  "emulator": "x16emu",
52
  "platform": "x16",
53
  "frame": 12345,
54
  "paused": true,
55
  "running": true,
56
  "capabilities": ["mem", "regs", "screenshot", "step", "key", "reset",
57
                   "mem-write", "audio", "pointer", "pad", "perf"]
58
}
59
```
60
- `contract` — this document's version. Clients MUST reject a mismatched MAJOR.
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.
68
- `platform` — one of `x16`, `neo6502`, `agon`, `pcfx` (extensible). Tells the
69
  client the memory/register model (see *Per-platform* appendix).
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.
83
- Reserved: `?frame=N` (block until frame N) — still reserved, not yet required.
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
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
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
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.
141
142
Injection plus `/step` retires scripted-autoexec input: press → step → assert →
143
release, deterministically.
144
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
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
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).
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.
242
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
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.
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).
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`.
402
- Audio: stereo (2 ch), native rate `25000000/512` ≈ **48828 Hz**, teed from the
403
  mixed `audio_render()` output.
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`
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**.
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.
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.
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.
457
458
### Agon (Console8 / Light 2) — FAB Agon Emulator
459
- **Adoption:** conform-only (Rust emulator — keeps its own HTTP server, matches
460
  this contract rather than vendoring the C core).
461
- Flag: `--control-port <N>`.
462
- Address space: eZ80 24-bit, flat 16 MB (`addr` up to `0xFFFFFF`); `bank`
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`).
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.
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.
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.
489
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.
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.
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.
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.
528
529
---
530
531
## Adoption paths
532
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++
537
  emulators. Implement the backend callbacks (four are enough for 0.1; add
538
  `inject_key`/`reset` for 0.2, `write_mem`/`capture_audio` for 0.3,
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
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
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.
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).
557
- `client/emu_control.py` — the shared pytest client.
558
- `conformance/` — a platform-agnostic pytest that proves an emulator conforms.