OXYGENHUB
# 67065eaa1a13 retro-remote-debug-controller / README.md
4.7 KiB · Markdown · last changed spec: document Agon /audio format in the platform appendix 3 months ago renderedsource blame history raw
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
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 |
20
21
Current contract: **0.3.0**. Minors are purely additive (a missing capability
22
returns 501 and lowers the advertised contract), so a client reads
23
`/status.contract` and fails fast only on a MAJOR mismatch. See [SPEC.md]SPEC.md.
24
25
## Why
26
27
Every one of these ports (Pac-Man on X16, Neo6502, Agon, and NEC PC-FX; and
28
whatever's next) is chasing **frame-exact** arcade fidelity, and every one had
29
its own fragile, platform-specific way to check state headless (write to the SD
30
card, dump 64 KB of RAM on a magic jump, grep stdout — which lies). This
31
replaces all of that with one contract:
32
33
- **`GET /mem`** → assert game state (sprite table, actor positions);
34
  **`POST /mem`** pokes it back for setup.
35
- **`GET /screenshot`** → Pillow → assert pixels, or just catch "nothing
36
  rendered" (the exact class of bug that costs an afternoon otherwise).
37
- **`POST /step`** → deterministic, frame-addressable capture, so CI diffs don't
38
  flake against a free-running 60 Hz loop.
39
- **`POST /key`** → drive input; **`GET /audio`** → assert the sound a stepped
40
  window produced.
41
42
The payoff is uniformity: the same `emu_control.py` and the same pytest patterns
43
drive the X16, Neo6502, Agon, and PC-FX emulators.
44
45
## Layout
46
47
```
48
SPEC.md                 the normative HTTP contract (start here)
49
core/retro_control.h    portable backend vtable each emulator implements
50
core/retro_control.c    shared server: sockets, HTTP, PPM/JSON, marshalling
51
client/emu_control.py   shared pytest client (stdlib; Pillow optional)
52
conformance/            a platform-agnostic pytest that proves an emulator conforms
53
CHANGELOG.md            the additive contract history
54
```
55
56
## Adopting it in an emulator
57
58
Two first-class paths (both normative in [SPEC.md]SPEC.md#adoption-paths):
59
60
- **Vendor the shared core** — for C/C++ emulators. Add this repo as a git
61
  submodule, compile `core/retro_control.c`, implement the backend callbacks,
62
  and wire three loop hooks. The socket loop, HTTP parsing, PPM/JSON encoding
63
  and pause/step control come for free.
64
- **Conform only** — for emulators in other languages (the FAB Agon emulator is
65
  Rust). Keep your own HTTP server; just match the contract and pass the
66
  conformance suite. Sharing the C core is an optimization, not a requirement.
67
68
The vendor path is one backend struct plus three hooks:
69
70
```c
71
#include "retro_control.h"
72
73
/* Four callbacks satisfy 0.1; add inject_key/reset for 0.2 and
74
   write_mem/capture_audio for 0.3. A NULL callback 501s its endpoint. */
75
static const retro_control_backend_t backend = {
76
    .platform        = "x16",
77
    .emulator        = "x16emu",
78
    .read_mem        = x16_read_mem,         /* debug-read, no side effects */
79
    .get_regs_json   = x16_get_regs_json,
80
    .get_framebuffer = x16_get_framebuffer,
81
    .get_frame_count = x16_get_frame_count,
82
    .inject_key      = x16_inject_key,       /* 0.2 */
83
    .reset           = x16_reset,            /* 0.2 */
84
    .write_mem       = x16_write_mem,        /* 0.3 */
85
    .capture_audio   = x16_capture_audio,    /* 0.3 */
86
};
87
88
if (control_port)                 /* set by -controlport / --control-port / env */
89
    retro_control_start(control_port, &backend);
90
```
91
92
Stepping and pausing are driven cooperatively from the emulator's main loop, not
93
by a callback — three hooks at a frame boundary:
94
95
```c
96
retro_control_service();                 /* run a pending request on the emu thread */
97
if (!retro_control_running()) { idle; }  /* gate advancing the machine (pause/step) */
98
... run one frame ...
99
if (frame_completed) retro_control_on_frame();   /* tick the /step budget */
100
```
101
102
## Status
103
104
Contract **0.3.0**, implemented across **four** emulators. The shared server and
105
the conformance suite (15 tests, green against the reference) are complete.
106
Reference implementation: the Commander X16 fork (`x16-emulator`), which vendors
107
`core/` as a git submodule; the NEC PC-FX fork (Mednafen) does the same, and the
108
FAB Agon emulator conforms directly in Rust.