OXYGENHUB

Core: implement 0.2 input injection (POST /key, POST /reset) 689a9ccb0138

Route /key and /reset through the emulator-thread marshal path; add two backend
callbacks (inject_key, reset) + a key-action enum + a query-string parser. The
server advertises 0.2.0 only when the backend provides inject_key, else 0.1.0,
so a 0.1-only backend stays honest.

Git-Commit: 00765cfd34d7b1ea957b14b7a0f9adff058a7190
Git-Author: Andy McCall <[email protected]>
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-07-19 19:39 UTC
Signature
◈ sealed by Andy McCall
Hash
689a9ccb013881493fa1b09406534311c6ee763e20f85a90e9edc9937aca1cc6
Depends on
961b3c610c66
Depended on by
8fd33bc520f9 45a475bb3fa8 346ca3ee83f4 674b8447022b
In bookmarks
main perf-6
Edits
12 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified core/retro_control.c +80 −3
…
44 unchanged lines
45 45
static long g_frames_to_run = -1;
46 46
static int  g_step_waiting  = 0;
47 47
48 -
/* single-slot marshalled request (mem / regs / screenshot) */
49 -
typedef enum { REQ_NONE = 0, REQ_MEM, REQ_REGS, REQ_SHOT } req_t;
48 +
/* single-slot marshalled request (mem / regs / screenshot / key / reset) */
49 +
typedef enum { REQ_NONE = 0, REQ_MEM, REQ_REGS, REQ_SHOT, REQ_KEY, REQ_RESET } req_t;
50 50
static volatile req_t g_req = REQ_NONE;
51 51
static uint32_t g_req_addr, g_req_len;
52 52
static int32_t  g_req_bank;
53 +
static int      g_key_is_text;   /* 1 => g_key_value is a char code point */
54 +
static uint32_t g_key_value;
55 +
static int      g_key_action;    /* retro_key_action_t */
53 56
54 57
static uint8_t    *g_resp_body  = NULL;
55 58
static size_t      g_resp_len   = 0;
…
64 unchanged lines
120 123
        if (g_be->get_framebuffer) build_ppm();
121 124
        else { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 501; }
122 125
        break;
126 +
    case REQ_KEY: {
127 +
        const char *json;
128 +
        if (!g_be->inject_key) {
129 +
            json = "{\"error\":\"not implemented\"}"; g_resp_status = 501;
130 +
        } else if (g_be->inject_key(g_key_is_text, g_key_value, g_key_action)) {
131 +
            json = "{\"injected\":true}"; g_resp_status = 200;
132 +
        } else {
133 +
            json = "{\"error\":\"unmapped key\"}"; g_resp_status = 400;
134 +
        }
135 +
        size_t l = strlen(json);
136 +
        uint8_t *b = (uint8_t *)malloc(l + 1);
137 +
        if (b) memcpy(b, json, l + 1);
138 +
        g_resp_body = b; g_resp_len = b ? l : 0;
139 +
        g_resp_ctype = "application/json";
140 +
        break;
141 +
    }
142 +
    case REQ_RESET: {
143 +
        const char *json;
144 +
        if (g_be->reset) { g_be->reset(); json = "{\"reset\":true}"; g_resp_status = 200; }
145 +
        else { json = "{\"error\":\"not implemented\"}"; g_resp_status = 501; }
146 +
        size_t l = strlen(json);
147 +
        uint8_t *b = (uint8_t *)malloc(l + 1);
148 +
        if (b) memcpy(b, json, l + 1);
149 +
        g_resp_body = b; g_resp_len = b ? l : 0;
150 +
        g_resp_ctype = "application/json";
151 +
        break;
152 +
    }
123 153
    default: break;
124 154
    }
125 155
    g_req = REQ_NONE;
…
75 unchanged lines
201 231
    return def;
202 232
}
203 233
234 +
/* copy the value of query key into out (NUL-terminated, truncated to cap).
235 +
 * Returns 1 if the key was present, 0 otherwise. */
