0.4.0: pointer injection (POST/GET /pointer) 346ca3ee83f4
Add the pointer-injection contract level. POST /pointer moves/clicks the pointer through the platform's native mouse path (absolute x,y or relative dx,dy, optional buttons bitmask); GET /pointer reads it back so a test can distinguish "input never arrived" from "wrong output drawn". - core: two backend callbacks (set_pointer, get_pointer); /pointer routing and marshalled handlers; the status ladder advertises 0.4.0 only when both are present, so a partial backend stays at 0.3.0 and 501s the endpoint. - client: EmuControl.pointer() / .pointer_get(). - conformance: pointer injection + readback + GET-is-GET, gated on minor >= 4. - SPEC/README/CHANGELOG: the endpoints, the button bitmask (bit0=primary, bit1=secondary), and per-platform pointer mapping (Neo concrete, X16 planned). Closes #1. Git-Commit: ad1717c90f2d9d406d2f51fabbd4ba9a7cbfd5d5 Git-Author: Andy McCall <[email protected]>
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-07-22 19:38 UTC
- Signature
- ◈ sealed by Andy McCall
- Hash
346ca3ee83f41f9495ac5380a55fe6fac1fa0369c3535942913f89d75c1f9f32- Depends on
- 4c0b050e9a25 67065eaa1a13 674b8447022b 689a9ccb0138 8ac19619e24b 961b3c610c66 d44319b1e712
- Depended on by
- 8fd33bc520f9 398756c0e9b2 bed0f1e524fe
- In bookmarks
- main perf-6
- Edits
- 28 atoms, 0 tree operations, 7 files
Browse the tree at this change
modified CHANGELOG.md +11 −0
| … | 6 unchanged lines |
||
| 7 | 7 | advertises the highest level whose backend callbacks are all present via |
|
| 8 | 8 | `/status.contract`. |
|
| 9 | 9 | ||
| 10 | + | ## 0.4.0 |
|
| 11 | + | - `POST /pointer?x=&y=[&buttons=]` (absolute) or `?dx=&dy=[&buttons=]` (relative) |
|
| 12 | + | — inject a pointer move/click through the platform's native mouse path. |
|
| 13 | + | `buttons` is a bitmask (bit0=primary, bit1=secondary); omit for a pure move. |
|
| 14 | + | Backend callback: `set_pointer`. |
|
| 15 | + | - `GET /pointer` — read back the current pointer `{x, y, buttons}`, so a test can |
|
| 16 | + | tell "input never arrived" from "wrong output drawn". Backend callback: |
|
| 17 | + | `get_pointer`. |
|
| 18 | + | - First consumer: the Neo6502 fork (drives the firmware mouse), unblocking the |
|
| 19 | + | pointer floor of the `rstar-api` RWM/RGM toolkit. |
|
| 20 | + | ||
| 10 | 21 | ## 0.3.0 |
|
| 11 | 22 | - `POST /mem?addr=&bank=` — write (poke) memory; bytes in the request body, |
|
| 12 | 23 | returns `{"written":n}`. Backend callback: `write_mem`. |
|
| … | 21 unchanged lines |
modified README.md +14 −8
| … | 16 unchanged lines |
||
| 17 | 17 | | `POST /key`, `POST /reset` | inject input / reset | 0.2 | |
|
| 18 | 18 | | `POST /mem?addr=&bank=` | write (poke) memory | 0.3 | |
|
| 19 | 19 | | `GET /audio` | drain synthesised PCM as a WAV | 0.3 | |
|
| 20 | + | | `POST /pointer?x=&y=&buttons=` | inject a pointer move/click | 0.4 | |
|
| 21 | + | | `GET /pointer` | read back pointer position + buttons | 0.4 | |
|
| 20 | 22 | ||
| 21 | - | Current contract: **0.3.0**. Minors are purely additive (a missing capability |
|
| 23 | + | Current contract: **0.4.0**. Minors are purely additive (a missing capability |
|
| 22 | 24 | returns 501 and lowers the advertised contract), so a client reads |
|
| 23 | 25 | `/status.contract` and fails fast only on a MAJOR mismatch. See [SPEC.md](SPEC.md). |
|
| 24 | 26 | ||
| … | 45 unchanged lines |
||
| 70 | 72 | ```c |
|
| 71 | 73 | #include "retro_control.h" |
|
| 72 | 74 | ||
| 73 | - | /* Four callbacks satisfy 0.1; add inject_key/reset for 0.2 and |
|
| 74 | - | write_mem/capture_audio for 0.3. A NULL callback 501s its endpoint. */ |
|
| 75 | + | /* Four callbacks satisfy 0.1; add inject_key/reset for 0.2, |
|
| 76 | + | write_mem/capture_audio for 0.3, set_pointer/get_pointer for 0.4. |
|
| 77 | + | A NULL callback 501s its endpoint. */ |
|
| 75 | 78 | static const retro_control_backend_t backend = { |
|
| 76 | 79 | .platform = "x16", |
|
| 77 | 80 | .emulator = "x16emu", |
|
| … | 5 unchanged lines |
||
| 83 | 86 | .reset = x16_reset, /* 0.2 */ |
|
| 84 | 87 | .write_mem = x16_write_mem, /* 0.3 */ |
|
| 85 | 88 | .capture_audio = x16_capture_audio, /* 0.3 */ |
|
| 89 | + | .set_pointer = x16_set_pointer, /* 0.4 */ |
|
| 90 | + | .get_pointer = x16_get_pointer, /* 0.4 */ |
|
| 86 | 91 | }; |
|
| 87 | 92 | ||
| 88 | 93 | if (control_port) /* set by -controlport / --control-port / env */ |
|
| … | 12 unchanged lines |
||
| 101 | 106 | ||
| 102 | 107 | ## Status |
|
| 103 | 108 | ||
| 104 | - | Contract **0.3.0**, implemented across **four** emulators. The shared server and |
|
| 105 | - | the conformance suite (15 tests, green against the reference) are complete. |
|
| 106 | - | Reference implementation: the Commander X16 fork (`x16-emulator`), which vendors |
|
| 107 | - | `core/` as a git submodule; the NEC PC-FX fork (Mednafen) does the same, and the |
|
| 108 | - | FAB Agon emulator conforms directly in Rust. |
|
| 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. |
modified SPEC.md +55 −11
| 1 | 1 | # Retro Remote Debug Controller — HTTP Control Contract |
|
| 2 | 2 | ||
| 3 | - | **Contract version: 0.3.0** (semver; clients assert on the MAJOR). 0.3 adds |
|
| 4 | - | memory write (`POST /mem`) and an audio drain (`GET /audio`); 0.2 added input |
|
| 5 | - | injection (`POST /key`, `POST /reset`). Every minor is purely additive, so older |
|
| 6 | - | clients keep working and servers advertise the highest level whose callbacks are |
|
| 7 | - | all present via `/status.contract`. |
|
| 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`. |
|
| 8 | 9 | ||
| 9 | 10 | A minimal, portable HTTP API that a retro-platform emulator exposes so an |
|
| 10 | 11 | external harness can screenshot the screen, read memory and registers, step the |
|
| … | 33 unchanged lines |
||
| 44 | 45 | Liveness + contract negotiation. Always available. |
|
| 45 | 46 | ```json |
|
| 46 | 47 | { |
|
| 47 | - | "contract": "0.3.0", |
|
| 48 | + | "contract": "0.4.0", |
|
| 48 | 49 | "emulator": "x16emu", |
|
| 49 | 50 | "platform": "x16", |
|
| 50 | 51 | "frame": 12345, |
|
| … | 79 unchanged lines |
||
| 130 | 131 | Injection plus `/step` retires scripted-autoexec input: press → step → assert → |
|
| 131 | 132 | release, deterministically. |
|
| 132 | 133 | ||
| 134 | + | ### `POST /pointer?x=<n>&y=<n> | ?dx=<n>&dy=<n> [&buttons=<mask>]` *(0.4)* |
|
| 135 | + | Inject a pointer (mouse) move and/or click through the platform's native pointer |
|
| 136 | + | path — the mouse analogue of `/key`. Query params (no body): |
|
| 137 | + | - `x`, `y` — **absolute** pointer position, in the platform's pointer coordinate |
|
| 138 | + | space (framebuffer pixels; appendix). Provide both. |
|
| 139 | + | - `dx`, `dy` — **relative** motion applied to the current position; either may be |
|
| 140 | + | omitted (treated as 0). `x`/`y` and `dx`/`dy` are alternatives; provide one |
|
| 141 | + | pair. Absolute is preferred for tests ("put the cursor on the button" beats |
|
| 142 | + | dead-reckoning). |
|
| 143 | + | - `buttons` — OPTIONAL bitmask, **bit0 = primary, bit1 = secondary**. Omitted → |
|
| 144 | + | the button state is left unchanged (a pure move). Provide `buttons=0` to |
|
| 145 | + | release. Hold a button across `/step` frames (re-assert it each move) so a |
|
| 146 | + | program that samples the pointer per frame sees the press. |
|
| 147 | + | - Response: `{ "injected": true }`. No pointer on the platform → 400. |
|
| 148 | + | ||
| 149 | + | Device-level, not driver-state: the injection MUST exercise the same path a real |
|
| 150 | + | mouse takes (e.g. the KERNAL/VERA mouse on the X16, the firmware mouse on the |
|
| 151 | + | Neo6502), so what the program reads is what a physical mouse would produce. |
|
| 152 | + | Determinism: inject under `/pause`, then `/step` — the move lands at the frame |
|
| 153 | + | boundary, so screenshot-diffing a hovered/clicked UI is reproducible. |
|
| 154 | + | ||
| 155 | + | ### `GET /pointer` *(0.4)* |
|
| 156 | + | Read back the current pointer state. Response: |
|
| 157 | + | ```json |
|
| 158 | + | { "x": 42, "y": 24, "buttons": 1 } |
|
| 159 | + | ``` |
|
| 160 | + | - `x`, `y` — current position in the same coordinate space as `POST /pointer`. |
|
| 161 | + | - `buttons` — the current button bitmask (same bit assignment). |
|
| 162 | + | - Lets a test distinguish "the input never reached the platform" from "it |
|
| 163 | + | arrived but the program drew the wrong thing". No pointer on the platform → 400. |
|
| 164 | + | ||
| 133 | 165 | ### `POST /reset` *(0.2)* |
|
| 134 | 166 | Soft/cold reset the machine. Response `{ "reset": true }`. The `frame` counter |
|
| 135 | 167 | MAY reset here (the one case `/status.frame` is allowed to go backwards). |
|
| … | 49 unchanged lines |
||
| 185 | 217 | - Frame source: `video.c` `frame_count`. |
|
| 186 | 218 | - Audio: stereo (2 ch), native rate `25000000/512` ≈ **48828 Hz**, teed from the |
|
| 187 | 219 | mixed `audio_render()` output. |
|
| 220 | + | - Pointer: `/pointer` maps to the KERNAL/VERA mouse — absolute/relative position |
|
| 221 | + | 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.) |
|
| 188 | 225 | ||
| 189 | 226 | ### Neo6502 — neo6502 emulator |
|
| 190 | 227 | - Flag: `--control-port <N>`. |
|
| 191 | 228 | - Address space: flat 64 KB; `bank` ignored. |
|
| 192 | 229 | - Registers: 6502 `a x y sp pc status`. |
|
| 193 | 230 | - Framebuffer: native resolution per the emulator's video mode. |
|
| 231 | + | - Pointer: `/pointer` drives the firmware mouse — absolute → `MSESetPosition`, |
|
| 232 | + | relative → `MSEOffsetPosition`, buttons → `MSEUpdateButtonState`; `GET /pointer` |
|
| 233 | + | reads `MSEGetState`. Coordinates are firmware graphics pixels (the same space a |
|
| 234 | + | program reads). Buttons: bit0 = left, bit1 = right (the firmware's native |
|
| 235 | + | masks), matching the contract's primary/secondary. The emulator enables the |
|
| 236 | + | mouse at reset, so injection works from boot. |
|
| 194 | 237 | ||
| 195 | 238 | ### Agon (Console8 / Light 2) — FAB Agon Emulator |
|
| 196 | 239 | - **Adoption:** conform-only (Rust emulator — keeps its own HTTP server, matches |
|
| … | 52 unchanged lines |
||
| 249 | 292 | ||
| 250 | 293 | - **Vendor the shared core** (`core/retro_control.c` + `.h`) — for C/C++ |
|
| 251 | 294 | emulators. Implement the backend callbacks (four are enough for 0.1; add |
|
| 252 | - | `inject_key`/`reset` for 0.2 and `write_mem`/`capture_audio` for 0.3 — a NULL |
|
| 253 | - | callback makes its endpoint return 501 and lowers the advertised contract), |
|
| 254 | - | wire three loop hooks, call `retro_control_start(port, &backend)`; the socket |
|
| 255 | - | loop, HTTP parsing, PPM/JSON encoding and pause/step control come for free. |
|
| 256 | - | The Commander X16 fork does this. |
|
| 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 |
|
| 298 | + | `retro_control_start(port, &backend)`; the socket loop, HTTP parsing, PPM/JSON |
|
| 299 | + | encoding and pause/step control come for free. The Commander X16 fork does |
|
| 300 | + | this. |
|
| 257 | 301 | - **Conform directly** — for emulators in other languages (the FAB Agon emulator |
|
| 258 | 302 | is Rust). Keep your own HTTP server; just match the contract and pass the |
|
| 259 | 303 | conformance suite. Sharing the C core is an optimization, not a requirement — |
|
| … | 9 unchanged lines |
modified client/emu_control.py +27 −1
| 1 | 1 | """emu_control.py — shared client for the Retro Remote Debug Controller. |
|
| 2 | 2 | ||
| 3 | 3 | One client for every port's pytest suite. Speaks the HTTP contract in |
|
| 4 | - | ../SPEC.md (0.3.x). Depends only on the stdlib; Pillow is optional and only |
|
| 4 | + | ../SPEC.md (0.4.x). Depends only on the stdlib; Pillow is optional and only |
|
| 5 | 5 | needed for .screenshot(). |
|
| 6 | 6 | ||
| 7 | 7 | from emu_control import EmuControl |
|
| … | 143 unchanged lines |
||
| 151 | 151 | body, _ = self._get("/audio") |
|
| 152 | 152 | return body |
|
| 153 | 153 | ||
| 154 | + | def pointer(self, x: int | None = None, y: int | None = None, |
|
| 155 | + | dx: int | None = None, dy: int | None = None, |
|
| 156 | + | buttons: int | None = None) -> None: |
|
| 157 | + | """Inject a pointer move/click (contract 0.4). Give absolute (x, y) or |
|
| 158 | + | relative (dx, dy). `buttons` is a bitmask (bit0=primary, bit1=secondary); |
|
| 159 | + | omit it for a pure move (leaves the button state unchanged). Hold buttons |
|
| 160 | + | across step() frames for a program that samples the pointer each frame.""" |
|
| 161 | + | parts = [] |
|
| 162 | + | if x is not None and y is not None: |
|
| 163 | + | parts.append(f"x={x}") |
|
| 164 | + | parts.append(f"y={y}") |
|
| 165 | + | elif dx is not None or dy is not None: |
|
| 166 | + | parts.append(f"dx={dx if dx is not None else 0}") |
|
| 167 | + | parts.append(f"dy={dy if dy is not None else 0}") |
|
| 168 | + | else: |
|
| 169 | + | raise EmuControlError("pointer() needs x= and y=, or dx=/dy=") |
|
| 170 | + | if buttons is not None: |
|
| 171 | + | parts.append(f"buttons={buttons}") |
|
| 172 | + | self._post("/pointer?" + "&".join(parts)) |
|
| 173 | + | ||
| 174 | + | def pointer_get(self) -> dict: |
|
| 175 | + | """GET /pointer — the current pointer as {x, y, buttons} (contract 0.4). |
|
| 176 | + | `buttons` uses the same bitmask as pointer().""" |
|
| 177 | + | body, _ = self._get("/pointer") |
|
| 178 | + | return json.loads(body) |
|
| 179 | + | ||
| 154 | 180 | def screenshot_ppm(self) -> bytes: |
|
| 155 | 181 | """Return the live screen as raw PPM (P6) bytes. Stdlib-only.""" |
|
| 156 | 182 | body, _ = self._get("/screenshot") |
|
| … | 29 unchanged lines |
modified conformance/test_conformance.py +41 −0
| … | 151 unchanged lines |
||
| 152 | 152 | emu.resume() |
|
| 153 | 153 | ||
| 154 | 154 | ||
| 155 | + | # -- contract 0.4: pointer injection (skipped on < 0.4 servers) -------------- |
|
| 156 | + | ||
| 157 | + | def test_pointer_injection(emu): |
|
| 158 | + | """/pointer accepts absolute (x,y) and relative (dx,dy) with an optional |
|
| 159 | + | buttons= bitmask, and rejects a request with neither. The pointer's *effect* |
|
| 160 | + | is platform-specific and tested per-port; here we assert the contract |
|
| 161 | + | surface only.""" |
|
| 162 | + | if _contract_minor(emu) < 4: |
|
| 163 | + | pytest.skip("contract < 0.4 (no pointer injection)") |
|
| 164 | + | emu.pointer(x=10, y=20) # absolute move, buttons unchanged |
|
| 165 | + | emu.pointer(x=10, y=20, buttons=1) # primary button down |
|
| 166 | + | emu.pointer(x=10, y=20, buttons=0) # buttons released |
|
| 167 | + | emu.pointer(dx=4, dy=-2) # relative move |
|
| 168 | + | code, _ = _raw(emu, "POST", "/pointer") # neither x/y nor dx/dy |
|
| 169 | + | assert code == 400 |
|
| 170 | + | ||
| 171 | + | ||
| 172 | + | def test_pointer_readback(emu): |
|
| 173 | + | """GET /pointer reports {x, y, buttons} as ints (contract 0.4). Values are |
|
| 174 | + | platform-specific; we assert only shape.""" |
|
| 175 | + | if _contract_minor(emu) < 4: |
|
| 176 | + | pytest.skip("contract < 0.4") |
|
| 177 | + | emu.pause() |
|
| 178 | + | try: |
|
| 179 | + | emu.pointer(x=42, y=24, buttons=0) |
|
| 180 | + | emu.step(1) |
|
| 181 | + | p = emu.pointer_get() |
|
| 182 | + | assert {"x", "y", "buttons"} <= p.keys() |
|
| 183 | + | assert all(isinstance(p[k], int) for k in ("x", "y", "buttons")) |
|
| 184 | + | finally: |
|
| 185 | + | emu.resume() |
|
| 186 | + | ||
| 187 | + | ||
| 188 | + | def test_pointer_get_is_get(emu): |
|
| 189 | + | if _contract_minor(emu) < 4: |
|
| 190 | + | pytest.skip("contract < 0.4") |
|
| 191 | + | # GET /pointer is the readback path (200); POST is the inject path. |
|
| 192 | + | code, _ = _raw(emu, "GET", "/pointer") |
|
| 193 | + | assert code == 200 |
|
| 194 | + | ||
| 195 | + | ||
| 155 | 196 | def test_reset(emu): |
|
| 156 | 197 | """/reset responds and is POST-only. Runs last (it restarts the machine).""" |
|
| 157 | 198 | if _contract_minor(emu) < 2: |
|
| … | 4 unchanged lines |
modified core/retro_control.c +71 −1
| … | 22 unchanged lines |
||
| 23 | 23 | #include <pthread.h> |
|
| 24 | 24 | #include <unistd.h> |
|
| 25 | 25 | #include <errno.h> |
|
| 26 | + | #include <limits.h> |
|
| 26 | 27 | #include <signal.h> |
|
| 27 | 28 | #include <sys/socket.h> |
|
| 28 | 29 | #include <netinet/in.h> |
|
| … | 18 unchanged lines |
||
| 47 | 48 | ||
| 48 | 49 | /* single-slot marshalled request. */ |
|
| 49 | 50 | typedef enum { REQ_NONE = 0, REQ_MEM, REQ_REGS, REQ_SHOT, REQ_KEY, REQ_RESET, |
|
| 50 | - | REQ_WRITE, REQ_AUDIO } req_t; |
|
| 51 | + | REQ_WRITE, REQ_AUDIO, REQ_PTR_SET, REQ_PTR_GET } req_t; |
|
| 51 | 52 | static volatile req_t g_req = REQ_NONE; |
|
| 52 | 53 | static uint32_t g_req_addr, g_req_len; |
|
| 53 | 54 | static int32_t g_req_bank; |
|
| … | 1 unchanged lines |
||
| 55 | 56 | static int g_key_is_text; /* 1 => g_key_value is a char code point */ |
|
| 56 | 57 | static uint32_t g_key_value; |
|
| 57 | 58 | static int g_key_action; /* retro_key_action_t */ |
|
| 59 | + | static int g_ptr_absolute; /* 1 => (x,y) absolute; 0 => relative deltas */ |
|
| 60 | + | static int32_t g_ptr_x, g_ptr_y; |
|
| 61 | + | static int g_ptr_buttons; /* button bitmask, or -1 = leave unchanged */ |
|
| 58 | 62 | static char g_resp_extra[64]; /* extra response header line, or "" */ |
|
| 59 | 63 | ||
| 60 | 64 | static uint8_t *g_resp_body = NULL; |
|
| … | 157 unchanged lines |
||
| 218 | 222 | g_resp_ctype = "application/json"; |
|
| 219 | 223 | } |
|
| 220 | 224 | break; |
|
| 225 | + | case REQ_PTR_SET: { |
|
| 226 | + | const char *json; |
|
| 227 | + | if (!g_be->set_pointer) { |
|
| 228 | + | json = "{\"error\":\"not implemented\"}"; g_resp_status = 501; |
|
| 229 | + | } else if (g_be->set_pointer(g_ptr_absolute, g_ptr_x, g_ptr_y, g_ptr_buttons)) { |
|
| 230 | + | json = "{\"injected\":true}"; g_resp_status = 200; |
|
| 231 | + | } else { |
|
| 232 | + | json = "{\"error\":\"no pointer\"}"; g_resp_status = 400; |
|
| 233 | + | } |
|
| 234 | + | size_t l = strlen(json); |
|
| 235 | + | uint8_t *b = (uint8_t *)malloc(l + 1); |
|
| 236 | + | if (b) memcpy(b, json, l + 1); |
|
| 237 | + | g_resp_body = b; g_resp_len = b ? l : 0; |
|
| 238 | + | g_resp_ctype = "application/json"; |
|
| 239 | + | break; |
|
| 240 | + | } |
|
| 241 | + | case REQ_PTR_GET: { |
|
| 242 | + | char *b; |
|
| 243 | + | if (!g_be->get_pointer) { |
|
| 244 | + | const char *json = "{\"error\":\"not implemented\"}"; |
|
| 245 | + | size_t l = strlen(json); b = (char *)malloc(l + 1); |
|
| 246 | + | if (b) memcpy(b, json, l + 1); |
|
| 247 | + | g_resp_len = b ? l : 0; g_resp_status = 501; |
|
| 248 | + | } else { |
|
| 249 | + | int32_t x = 0, y = 0; int buttons = 0; |
|
| 250 | + | int ok = g_be->get_pointer(&x, &y, &buttons); |
|
| 251 | + | b = (char *)malloc(96); |
|
| 252 | + | if (!b) { g_resp_len = 0; g_resp_status = 500; } |
|
| 253 | + | else if (ok) { |
|
| 254 | + | g_resp_len = (size_t)snprintf(b, 96, |
|
| 255 | + | "{\"x\":%ld,\"y\":%ld,\"buttons\":%d}", |
|
| 256 | + | (long)x, (long)y, buttons); |
|
| 257 | + | g_resp_status = 200; |
|
| 258 | + | } else { |
|
| 259 | + | g_resp_len = (size_t)snprintf(b, 96, "{\"error\":\"no pointer\"}"); |
|
| 260 | + | g_resp_status = 400; |
|
| 261 | + | } |
|
| 262 | + | } |
|
| 263 | + | g_resp_body = (uint8_t *)b; |
|
| 264 | + | g_resp_ctype = "application/json"; |
|
| 265 | + | break; |
|
| 266 | + | } |
|
| 221 | 267 | default: break; |
|
| 222 | 268 | } |
|
| 223 | 269 | g_req = REQ_NONE; |
|
| … | 124 unchanged lines |
||
| 348 | 394 | unsigned long long fc = g_be->get_frame_count ? g_be->get_frame_count() : 0; |
|
| 349 | 395 | /* Advertise the highest level whose defining callbacks are all present. */ |
|
| 350 | 396 | const char *contract = |
|
| 397 | + | (g_be->inject_key && g_be->write_mem && g_be->capture_audio && |
|
| 398 | + | g_be->set_pointer && g_be->get_pointer) ? "0.4.0" : |
|
| 351 | 399 | (g_be->inject_key && g_be->write_mem && g_be->capture_audio) ? "0.3.0" : |
|
| 352 | 400 | g_be->inject_key ? "0.2.0" : "0.1.0"; |
|
| 353 | 401 | char buf[320]; |
|
| … | 139 unchanged lines |
||
| 493 | 541 | if (!strcmp(target, "/reset")) { |
|
| 494 | 542 | if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; } |
|
| 495 | 543 | do_marshalled(fd, REQ_RESET); |
|
| 544 | + | return; |
|
| 545 | + | } |
|
| 546 | + | if (!strcmp(target, "/pointer")) { |
|
| 547 | + | if (is_post) { /* 0.4: move/click the pointer */ |
|
| 548 | + | long x = query_long(query, "x", LONG_MIN); |
|
| 549 | + | long y = query_long(query, "y", LONG_MIN); |
|
| 550 | + | long dx = query_long(query, "dx", LONG_MIN); |
|
| 551 | + | long dy = query_long(query, "dy", LONG_MIN); |
|
| 552 | + | if (x != LONG_MIN && y != LONG_MIN) { |
|
| 553 | + | g_ptr_absolute = 1; g_ptr_x = (int32_t)x; g_ptr_y = (int32_t)y; |
|
| 554 | + | } else if (dx != LONG_MIN || dy != LONG_MIN) { |
|
| 555 | + | g_ptr_absolute = 0; |
|
| 556 | + | g_ptr_x = (int32_t)(dx == LONG_MIN ? 0 : dx); |
|
| 557 | + | g_ptr_y = (int32_t)(dy == LONG_MIN ? 0 : dy); |
|
| 558 | + | } else { |
|
| 559 | + | send_json(fd, 400, "{\"error\":\"pointer needs x&y or dx&dy\"}"); return; |
|
| 560 | + | } |
|
| 561 | + | g_ptr_buttons = (int)query_long(query, "buttons", -1); /* -1 = unchanged */ |
|
| 562 | + | do_marshalled(fd, REQ_PTR_SET); |
|
| 563 | + | } else { /* 0.4: read back the pointer */ |
|
| 564 | + | do_marshalled(fd, REQ_PTR_GET); |
|
| 565 | + | } |
|
| 496 | 566 | return; |
|
| 497 | 567 | } |
|
| 498 | 568 | send_json(fd, 404, "{\"error\":\"not found\"}"); |
|
| … | 56 unchanged lines |
modified core/retro_control.h +18 −4
| … | 26 unchanged lines |
||
| 27 | 27 | #include <stddef.h> |
|
| 28 | 28 | ||
| 29 | 29 | /* Highest contract the core implements. The server advertises the highest level |
|
| 30 | - | * whose backend callbacks are all present: 0.3.0 needs inject_key + write_mem + |
|
| 31 | - | * capture_audio; 0.2.0 needs inject_key; else 0.1.0. Keeps a partial backend |
|
| 32 | - | * honest. */ |
|
| 33 | - | #define RETRO_CONTROL_CONTRACT "0.3.0" |
|
| 30 | + | * whose backend callbacks are all present: 0.4.0 needs set_pointer + get_pointer |
|
| 31 | + | * (on top of 0.3); 0.3.0 needs inject_key + write_mem + capture_audio; 0.2.0 |
|
| 32 | + | * needs inject_key; else 0.1.0. Keeps a partial backend honest. */ |
|
| 33 | + | #define RETRO_CONTROL_CONTRACT "0.4.0" |
|
| 34 | 34 | ||
| 35 | 35 | /* Native framebuffer pixel layout; the server swizzles to PPM RGB. */ |
|
| 36 | 36 | typedef enum { |
|
| … | 58 unchanged lines |
||
| 95 | 95 | * Return the number of int16 samples written. NULL => /audio returns 501. */ |
|
| 96 | 96 | uint32_t (*capture_audio)(int16_t *out, uint32_t cap, |
|
| 97 | 97 | int *rate, int *channels, uint32_t *dropped); |
|
| 98 | + | ||
| 99 | + | /* 0.4: inject a pointer (mouse) move/click through the platform's native |
|
| 100 | + | * pointer path. `absolute` nonzero => (x,y) are absolute pointer |
|
| 101 | + | * coordinates (framebuffer/pointer space); zero => (x,y) are relative |
|
| 102 | + | * deltas applied to the current position. `buttons` is a bitmask |
|
| 103 | + | * (bit0 = primary, bit1 = secondary), or -1 to leave the button state |
|
| 104 | + | * unchanged (a pure move). Return 1 on success, 0 if the platform has no |
|
| 105 | + | * pointer (server -> 400). NULL => POST /pointer returns 501. */ |
|
| 106 | + | int (*set_pointer)(int absolute, int32_t x, int32_t y, int buttons); |
|
| 107 | + | ||
| 108 | + | /* 0.4: report the current pointer position and button bitmask (same |
|
| 109 | + | * semantics as set_pointer's `buttons`). Return 1 on success (values |
|
| 110 | + | * written), 0 if no pointer is available. NULL => GET /pointer returns 501. */ |
|
| 111 | + | int (*get_pointer)(int32_t *x, int32_t *y, int *buttons); |
|
| 98 | 112 | } retro_control_backend_t; |
|
| 99 | 113 | ||
| 100 | 114 | /* Start the server thread bound to 127.0.0.1:port. Non-blocking; 0 on success, |
|
| … | 20 unchanged lines |