OXYGENHUB

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