236 +
static int query_str(const char *q, const char *key, char *out, size_t cap)
237 +
{
238 +
    size_t kl = strlen(key);
239 +
    const char *p = q;
240 +
    while (p && *p) {
241 +
        if (!strncmp(p, key, kl) && p[kl] == '=') {
242 +
            const char *v = p + kl + 1;
243 +
            const char *e = strchr(v, '&');
244 +
            size_t n = e ? (size_t)(e - v) : strlen(v);
245 +
            if (n >= cap) n = cap - 1;
246 +
            memcpy(out, v, n);
247 +
            out[n] = 0;
248 +
            return 1;
249 +
        }
250 +
        p = strchr(p, '&');
251 +
        if (p) p++;
252 +
    }
253 +
    if (cap) out[0] = 0;
254 +
    return 0;
255 +
}
256 +
204 257
/* marshal a read to the emulator thread, wait, send the response. */
205 258
static void do_marshalled(int fd, req_t r)
206 259
{
…
16 unchanged lines
223 276
    long f = g_frames_to_run;
224 277
    pthread_mutex_unlock(&g_lock);
225 278
    unsigned long long fc = g_be->get_frame_count ? g_be->get_frame_count() : 0;
279 +
    /* Advertise 0.2 only when the backend actually offers input injection. */
280 +
    const char *contract = g_be->inject_key ? "0.2.0" : "0.1.0";
226 281
    char buf[320];
227 282
    snprintf(buf, sizeof buf,
228 283
        "{\"contract\":\"%s\",\"emulator\":\"%s\",\"platform\":\"%s\","
229 284
        "\"frame\":%llu,\"paused\":%s,\"running\":true}",
230 -
        RETRO_CONTROL_CONTRACT,
285 +
        contract,
231 286
        g_be->emulator ? g_be->emulator : "",
232 287
        g_be->platform ? g_be->platform : "",
233 288
        fc, (f == 0) ? "true" : "false");
…
68 unchanged lines
302 357
    if (!strcmp(target, "/resume")) {
303 358
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
304 359
        do_setrun(fd, -1, 0); return;
360 +
    }
361 +
    if (!strcmp(target, "/key")) {
362 +
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
363 +
        char text[8];
364 +
        long code = query_long(query, "code", -1);
365 +
        long down = query_long(query, "down", -1);
366 +
        if (query_str(query, "text", text, sizeof text) && text[0]) {
367 +
            g_key_is_text = 1; g_key_value = (uint32_t)(unsigned char)text[0];
368 +
        } else if (code >= 0) {
369 +
            g_key_is_text = 0; g_key_value = (uint32_t)code;
370 +
        } else {
371 +
            send_json(fd, 400, "{\"error\":\"key needs text= or code=\"}"); return;
372 +
        }
373 +
        g_key_action = (down < 0) ? RETRO_KEY_TAP
374 +
                     : (down != 0) ? RETRO_KEY_DOWN : RETRO_KEY_UP;
375 +
        do_marshalled(fd, REQ_KEY);
376 +
        return;
377 +
    }
378 +
    if (!strcmp(target, "/reset")) {
379 +
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
380 +
        do_marshalled(fd, REQ_RESET);
381 +
        return;
305 382
    }
306 383
    send_json(fd, 404, "{\"error\":\"not found\"}");
307 384
}
…
55 unchanged lines
modified core/retro_control.h +20 −1
…
23 unchanged lines
24 24
#include <stdint.h>
25 25
#include <stddef.h>
26 26
27 -
#define RETRO_CONTROL_CONTRACT "0.1.0"
27 +
/* Highest contract the core implements. The server ADVERTISES 0.2.0 only when
28 +
 * the backend provides input injection (inject_key); otherwise it reports
29 +
 * 0.1.0, so a 0.1-only backend stays honest. */
30 +
#define RETRO_CONTROL_CONTRACT "0.2.0"
28 31
29 32
/* Native framebuffer pixel layout; the server swizzles to PPM RGB. */
30 33
typedef enum {
…
9 unchanged lines
40 43
    retro_pix_fmt_t fmt;
41 44
} retro_framebuffer_t;
42 45
46 +
/* Key action for inject_key (0.2). */
47 +
typedef enum {
48 +
    RETRO_KEY_TAP  = 0,   /* press then release */
49 +
    RETRO_KEY_DOWN = 1,   /* press / hold       */
50 +
    RETRO_KEY_UP   = 2    /* release            */
51 +
} retro_key_action_t;
52 +
43 53
/* The emulator implements these. Any may be NULL: the matching endpoint then
44 54
 * returns 501 Not Implemented. */
45 55
typedef struct retro_control_backend {
…
14 unchanged lines
60 70
61 71
    /* Monotonic completed-frame counter. */
62 72
    uint64_t (*get_frame_count)(void);
73 +
74 +
    /* 0.2: inject a key. is_text=1 => `value` is a character code point;
75 +
     * is_text=0 => `value` is a raw platform key code. `action` is a
76 +
     * retro_key_action_t. Return 1 on success, 0 if unmappable (server -> 400).
77 +
     * NULL => /key returns 501 and the server advertises contract 0.1.0. */
78 +
    int (*inject_key)(int is_text, uint32_t value, int action);
79 +
80 +
    /* 0.2: soft/cold reset the machine. NULL => /reset returns 501. */
81 +
    void (*reset)(void);
63 82
} retro_control_backend_t;
64 83
65 84
/* Start the server thread bound to 127.0.0.1:port. Non-blocking; 0 on success,
…
20 unchanged lines