OXYGENHUB

docs: full sweep to 0.3.0 / four platforms 4c0b050e9a25

- README.md: rewrite the stale 0.1/"landing next" status — current endpoint
  table (incl. POST /mem, GET /audio, pause/resume), the real 8-callback vtable
  (the old example referenced a nonexistent step_frames), both adoption paths
  (submodule vendor + conform-only), four platforms.
- SPEC.md: four ports (was three); add pcfx to /status.platform; drop the stale
  "not required in 0.1" note; PC-FX framebuffer now documents the per-line
  LineWidths normalization.
- conformance/README.md: 0.3.x (was 0.1.x), 15 tests, add PC-FX, document the
  0.2/0.3 test categories (key/reset/write/audio) it already runs.
- core/retro_control.h: header comment said "Contract 0.1.0" — point at the
  RETRO_CONTROL_CONTRACT macro instead.
- client/emu_control.py: docstrings for the simple endpoint methods.
- CHANGELOG.md: new — the additive 0.1 -> 0.2 -> 0.3 history.

Git-Commit: 55f27a274dcad0766c28787d88e4753e74bfd568
Git-Author: Andy McCall <[email protected]>
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-07-21 23:08 UTC
Signature
◈ sealed by Andy McCall
Hash
4c0b050e9a255596eb36b5d40e9ad07da4a9e0a3f8827cf139b9ee8cf9bfc591
Depends on
8ac19619e24b 961b3c610c66 c17f852c2a5a d44319b1e712
Depended on by
8fd33bc520f9 bed0f1e524fe 346ca3ee83f4
In bookmarks
main perf-6
Edits
18 atoms, 1 tree operation, 5 files

Browse the tree at this change

added CHANGELOG.md +33 −0
1 +
# Changelog
2 +
3 +
Contract versions of the Retro Remote Debug Controller (see [SPEC.md](SPEC.md)).
4 +
Semantic versioning: MAJOR = breaking change to an existing endpoint, MINOR =
5 +
additive (new endpoint / optional field), PATCH = clarification. Every minor so
6 +
far has been purely additive — older clients keep working, and a server
7 +
advertises the highest level whose backend callbacks are all present via
8 +
`/status.contract`.
9 +
10 +
## 0.3.0
11 +
- `POST /mem?addr=&bank=` — write (poke) memory; bytes in the request body,
12 +
  returns `{"written":n}`. Backend callback: `write_mem`.
13 +
- `GET /audio` — drain synthesised PCM since the last call as a self-describing
14 +
  WAV (the audio analogue of `/screenshot`); `X-Rrdc-Audio-Dropped` header on
15 +
  ring overflow. Backend callback: `capture_audio`.
16 +
- The NEC PC-FX (Mednafen) fork joins as the fourth conforming emulator, the
17 +
  second to vendor the C core as a git submodule.
18 +
19 +
## 0.2.0
20 +
- `POST /key?text=|code=[&down=]` — inject input (press / release / tap).
21 +
  Backend callback: `inject_key`.
22 +
- `POST /reset` — soft/cold reset. Backend callback: `reset`.
23 +
- Contract gating introduced: a NULL callback returns 501 for its endpoint and
24 +
  lowers the advertised contract, keeping partial backends honest.
25 +
26 +
## 0.1.0
27 +
- Initial contract: `GET /status`, `GET /screenshot` (PPM), `GET /mem`,
28 +
  `GET /regs`, `POST /step`, `POST /pause` / `POST /resume`.
29 +
- Shared C core (`core/retro_control.{c,h}`) — sockets, HTTP, PPM/JSON encoding,
30 +
  and cooperative pause/step, driven by three loop hooks. Backend callbacks:
31 +
  `read_mem`, `get_regs_json`, `get_framebuffer`, `get_frame_count`.
32 +
- Shared pytest client (`client/emu_control.py`) and the platform-agnostic
33 +
  conformance suite. Reference implementation: the Commander X16 fork.
