OXYGENHUB
◆ perf-6 retro-remote-debug-controller / README.md
6.6 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
2
3
A tiny, portable **HTTP control API for retro-platform emulators**, and the
4
shared tooling that speaks it. One contract → one test harness across every
5
Doomsday One port.
6
7
An emulator launched with a control-port flag exposes a localhost HTTP server:
8
67065eaa Andy McCall ↶ 9
| Endpoint | Returns | Since |
10
|---|---|---|
11
| `GET /status` | contract version, platform, frame counter, paused/running | 0.1 |
12
| `GET /screenshot` | PPM image of the live screen | 0.1 |
13
| `GET /mem?addr=&len=&bank=` | raw memory bytes | 0.1 |
14
| `GET /regs` | CPU registers (JSON) | 0.1 |
15
| `POST /step?frames=` | advance N frames deterministically | 0.1 |
16
| `POST /pause` / `POST /resume` | halt / resume the machine | 0.1 |
17
| `POST /key`, `POST /reset` | inject input / reset | 0.2 |
18
| `POST /mem?addr=&bank=` | write (poke) memory | 0.3 |
19
| `GET /audio` | drain synthesised PCM as a WAV | 0.3 |
346ca3ee Andy McCall ↶ 20
| `POST /pointer?x=&y=&buttons=` | inject a pointer move/click | 0.4 |
21
| `GET /pointer` | read back pointer position + buttons | 0.4 |
bed0f1e5 Andy McCall ↶ 22
| `POST /pad?index=&buttons=&connected=` | present a virtual controller, set what is held | 0.5 |
23
| `GET /pad?index=` | read back a pad's presence + held buttons | 0.5 |
8fd33bc5 Andy McCall ↶ 24
| `GET /perf?windows=&frames=&since=` | performance metrics: the frame budget, work/idle/IRQ cycles, statistics windows, zones, raw frames | 0.6 |
25
| `POST /perf` | performance settings (JSON body): target FPS, idle mode, history, zones and marker names | 0.6 |
961b3c61 Andy McCall 26
8fd33bc5 Andy McCall ↶ 27
`/status` also lists `capabilities` from 0.6: the groups a server serves,
28
whatever level it reaches.
29
30
Current contract: **0.6.0**. Minors are purely additive (a missing capability
67065eaa Andy McCall ↶ 31
returns 501 and lowers the advertised contract), so a client reads
32
`/status.contract` and fails fast only on a MAJOR mismatch. See [SPEC.md]SPEC.md.
33
961b3c61 Andy McCall 34
## Why
35
67065eaa Andy McCall ↶ 36
Every one of these ports (Pac-Man on X16, Neo6502, Agon, and NEC PC-FX; and
37
whatever's next) is chasing **frame-exact** arcade fidelity, and every one had
38
its own fragile, platform-specific way to check state headless (write to the SD
39
card, dump 64 KB of RAM on a magic jump, grep stdout — which lies). This
40
replaces all of that with one contract:
961b3c61 Andy McCall 41
67065eaa Andy McCall ↶ 42
- **`GET /mem`** → assert game state (sprite table, actor positions);
43
  **`POST /mem`** pokes it back for setup.
961b3c61 Andy McCall 44
- **`GET /screenshot`** → Pillow → assert pixels, or just catch "nothing
45
  rendered" (the exact class of bug that costs an afternoon otherwise).
46
- **`POST /step`** → deterministic, frame-addressable capture, so CI diffs don't
47
  flake against a free-running 60 Hz loop.
67065eaa Andy McCall ↶ 48
- **`POST /key`** → drive input; **`GET /audio`** → assert the sound a stepped
49
  window produced.
961b3c61 Andy McCall 50
51
The payoff is uniformity: the same `emu_control.py` and the same pytest patterns
67065eaa Andy McCall ↶ 52
drive the X16, Neo6502, Agon, and PC-FX emulators.
961b3c61 Andy McCall 53
54
## Layout
55
56
```
57
SPEC.md                 the normative HTTP contract (start here)
58
core/retro_control.h    portable backend vtable each emulator implements
67065eaa Andy McCall ↶ 59
core/retro_control.c    shared server: sockets, HTTP, PPM/JSON, marshalling
60
client/emu_control.py   shared pytest client (stdlib; Pillow optional)
61
conformance/            a platform-agnostic pytest that proves an emulator conforms
62
CHANGELOG.md            the additive contract history
961b3c61 Andy McCall 63
```
67065eaa Andy McCall ↶ 64
65
## Adopting it in an emulator
66
67
Two first-class paths (both normative in [SPEC.md]SPEC.md#adoption-paths):
961b3c61 Andy McCall 68
67065eaa Andy McCall ↶ 69
- **Vendor the shared core** — for C/C++ emulators. Add this repo as a git
70
  submodule, compile `core/retro_control.c`, implement the backend callbacks,
71
  and wire three loop hooks. The socket loop, HTTP parsing, PPM/JSON encoding
72
  and pause/step control come for free.
73
- **Conform only** — for emulators in other languages (the FAB Agon emulator is
74
  Rust). Keep your own HTTP server; just match the contract and pass the
75
  conformance suite. Sharing the C core is an optimization, not a requirement.
961b3c61 Andy McCall 76
67065eaa Andy McCall ↶ 77
The vendor path is one backend struct plus three hooks:
961b3c61 Andy McCall 78
79
```c
80
#include "retro_control.h"
81
346ca3ee Andy McCall ↶ 82
/* Four callbacks satisfy 0.1; add inject_key/reset for 0.2,
bed0f1e5 Andy McCall ↶ 83
   write_mem/capture_audio for 0.3, set_pointer/get_pointer for 0.4,
8fd33bc5 Andy McCall ↶ 84
   set_pad/get_pad for 0.5, get_perf/set_perf for 0.6.
346ca3ee Andy McCall ↶ 85
   A NULL callback 501s its endpoint. */
67065eaa Andy McCall ↶ 86
static const retro_control_backend_t backend = {
87
    .platform        = "x16",
88
    .emulator        = "x16emu",
89
    .read_mem        = x16_read_mem,         /* debug-read, no side effects */
90
    .get_regs_json   = x16_get_regs_json,
91
    .get_framebuffer = x16_get_framebuffer,
92
    .get_frame_count = x16_get_frame_count,
93
    .inject_key      = x16_inject_key,       /* 0.2 */
94
    .reset           = x16_reset,            /* 0.2 */
95
    .write_mem       = x16_write_mem,        /* 0.3 */
96
    .capture_audio   = x16_capture_audio,    /* 0.3 */
346ca3ee Andy McCall ↶ 97
    .set_pointer     = x16_set_pointer,      /* 0.4 */
bed0f1e5 Andy McCall ↶ 98
    .set_pad         = x16_set_pad,          /* 0.5 */
346ca3ee Andy McCall ↶ 99
    .get_pointer     = x16_get_pointer,      /* 0.4 */
8fd33bc5 Andy McCall ↶ 100
    .get_perf        = x16_get_perf,         /* 0.6: malloc'd JSON */
101
    .set_perf        = x16_set_perf,         /* 0.6 */
961b3c61 Andy McCall 102
};
103
67065eaa Andy McCall ↶ 104
if (control_port)                 /* set by -controlport / --control-port / env */
961b3c61 Andy McCall 105
    retro_control_start(control_port, &backend);
106
```
107
67065eaa Andy McCall ↶ 108
Stepping and pausing are driven cooperatively from the emulator's main loop, not
109
by a callback — three hooks at a frame boundary:
110
111
```c
112
retro_control_service();                 /* run a pending request on the emu thread */
113
if (!retro_control_running()) { idle; }  /* gate advancing the machine (pause/step) */
114
... run one frame ...
115
if (frame_completed) retro_control_on_frame();   /* tick the /step budget */
116
```
961b3c61 Andy McCall 117
118
## Status
119
398756c0 Andy McCall ↶ 120
Contract **0.5.0**, implemented across **four** emulators. The shared server and
121
the conformance suite (22 tests) are complete; tests gated *above* the server's
122
advertised level skip, so a 0.1-only backend still passes.
123
124
Reference implementation: the Commander X16 fork (`x16-emulator`), which vendors
125
`core/` as a git submodule; the NEC PC-FX fork (Mednafen) does the same, and the
126
FAB Agon emulator conforms directly in Rust.
127
128
> **Known gap — cumulative levels vs. absent devices.** A level is advertised
129
> only when *all* its callbacks are present, so a platform that lacks a device
130
> cannot advertise past it without supplying a stub. The PC-FX has no pointing
131
> device: it stubs `/pointer` to 400 in order to advertise **0.5.0** for the
132
> `/pad` support it genuinely has — but the suite gates the pointer tests on
133
> `minor >= 4`, so they then *run* and fail. Measured against the PC-FX fork:
134
> **19 passed, 3 failed**, the three being pointer tests. A single cumulative
135
> integer cannot express "has pad, has no pointer"; this needs either a
136
> per-capability declaration in `/status` or a conformance rule that treats a
137
> documented "no such device" 400 as a skip.