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 |