docs: bring SPEC/README up to 0.5.0 and record the no-pointer gap 398756c0e9b2
SPEC.md's header still declared "Contract version: 0.4.0" while the body documented POST/GET /pad, so a client asserting on the header would misread a 0.5 server. Fixed there, in the /status example, and in the adoption-paths callback ladder (which stopped at set_pointer/get_pointer). README's status block said 0.4.0 across four emulators with an 18-test suite; it is 0.5.0 and 22 tests, and the x16 fork is no longer "callbacks not yet wired" — it ships the full 0.5 set. Also records a real gap found running the suite against the PC-FX fork. Levels are advertised cumulatively — a level is claimed only when all of its callbacks are present — so a platform that lacks a device cannot advertise past it without supplying a stub. The PC-FX has no pointing device: it stubs /pointer to 400 so it can advertise 0.5.0 for the /pad support it genuinely has, but the suite gates the pointer tests on minor >= 4, so they then run and fail. Measured: 19 passed, 3 failed, the three being pointer tests. Leaving the callbacks NULL is no better -- /pointer would 501 but the advertised contract drops to 0.3.0, hiding real /pad support. Both options misreport. A single cumulative integer cannot express "has pad, has no pointer". The fix is either a per-capability declaration in /status or a conformance rule that treats a documented "no such device" 400 as a skip; until one lands the gap is at least written down rather than silently wrong. The PC-FX platform appendix gains its /pad mapping, the pointer position, the audio note (-sound 0 yields a valid but header-only 44-byte WAV, so an "did /audio return 200" assertion passes against silence), and the ConfigOrder trap behind the pad bit layout. Git-Commit: 325d9a257381c8881da00c7314fe7a3b1190dcb5 Git-Author: Andy McCall <[email protected]>
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-07-29 00:14 UTC
- Signature
- ◈ sealed by Andy McCall
- Hash
398756c0e9b2f3532c7c81dd2fb3be39d579b1b0528363a3f5faf7c14b21ebd0- Depends on
- 346ca3ee83f4 674b8447022b 961b3c610c66 c17f852c2a5a
- Depended on by
- 8fd33bc520f9
- In bookmarks
- main perf-6
- Edits
- 8 atoms, 0 tree operations, 2 files
Browse the tree at this change
modified README.md +18 −6
| … | 105 unchanged lines |
||
| 106 | 106 | ||
| 107 | 107 | ## Status |
|
| 108 | 108 | ||
| 109 | - | Contract **0.4.0**, implemented across **four** emulators. The shared server and |
|
| 110 | - | the conformance suite (18 tests) are complete; the three 0.4 pointer tests are |
|
| 111 | - | exercised by the Neo6502 fork (the first 0.4 consumer) and skip on servers still |
|
| 112 | - | at 0.3. Reference implementation: the Commander X16 fork (`x16-emulator`), which |
|
| 113 | - | vendors `core/` as a git submodule; the NEC PC-FX fork (Mednafen) does the same, |
|
| 114 | - | and the FAB Agon emulator conforms directly in Rust. |
|
| 109 | + | Contract **0.5.0**, implemented across **four** emulators. The shared server and |
|
| 110 | + | the conformance suite (22 tests) are complete; tests gated *above* the server's |
|
| 111 | + | advertised level skip, so a 0.1-only backend still passes. |
|
| 112 | + | ||
| 113 | + | Reference implementation: the Commander X16 fork (`x16-emulator`), which vendors |
|
| 114 | + | `core/` as a git submodule; the NEC PC-FX fork (Mednafen) does the same, and the |
|
| 115 | + | FAB Agon emulator conforms directly in Rust. |
|
| 116 | + | ||
| 117 | + | > **Known gap — cumulative levels vs. absent devices.** A level is advertised |
|
| 118 | + | > only when *all* its callbacks are present, so a platform that lacks a device |
|
| 119 | + | > cannot advertise past it without supplying a stub. The PC-FX has no pointing |
|
| 120 | + | > device: it stubs `/pointer` to 400 in order to advertise **0.5.0** for the |
|
| 121 | + | > `/pad` support it genuinely has — but the suite gates the pointer tests on |
|
| 122 | + | > `minor >= 4`, so they then *run* and fail. Measured against the PC-FX fork: |
|
| 123 | + | > **19 passed, 3 failed**, the three being pointer tests. A single cumulative |
|
| 124 | + | > integer cannot express "has pad, has no pointer"; this needs either a |
|
| 125 | + | > per-capability declaration in `/status` or a conformance rule that treats a |
|
| 126 | + | > documented "no such device" 400 as a skip. |
modified SPEC.md +30 −13
| 1 | 1 | # Retro Remote Debug Controller — HTTP Control Contract |
|
| 2 | 2 | ||
| 3 | - | **Contract version: 0.4.0** (semver; clients assert on the MAJOR). 0.4 adds |
|
| 4 | - | pointer injection (`POST /pointer`, `GET /pointer`); 0.3 added memory write |
|
| 5 | - | (`POST /mem`) and an audio drain (`GET /audio`); 0.2 added input injection |
|
| 6 | - | (`POST /key`, `POST /reset`). Every minor is purely additive, so older clients |
|
| 7 | - | keep working and servers advertise the highest level whose callbacks are all |
|
| 8 | - | present via `/status.contract`. |
|
| 3 | + | **Contract version: 0.5.0** (semver; clients assert on the MAJOR). 0.5 adds a |
|
| 4 | + | virtual game controller (`POST /pad`, `GET /pad`); 0.4 added pointer injection |
|
| 5 | + | (`POST /pointer`, `GET /pointer`); 0.3 added memory write (`POST /mem`) and an |
|
| 6 | + | audio drain (`GET /audio`); 0.2 added input injection (`POST /key`, |
|
| 7 | + | `POST /reset`). Every minor is purely additive, so older clients keep working and |
|
| 8 | + | servers advertise the highest level whose callbacks are all present via |
|
| 9 | + | `/status.contract`. |
|
| 9 | 10 | ||
| 10 | 11 | A minimal, portable HTTP API that a retro-platform emulator exposes so an |
|
| 11 | 12 | external harness can screenshot the screen, read memory and registers, step the |
|
| … | 33 unchanged lines |
||
| 45 | 46 | Liveness + contract negotiation. Always available. |
|
| 46 | 47 | ```json |
|
| 47 | 48 | { |
|
| 48 | - | "contract": "0.4.0", |
|
| 49 | + | "contract": "0.5.0", |
|
| 49 | 50 | "emulator": "x16emu", |
|
| 50 | 51 | "platform": "x16", |
|
| 51 | 52 | "frame": 12345, |
|
| … | 167 unchanged lines |
||
| 219 | 220 | mixed `audio_render()` output. |
|
| 220 | 221 | - Pointer: `/pointer` maps to the KERNAL/VERA mouse — absolute/relative position |
|
| 221 | 222 | and buttons fed through the same path the host mouse uses, so `MOUSE_GET` |
|
| 222 | - | (and VERA sprite 0) see it. Coordinates are screen pixels. (Backend |
|
| 223 | - | callbacks not yet wired — until then the X16 fork advertises 0.3.0 and |
|
| 224 | - | `/pointer` returns 501.) |
|
| 223 | + | (and VERA sprite 0) see it. Coordinates are screen pixels. |
|
| 224 | + | - Pad: `/pad` presents a virtual controller through the emulator's own joystick |
|
| 225 | + | read path. The fork implements the full 0.5 callback set and advertises |
|
| 226 | + | **0.5.0**. |
|
| 225 | 227 | ||
| 226 | 228 | ### Neo6502 — neo6502 emulator |
|
| 227 | 229 | - Flag: `--control-port <N>`. |
|
| … | 54 unchanged lines |
||
| 282 | 284 | `1`–`6` = buttons I–VI. A held button is re-asserted each frame (the frontend |
|
| 283 | 285 | rewrites the pad from physical input every frame), so holds persist across |
|
| 284 | 286 | `/step`. `/reset` power-cycles the machine. |
|
| 287 | + | - Pad: `/pad` remaps the canonical mask to the FX pad word (`I=0 II=1 III=2 IV=3 |
|
| 288 | + | V=4 VI=5 SELECT=6 RUN=7 UP=8 RIGHT=9 DOWN=10 LEFT=11`; A/B → I/II, X/Y → |
|
| 289 | + | III/IV, L/R → V/VI, START → RUN) and OR-merges it into the pad read every |
|
| 290 | + | frame, as a level. **Trap:** that layout is implicit in the *order* of |
|
| 291 | + | Mednafen's `PCFX_GamepadIDII` array — the third `IDIIS_Button` argument is |
|
| 292 | + | `ConfigOrder`, not a bit offset. Using those numbers as bits binds every button |
|
| 293 | + | wrongly and is a defect currently present in the fork. |
|
| 294 | + | - Pointer: the PC-FX has no pointing device. Because 0.5.0 advertises |
|
| 295 | + | cumulatively, the fork supplies refusing stubs (`/pointer` → **400**) rather |
|
| 296 | + | than NULL callbacks, so it can honestly advertise **0.5.0** for `/pad`. |
|
| 297 | + | - Audio: `/audio` is fed by the mixer, which only runs when sound is enabled. |
|
| 298 | + | Launching with `-sound 0` yields a valid but **header-only (44-byte)** WAV, not |
|
| 299 | + | an error — use `-sound 1 -sound.driver dummy` for headless audio capture. |
|
| 285 | 300 | ||
| 286 | 301 | --- |
|
| 287 | 302 | ||
| … | 4 unchanged lines |
||
| 292 | 307 | ||
| 293 | 308 | - **Vendor the shared core** (`core/retro_control.c` + `.h`) — for C/C++ |
|
| 294 | 309 | emulators. Implement the backend callbacks (four are enough for 0.1; add |
|
| 295 | - | `inject_key`/`reset` for 0.2, `write_mem`/`capture_audio` for 0.3, and |
|
| 296 | - | `set_pointer`/`get_pointer` for 0.4 — a NULL callback makes its endpoint return |
|
| 297 | - | 501 and lowers the advertised contract), wire three loop hooks, call |
|
| 310 | + | `inject_key`/`reset` for 0.2, `write_mem`/`capture_audio` for 0.3, |
|
| 311 | + | `set_pointer`/`get_pointer` for 0.4, and `set_pad`/`get_pad` for 0.5 — a NULL |
|
| 312 | + | callback makes its endpoint return 501 and lowers the advertised contract, so a |
|
| 313 | + | platform lacking a device should supply a stub that returns 400 instead), wire |
|
| 314 | + | three loop hooks, call |
|
| 298 | 315 | `retro_control_start(port, &backend)`; the socket loop, HTTP parsing, PPM/JSON |
|
| 299 | 316 | encoding and pause/step control come for free. The Commander X16 fork does |
|
| 300 | 317 | this. |
|
| … | 12 unchanged lines |