OXYGENHUB

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