|
|
… |
5 unchanged lines |
| 6 |
6 |
|
|
| 7 |
7 |
|
An emulator launched with a control-port flag exposes a localhost HTTP server: |
| 8 |
8 |
|
|
| 9 |
|
- |
| Endpoint | Returns | |
| 10 |
|
- |
|---|---| |
| 11 |
|
- |
| `GET /status` | contract version, platform, frame counter, paused state | |
| 12 |
|
- |
| `GET /screenshot` | PPM image of the live screen | |
| 13 |
|
- |
| `GET /mem?addr=&len=&bank=` | raw memory bytes | |
| 14 |
|
- |
| `GET /regs` | CPU registers (JSON) | |
| 15 |
|
- |
| `POST /step?frames=` | advance N frames deterministically | |
| 16 |
|
- |
| `POST /key`, `/reset` | *(0.2)* inject input | |
|
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 | |
| 17 |
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 |
+ |
|
| 18 |
25 |
|
## Why |
| 19 |
26 |
|
|
| 20 |
|
- |
Every one of these ports (Pac-Man on X16, Neo6502, Agon; and whatever's next) is |
| 21 |
|
- |
chasing **frame-exact** arcade fidelity, and every one had its own fragile, |
| 22 |
|
- |
platform-specific way to check state headless (write to the SD card, dump 64 KB |
| 23 |
|
- |
of RAM on a magic jump, grep stdout — which lies). This replaces all of that |
| 24 |
|
- |
with one contract: |
|
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: |
| 25 |
32 |
|
|
| 26 |
|
- |
- **`GET /mem`** → assert game state (sprite table, actor positions). |
|
33 |
+ |
- **`GET /mem`** → assert game state (sprite table, actor positions); |
|
34 |
+ |
**`POST /mem`** pokes it back for setup. |
| 27 |
35 |
|
- **`GET /screenshot`** → Pillow → assert pixels, or just catch "nothing |
| 28 |
36 |
|
rendered" (the exact class of bug that costs an afternoon otherwise). |
| 29 |
37 |
|
- **`POST /step`** → deterministic, frame-addressable capture, so CI diffs don't |
| 30 |
38 |
|
flake against a free-running 60 Hz loop. |
|
39 |
+ |
- **`POST /key`** → drive input; **`GET /audio`** → assert the sound a stepped |
|
40 |
+ |
window produced. |
| 31 |
41 |
|
|
| 32 |
42 |
|
The payoff is uniformity: the same `emu_control.py` and the same pytest patterns |
| 33 |
|
- |
drive the X16, Neo6502, and Agon emulators. |
|
43 |
+ |
drive the X16, Neo6502, Agon, and PC-FX emulators. |
| 34 |
44 |
|
|
| 35 |
45 |
|
## Layout |
| 36 |
46 |
|
|
| 37 |
47 |
|
``` |
| 38 |
48 |
|
SPEC.md the normative HTTP contract (start here) |
| 39 |
49 |
|
core/retro_control.h portable backend vtable each emulator implements |
| 40 |
|
- |
core/retro_control.c shared server: sockets, HTTP, PPM/JSON, marshalling [next] |
| 41 |
|
- |
client/emu_control.py shared pytest client |
| 42 |
|
- |
conformance/ tests an emulator runs to prove it conforms [next] |
|
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 |
| 43 |
54 |
|
``` |
|
55 |
+ |
|
|
56 |
+ |
## Adopting it in an emulator |
|
57 |
+ |
|
|
58 |
+ |
Two first-class paths (both normative in [SPEC.md](SPEC.md#adoption-paths)): |
| 44 |
59 |
|
|
| 45 |
|
- |
## Adopting it in an emulator (the shape) |
|
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. |
| 46 |
67 |
|
|
| 47 |
|
- |
Write the server once, adapt per fork. Each fork vendors `core/` and implements |
| 48 |
|
- |
~5 callbacks, then calls the server: |
|
68 |
+ |
The vendor path is one backend struct plus three hooks: |
| 49 |
69 |
|
|
| 50 |
70 |
|
```c |
| 51 |
71 |
|
#include "retro_control.h" |
| 52 |
72 |
|
|
| 53 |
|
- |
static retro_control_backend_t backend = { |
| 54 |
|
- |
.platform = "x16", |
| 55 |
|
- |
.read_mem = x16_read_mem, /* debug-read, no side effects */ |
| 56 |
|
- |
.get_regs_json = x16_get_regs_json, |
| 57 |
|
- |
.get_framebuffer= x16_get_framebuffer, |
| 58 |
|
- |
.step_frames = x16_step_frames, |
| 59 |
|
- |
.get_frame_count= x16_get_frame_count, |
|
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 */ |
| 60 |
86 |
|
}; |
| 61 |
87 |
|
|
| 62 |
|
- |
if (control_port) /* set by -controlport / --control-port */ |
|
88 |
+ |
if (control_port) /* set by -controlport / --control-port / env */ |
| 63 |
89 |
|
retro_control_start(control_port, &backend); |
| 64 |
90 |
|
``` |
| 65 |
91 |
|
|
| 66 |
|
- |
The socket loop, HTTP parsing, PPM encoding, JSON framing, and the |
| 67 |
|
- |
enqueue-to-emulator-thread marshalling live in the shared core — not in three |
| 68 |
|
- |
different codebases. |
|
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 |
+ |
``` |
| 69 |
101 |
|
|
| 70 |
102 |
|
## Status |
| 71 |
103 |
|
|
| 72 |
|
- |
Early. `SPEC.md` (0.1.0) and the interfaces below are the stable target the |
| 73 |
|
- |
forks build against; the server `.c` and conformance suite are landing next. |
| 74 |
|
- |
Reference implementation: the Commander X16 fork (`x16-emulator`). |
|
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. |