modified SPEC.md +13 −8
…
6 unchanged lines
7 7
A minimal, portable HTTP API that a retro-platform emulator exposes so an
8 8
external harness can screenshot the screen, read memory and registers, step the
9 9
machine deterministically, and inject input. The same contract is implemented
10 -
by the forked FAB Agon Emulator, the Neo6502 emulator, and the Commander X16
11 -
emulator, so **one** pytest harness drives all three ports.
10 +
by the forked FAB Agon Emulator, the Neo6502 emulator, the Commander X16
11 +
emulator, and the NEC PC-FX (Mednafen) emulator, so **one** pytest harness
12 +
drives all four ports.
12 13
13 14
## Principles
14 15
…
35 unchanged lines
50 51
}
51 52
```
52 53
- `contract` — this document's version. Clients MUST reject a mismatched MAJOR.
53 -
- `platform` — one of `x16`, `neo6502`, `agon` (extensible). Tells the client
54 -
  the memory/register model (see *Per-platform* appendix).
54 +
- `platform` — one of `x16`, `neo6502`, `agon`, `pcfx` (extensible). Tells the
55 +
  client the memory/register model (see *Per-platform* appendix).
55 56
- `frame` — monotonic completed-frame counter since boot. The determinism
56 57
  anchor: a screenshot taken at `frame == N` shows the state after N complete
57 58
  frames.
…
7 unchanged lines
65 66
  thread serves that snapshot.
66 67
- Dimensions are the platform's native framebuffer (appendix). Clients read
67 68
  `width`/`height` from the PPM header — do not hardcode.
68 -
- Reserved: `?frame=N` (block until frame N) — not required in 0.1.
69 +
- Reserved: `?frame=N` (block until frame N) — still reserved, not yet required.
69 70
70 71
### `GET /mem?addr=<a>&len=<n>[&bank=<b>]`
71 72
Raw memory bytes.
…
112 unchanged lines
184 185
- Address space: V810, flat **2 MB** main RAM (`0x000000–0x1FFFFF`); `addr`
185 186
  wraps in that space and `bank` is ignored.
186 187
- Registers: `pc` + `r0`–`r31` (33 × 32-bit), from the V810 core.
187 -
- Framebuffer: the active display rect (typically 256×240), decoded to RGB888
188 -
  by the fork with the surface's channel shifts, so it is correct regardless of
189 -
  Mednafen's display-dependent pixel order.
188 +
- Framebuffer: the active display, decoded to RGB888 by the fork with the
189 +
  surface's channel shifts (so it is correct regardless of Mednafen's
190 +
  display-dependent pixel order). The PC-FX varies horizontal resolution per
191 +
  scene (256 for the maze, up to 1024 for high-res text); the fork normalizes
192 +
  each scanline by Mednafen's per-line `LineWidths`, so the PPM is centred and
193 +
  complete at whatever width the scene uses. Height is the visible line count
194 +
  (~232). Clients read `width`/`height` from the PPM header.
190 195
- Audio: stereo, at Mednafen's configured output rate (`espec.SoundRate`),
191 196
  teed from the per-frame `SoundBuf`.
192 197
- Input: the PC-FX has a gamepad, not a keyboard, so `/key?text=<c>` and
…
29 unchanged lines
modified client/emu_control.py +8 −0
…
74 unchanged lines
75 75
76 76
    # -- endpoints ---------------------------------------------------------
77 77
    def status(self) -> dict:
78 +
        """GET /status — liveness + contract dict; raises on a MAJOR mismatch."""
78 79
        body, _ = self._get("/status")
79 80
        st = json.loads(body)
80 81
        major = int(str(st.get("contract", "0")).split(".")[0])
…
5 unchanged lines
86 87
        return st
87 88
88 89
    def frame(self) -> int:
90 +
        """Current completed-frame counter (the determinism anchor)."""
89 91
        return int(self.status()["frame"])
90 92
91 93
    def regs(self) -> dict:
94 +
        """GET /regs — CPU registers as a dict (keys are platform-specific)."""
92 95
        body, _ = self._get("/regs")
93 96
        return json.loads(body)
94 97
95 98
    def mem(self, addr: int, length: int, bank: int | None = None) -> bytes:
99 +
        """GET /mem — read `length` bytes at `addr` (optional `bank`)."""
96 100
        path = f"/mem?addr={addr}&len={length}"
97 101
        if bank is not None:
98 102
            path += f"&bank={bank}"
…
1 unchanged lines
100 104
        return body
101 105
102 106
    def step(self, frames: int = 1) -> int:
107 +
        """POST /step — advance `frames` complete frames, halt; returns the new
108 +
        frame counter. Meaningful once paused."""
103 109
        body = self._post(f"/step?frames={frames}")
104 110
        return int(json.loads(body)["frame"])
105 111
106 112
    def pause(self) -> None:
113 +
        """POST /pause — halt the machine (advances only via step())."""
107 114
        self._post("/pause")
108 115
109 116
    def resume(self) -> None:
117 +
        """POST /resume — return the machine to free-running."""
110 118
        self._post("/resume")
111 119
112 120
    def key(self, text: str | None = None, code: int | None = None,
…
52 unchanged lines
modified conformance/README.md +13 −6
1 1
# Conformance suite
2 2
3 -
pytest tests that assert an emulator correctly implements the control contract
4 -
(`../SPEC.md`, 0.1.x). Point them at any emulator — the reference x16emu, FAB,
5 -
the Neo6502 — and they check the same surface. Platform-agnostic: framebuffer
6 -
dimensions come from the PPM header, addresses use `$0000`, and `bank` is
7 -
exercised without requiring the platform to have banking.
3 +
pytest tests (15 of them) that assert an emulator correctly implements the
4 +
control contract (`../SPEC.md`, 0.3.x). Point them at any emulator — the
5 +
reference x16emu, FAB, the Neo6502, the PC-FX (Mednafen) fork — and they check
6 +
the same surface. Tests gated above the emulator's advertised contract skip
7 +
automatically, so a 0.1-only backend still passes. Platform-agnostic:
8 +
framebuffer dimensions come from the PPM header, addresses use `$0000`, and
9 +
`bank` is exercised without requiring the platform to have banking.
8 10
9 11
## Run
10 12
…
23 unchanged lines
34 36
- `/step` — deterministic: equal steps advance the frame counter by equal,
35 37
  positive amounts, and `/step` returns the halted counter.
36 38
- `/pause` + `/resume` — toggle the paused state.
37 -
- Errors — unknown path → 404; `GET /step` → 405.
39 +
- `/key` *(0.2)* — accepts `text=`/`code=` with optional `down=`, rejects a
40 +
  request with neither; is POST-only.
41 +
- `/reset` *(0.2)* — resets and returns `{"reset":true}`.
42 +
- `POST /mem` *(0.3)* — write→read round-trips; is POST-only.
43 +
- `/audio` *(0.3)* — returns a well-formed PCM WAV.
44 +
- Errors — unknown path → 404; `GET /step` / `GET /key` → 405.
38 45
39 46
## Environment
40 47
…
8 unchanged lines
modified core/retro_control.h +3 −1
…
2 unchanged lines
3 3
 * A retro-platform emulator implements the small read-only backend below and
4 4
 * calls retro_control_start(). The shared server (retro_control.c) owns the
5 5
 * socket loop, HTTP parsing, PPM/JSON encoding, and control flow
6 -
 * (pause / step / free-run). It always binds 127.0.0.1. Contract 0.1.0.
6 +
 * (pause / step / free-run). It always binds 127.0.0.1. Contract version is
7 +
 * RETRO_CONTROL_CONTRACT (below); the server advertises the highest level whose
8 +
 * backend callbacks are all present.
7 9
 *
8 10
 * Integration is three calls in the emulator's main loop:
9 11
 *   retro_control_service();                 // once per iteration (cheap)
…
76 unchanged lines