{"number":2,"title":"The RRDC control port, with contract 0.6's performance metrics (GET/POST /perf) (#1)","body":"Closes #1.\n\n**The RRDC control port (`-controlport <port>`)**, serving contract 0.6.\n- **The core:** vendored as an oxy submodule at `src/extern/retro-remote-debug-controller`, following RRDC's `perf-6` bookmark until retro-remote-debug-controller #7 lands.\n- **The backend (`src/control_backend.c`):** ported from the plain fork's. It covers memory, registers, the screen, stepping, keys, reset, audio, the pointer and pads; the pads go through this emulator's own virtual joysticks.\n- **`GET` and `POST /perf`** answer from the measurements the Performance panel and DAP make. DAP's JSON builders moved into `perf_json.c`, so both surfaces give the same fields. Added for `/perf`:\n  - each zone's statistics per window;\n  - raw frames `since=` a frame;\n  - each scanline's bus clocks (`lines=1`);\n  - the sprite renderer's time, not exposed remotely before.\n- **`perf_budget_zone_add_marker`:** a client names a guest marker region before the guest enters it.\n- **Hooks:** the main loop serves the port's requests between instructions and closes `/step` after the budget closes its frame. The Windows drag path stands aside while the port is on, as it does for the debuggers.\n- **CMake:** the sources and RRDC's include path. `/perf` comes with cJSON (`USE_DAP`); without it the port serves 0.5.\n\n**Two fixes in the measurement code, found by measuring Metal Gear:**\n1. **A framed guest's regions carry across the frame boundary.** The module's frame runs vsync to vsync, while a game's starts where it waits, so its work runs across the boundary. Closing regions there handed the rest of the phase then running to no zone: Metal Gear's \"common\" read 2.8 scanlines against the true 4.3 its cycle counter gives. A guest that frames its work with `$F0` … `$F1` now keeps its regions until its own next `$F0`/`$F1`. A guest that never writes `$F0` has them closed at the boundary, as before.\n2. **An interrupt after `$F1` is work.** In marker mode, cycles in an IRQ handler during the idle span counted as idle, so main (work − IRQ) came up short. The docs already said interrupt cycles are never idle.\n\n**Docs:** README (the control port, `/perf`, a run recipe) and `docs/perf-budget.md` (named regions, the framed-region rule, the IRQ rule, and where it's implemented).\n\n**Tests:**\n- **Unit tests:** 40 of 40. `perf_budget` passes 113 checks, with new ones for the framed carry, the IRQ rule and named markers. The old \"a region left open is closed by the frame boundary\" test is now two cases: unframed (still closed) and framed (closed at the guest's next `$F0`).\n- **RRDC's conformance suite:** 27 of 27, including the five 0.6 tests.\n- **`testbench/test_dap.py`:** 84 passed, 0 failed, 1 skipped (needs `-bp`), so DAP is unchanged by the builders' move.\n- **Metal Gear's harness on this build:** `test_smoke` and `test_frame_cost` (64 tests) and `test_perf_markers`. Its per-phase means from `/perf` match the guest's cycle counter to within 0.1 of a scanline.","author":"andymccall","state":"landed","target":"main","source":{"kind":"bookmark","bookmark":"rrdc-perf"},"changes":[{"hash":"2980b7b6d7cdf3928225cc12b965cc5c497b02b98a2ab5cf271549a7dfbe0c1e","message":"feat: the RRDC control port (-controlport), contract 0.6: GET/POST /perf from the Performance panel's measurements, built by perf_json.c with DAP's x16/perfStats; a guest marker region named in advance; a framed guest's regions carried across the frame boundary and an interrupt after $F1 counted as work (#1)","author":{"name":"Andy McCall","email":"mailme@andymccall.co.uk","key":"8bc05e0a32a86508708a7dffa1fdccee1f6601e8c8395275244a6c48a9684c8c","account":null,"note":"signed by a key not linked to any account; its owner links it under Settings → Keys (/settings/keys)"},"when_ms":1791665842082,"deps":["9120e87480a65aa6063a8292fe68f2f7ab935e4368042bb93bb702ccb3a7c4f9"],"head":false,"signature":{"status":"valid","key":"8bc05e0a32a86508708a7dffa1fdccee1f6601e8c8395275244a6c48a9684c8c","sealed":false,"revoked":false}}],"projected_heads":["2980b7b6d7cdf3928225cc12b965cc5c497b02b98a2ab5cf271549a7dfbe0c1e"],"projected_state":"230bfa33f3d9e13ca6e67e81d83ed94c1eeef9eaef4032854a9c0d1751b38090","conflicted":false,"approvals":[],"landable":false,"blockers":[],"created_ms":1791671556375,"updated_ms":1791671813348,"url":"https://oxygenhub.io/doomsdayone/enhanced-x16-emulator/proposal/2","repository_url":"https://oxygenhub.io/doomsdayone/enhanced-x16-emulator","source_url":null,"landed":{"by":"andymccall","via":"web","when_ms":1791671813348},"auto_land":null,"requested_reviewers":[],"labels":[]}