OXYGENHUB

Home

Andy McCall edited this page · 5 revisions

Retro Remote Debug Controller

A tiny, portable HTTP control API for retro-platform emulators, and the shared tooling that speaks it. One contract → one test harness across every Doomsday One port (Commander X16, Neo6502, Agon, PC-FX — and whatever's next).

An emulator launched with a control-port flag exposes a localhost HTTP server:

EndpointMethodReturns
/statusGETcontract version, platform, frame counter, paused state, (0.6) capabilities
/screenshotGETPPM image of the live screen
/mem?addr=&len=&bank=GETraw memory bytes
/regsGETCPU registers (JSON)
/step?frames=POSTadvance N frames deterministically
/pause, /resumePOSThalt / free-run
/key, /resetPOST(0.2) inject a key; reset the machine
/mem?addr=&bank=POST(0.3) write (poke) memory
/audioGET(0.3) the sound since the last call, as a WAV
/pointerPOST / GET(0.4) move and click the pointer; read it back
/pad?index=POST / GET(0.5) present a virtual game controller; read it back
/perfGET / POST(0.6) performance metrics: how much of each frame the program uses, by phase; their settings

How it fits together

flowchart LR
    subgraph clients ["Clients"]
        py["pytest + emu_control.py"]
        curl["curl, scripts"]
        agent["an agent, an IDE"]
    end
    subgraph emu ["The emulator process"]
        subgraph srv ["server thread (retro_control.c)"]
            http["127.0.0.1 socket loop<br/>HTTP parse · PPM / JSON / WAV encode"]
        end
        subgraph emut ["emulator thread"]
            svc["retro_control_service()<br/>once a loop iteration"]
            be["the backend's callbacks<br/>read_mem · get_framebuffer · get_regs_json<br/>inject_key · set_pad · get_perf …"]
            mach["the machine"]
            svc --> be --> mach
        end
        http -- "queued request" --> svc
        svc -- "reply" --> http
    end
    py & curl & agent -- "HTTP" --> http

The server never touches machine state on its own thread: every read and write runs inside retro_control_service() on the emulator's thread, so what a client sees is one consistent instant of the machine.

The contract grows in levels, and a server advertises the highest one its backend serves in full. /status.capabilities (0.6) lists each group it serves, whatever the level:

flowchart LR
    l1["0.1<br/>status · screenshot · mem · regs<br/>step · pause · resume"] --> l2["0.2<br/>key · reset"]
    l2 --> l3["0.3<br/>mem writes · audio"] --> l4["0.4<br/>pointer"] --> l5["0.5<br/>pad"] --> l6["0.6<br/>perf · capabilities"]

Why this exists

Every Doomsday One retro port chases frame-exact arcade fidelity, and each one grew its own fragile way to inspect state headlessly — write to the SD card, dump 64 KB of RAM on a magic jump, grep stdout (which lies). This replaces all of that with one contract:

  • GET /mem → assert game state (sprite table, actor positions).
  • GET /screenshot → Pillow → assert pixels, or just catch "nothing rendered" (the exact class of bug that otherwise costs an afternoon).
  • POST /step → deterministic, frame-addressable capture, so CI diffs don't flake against a free-running 60 Hz loop.
  • GET /perf → how much of each frame the program spends, and on what: a speed regression becomes a failing test instead of a player's complaint.

The payoff is uniformity: the same emu_control.py client and the same pytest patterns drive all three emulators.

Pages

Repo

Source: doomsdayone/retro-remote-debug-controller. Contract version 0.6.0. Reference implementations: the Commander X16 fork (0.5.0) and the enhanced X16 emulator (0.6.0, with /perf).