OXYGENHUB
◆ main retro-remote-debug-controller / core / retro_control.c
28.8 KiB · C · last changed feat: contract 0.6 — GET/POST /perf, the performance metrics (budget, current frame, windows, zones, raw frames since one, a platform section) and their settings, and /status.capabilities; backend callbacks get_perf and set_perf return allocated JSON (#6) 25 days ago source blame history raw
1
/* retro_control.c — portable emulator control server. See retro_control.h
2
 * and ../SPEC.md. POSIX (Linux/macOS); on Windows the entry points are no-ops.
3
 *
4
 * One accept thread, one request in flight at a time. State reads are marshalled
5
 * to the emulator thread via retro_control_service() so they observe a
6
 * consistent machine; /step is driven cooperatively by retro_control_on_frame().
7
 */
8
#include "retro_control.h"
9
10
#include <stdio.h>
11
#include <stdlib.h>
12
#include <string.h>
13
14
#if defined(_WIN32) || defined(__EMSCRIPTEN__)
15
/* No TCP control server on Windows or in the browser (emscripten) — a localhost
16
 * socket server has no meaning there. Stub out so those builds still link. */
17
int  retro_control_start(int port, const retro_control_backend_t *b) { (void)port; (void)b; return -1; }
18
void retro_control_service(void) {}
19
int  retro_control_running(void) { return 1; }
20
void retro_control_on_frame(void) {}
21
void retro_control_stop(void) {}
22
#else
23
24
#include <pthread.h>
25
#include <stdatomic.h>
26
#include <unistd.h>
27
#include <errno.h>
28
#include <limits.h>
29
#include <signal.h>
30
#include <sys/socket.h>
31
#include <netinet/in.h>
32
#include <arpa/inet.h>
33
34
#ifndef MSG_NOSIGNAL
35
#define MSG_NOSIGNAL 0
36
#endif
37
38
#define MEM_CAP (1u << 20)   /* /mem hard cap: 1 MiB per request */
39
40
static const retro_control_backend_t *g_be = NULL;
41
static int       g_listen_fd = -1;
42
static pthread_t g_thread;
43
44
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
45
static pthread_cond_t  g_cv   = PTHREAD_COND_INITIALIZER;
46
47
/* run control: -1 free-run, 0 halted, >0 run this many frames then halt.
48
 * Atomic so retro_control_running() (called every instruction) reads it without
49
 * taking the lock — the mutex is kept only for the /step completion handshake. */
50
static _Atomic long g_frames_to_run = -1;
51
static int  g_step_waiting  = 0;
52
53
/* single-slot marshalled request. */
54
typedef enum { REQ_NONE = 0, REQ_MEM, REQ_REGS, REQ_SHOT, REQ_KEY, REQ_RESET,
55
               REQ_WRITE, REQ_AUDIO, REQ_PTR_SET, REQ_PTR_GET,
56
               REQ_PAD_SET, REQ_PAD_GET, REQ_PERF_GET, REQ_PERF_SET } req_t;
57
static volatile req_t g_req = REQ_NONE;
58
static uint32_t g_req_addr, g_req_len;
59
static int32_t  g_req_bank;
60
static const uint8_t *g_write_data;   /* POST /mem body (HTTP-thread owned) */
61
static int      g_key_is_text;   /* 1 => g_key_value is a char code point */
62
static uint32_t g_key_value;
63
static int      g_key_action;    /* retro_key_action_t */
64
static int      g_ptr_absolute;  /* 1 => (x,y) absolute; 0 => relative deltas */
65
static int32_t  g_ptr_x, g_ptr_y;
66
static int      g_ptr_buttons;   /* button bitmask, or -1 = leave unchanged */
67
static int      g_pad_index;     /* which pad (0-based) */
68
static int      g_pad_buttons;   /* held mask, or -1 = leave unchanged */
69
static int      g_pad_connected; /* 1 plug, 0 unplug, -1 = unchanged */
70
static char     g_resp_extra[64];     /* extra response header line, or "" */
71
static char     g_perf_query[1024];   /* GET /perf's query string (0.6) */
72
static const char *g_perf_body;       /* POST /perf's body, NUL-terminated (HTTP-thread owned) */
73
74
static uint8_t    *g_resp_body  = NULL;
75
static size_t      g_resp_len   = 0;
76
static int         g_resp_status = 200;
77
static const char *g_resp_ctype = "application/octet-stream";
78
static int         g_resp_ready = 0;
79
80
/* ---- emulator-thread side ------------------------------------------------ */
81
82
static void build_ppm(void)
83
{
84
    retro_framebuffer_t fb;
85
    memset(&fb, 0, sizeof fb);
86
    g_be->get_framebuffer(&fb);
87
    int w = fb.width, h = fb.height;
88
    int bpp = (fb.fmt == RETRO_PIX_RGB888) ? 3 : 4;
89
90
    char hdr[64];
91
    int hl = snprintf(hdr, sizeof hdr, "P6\n%d %d\n255\n", w, h);
92
    size_t total = (size_t)hl + (size_t)3 * w * h;
93
    uint8_t *out = (uint8_t *)malloc(total ? total : 1);
94
    if (!out) { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 500; return; }
95
96
    memcpy(out, hdr, hl);
97
    uint8_t *p = out + hl;
98
    const uint8_t *s = fb.pixels;
99
    for (int i = 0; i < w * h; i++) {
100
        const uint8_t *px = s + (size_t)i * bpp;
101
        if (fb.fmt == RETRO_PIX_BGRA8888) {
102
            p[0] = px[2]; p[1] = px[1]; p[2] = px[0];
103
        } else {                       /* RGB888 / RGBA8888 */
104
            p[0] = px[0]; p[1] = px[1]; p[2] = px[2];
105
        }
106
        p += 3;
107
    }
108
    g_resp_body = out;
109
    g_resp_len = total;
110
    g_resp_status = 200;
111
    g_resp_ctype = "image/x-portable-pixmap";
112
}
113
114
#define AUDIO_MAX_SAMPLES (1u << 18)   /* drain cap: 256K int16 (~2.7 s stereo @48.8k) */
115
116
static void le16(uint8_t *p, uint16_t v) { p[0] = v; p[1] = v >> 8; }
117
static void le32(uint8_t *p, uint32_t v) { p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24; }
118
119
/* Drain the backend's audio ring and wrap it as a canonical PCM WAV. */
120
static void build_wav(void)
121
{
122
    static int16_t samp[AUDIO_MAX_SAMPLES];
123
    int rate = 48000, channels = 1;
124
    uint32_t dropped = 0;
125
    uint32_t n = g_be->capture_audio(samp, AUDIO_MAX_SAMPLES, &rate, &channels, &dropped);
126
    if (channels < 1) channels = 1;
127
    if (rate < 1) rate = 1;
128
129
    uint32_t data_bytes = n * 2;               /* int16 samples */
130
    uint32_t total = 44 + data_bytes;
131
    uint8_t *out = (uint8_t *)malloc(total);
132
    if (!out) { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 500; return; }
133
134
    uint32_t byte_rate = (uint32_t)rate * channels * 2;
135
    memcpy(out, "RIFF", 4);       le32(out + 4, 36 + data_bytes);
136
    memcpy(out + 8, "WAVE", 4);
137
    memcpy(out + 12, "fmt ", 4);  le32(out + 16, 16);
138
    le16(out + 20, 1);            le16(out + 22, (uint16_t)channels);
139
    le32(out + 24, (uint32_t)rate);
140
    le32(out + 28, byte_rate);    le16(out + 32, (uint16_t)(channels * 2));
141
    le16(out + 34, 16);
142
    memcpy(out + 36, "data", 4);  le32(out + 40, data_bytes);
143
    memcpy(out + 44, samp, data_bytes);
144
145
    g_resp_body = out; g_resp_len = total; g_resp_status = 200;
146
    g_resp_ctype = "audio/wav";
147
    snprintf(g_resp_extra, sizeof g_resp_extra,
148
             "X-Rrdc-Audio-Dropped: %u\r\n", (unsigned)dropped);
149
}
150
151
void retro_control_service(void)
152
{
153
    if (!g_be || g_req == REQ_NONE) return;   /* cheap unlocked fast-path */
154
155
    pthread_mutex_lock(&g_lock);
156
    g_resp_extra[0] = 0;
157
    switch (g_req) {
158
    case REQ_MEM: {
159
        uint32_t len = g_req_len > MEM_CAP ? MEM_CAP : g_req_len;
160
        uint8_t *buf = (uint8_t *)malloc(len ? len : 1);
161
        uint32_t got = 0;
162
        if (buf && g_be->read_mem) got = g_be->read_mem(g_req_addr, g_req_bank, len, buf, len);
163
        g_resp_body = buf; g_resp_len = got;
164
        g_resp_status = g_be->read_mem ? 200 : 501;
165
        g_resp_ctype = "application/octet-stream";
166
        break;
167
    }
168
    case REQ_REGS: {
169
        char *buf = (char *)malloc(1024);
170
        if (buf && g_be->get_regs_json) { g_be->get_regs_json(buf, 1024); }
171
        else if (buf) { buf[0] = 0; }
172
        g_resp_body = (uint8_t *)buf; g_resp_len = buf ? strlen(buf) : 0;
173
        g_resp_status = g_be->get_regs_json ? 200 : 501;
174
        g_resp_ctype = "application/json";
175
        break;
176
    }
177
    case REQ_SHOT:
178
        if (g_be->get_framebuffer) build_ppm();
179
        else { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 501; }
180
        break;
181
    case REQ_KEY: {
182
        const char *json;
183
        if (!g_be->inject_key) {
184
            json = "{\"error\":\"not implemented\"}"; g_resp_status = 501;
185
        } else if (g_be->inject_key(g_key_is_text, g_key_value, g_key_action)) {
186
            json = "{\"injected\":true}"; g_resp_status = 200;
187
        } else {
188
            json = "{\"error\":\"unmapped key\"}"; g_resp_status = 400;
189
        }
190
        size_t l = strlen(json);
191
        uint8_t *b = (uint8_t *)malloc(l + 1);
192
        if (b) memcpy(b, json, l + 1);
193
        g_resp_body = b; g_resp_len = b ? l : 0;
194
        g_resp_ctype = "application/json";
195
        break;
196
    }
197
    case REQ_RESET: {
198
        const char *json;
199
        if (g_be->reset) { g_be->reset(); json = "{\"reset\":true}"; g_resp_status = 200; }
200
        else { json = "{\"error\":\"not implemented\"}"; g_resp_status = 501; }
201
        size_t l = strlen(json);
202
        uint8_t *b = (uint8_t *)malloc(l + 1);
203
        if (b) memcpy(b, json, l + 1);
204
        g_resp_body = b; g_resp_len = b ? l : 0;
205
        g_resp_ctype = "application/json";
206
        break;
207
    }
208
    case REQ_WRITE: {
209
        const char *json; char *b;
210
        if (!g_be->write_mem) {
211
            json = "{\"error\":\"not implemented\"}"; g_resp_status = 501;
212
            size_t l = strlen(json); b = (char *)malloc(l + 1);
213
            if (b) memcpy(b, json, l + 1);
214
            g_resp_len = b ? l : 0;
215
        } else {
216
            uint32_t wrote = g_be->write_mem(g_req_addr, g_req_bank, g_req_len, g_write_data);
217
            b = (char *)malloc(48);
218
            g_resp_len = b ? (size_t)snprintf(b, 48, "{\"written\":%u}", (unsigned)wrote) : 0;
219
            g_resp_status = 200;
220
        }
221
        g_resp_body = (uint8_t *)b;
222
        g_resp_ctype = "application/json";
223
        break;
224
    }
225
    case REQ_AUDIO:
226
        if (g_be->capture_audio) build_wav();
227
        else {
228
            const char *json = "{\"error\":\"not implemented\"}";
229
            size_t l = strlen(json); uint8_t *b = (uint8_t *)malloc(l + 1);
230
            if (b) memcpy(b, json, l + 1);
231
            g_resp_body = b; g_resp_len = b ? l : 0; g_resp_status = 501;
232
            g_resp_ctype = "application/json";
233
        }
234
        break;
235
    case REQ_PTR_SET: {
236
        const char *json;
237
        if (!g_be->set_pointer) {
238
            json = "{\"error\":\"not implemented\"}"; g_resp_status = 501;
239
        } else if (g_be->set_pointer(g_ptr_absolute, g_ptr_x, g_ptr_y, g_ptr_buttons)) {
240
            json = "{\"injected\":true}"; g_resp_status = 200;
241
        } else {
242
            json = "{\"error\":\"no pointer\"}"; g_resp_status = 400;
243
        }
244
        size_t l = strlen(json);
245
        uint8_t *b = (uint8_t *)malloc(l + 1);
246
        if (b) memcpy(b, json, l + 1);
247
        g_resp_body = b; g_resp_len = b ? l : 0;
248
        g_resp_ctype = "application/json";
249
        break;
250
    }
251
    case REQ_PTR_GET: {
252
        char *b;
253
        if (!g_be->get_pointer) {
254
            const char *json = "{\"error\":\"not implemented\"}";
255
            size_t l = strlen(json); b = (char *)malloc(l + 1);
256
            if (b) memcpy(b, json, l + 1);
257
            g_resp_len = b ? l : 0; g_resp_status = 501;
258
        } else {
259
            int32_t x = 0, y = 0; int buttons = 0;
260
            int ok = g_be->get_pointer(&x, &y, &buttons);
261
            b = (char *)malloc(96);
262
            if (!b) { g_resp_len = 0; g_resp_status = 500; }
263
            else if (ok) {
264
                g_resp_len = (size_t)snprintf(b, 96,
265
                    "{\"x\":%ld,\"y\":%ld,\"buttons\":%d}",
266
                    (long)x, (long)y, buttons);
267
                g_resp_status = 200;
268
            } else {
269
                g_resp_len = (size_t)snprintf(b, 96, "{\"error\":\"no pointer\"}");
270
                g_resp_status = 400;
271
            }
272
        }
273
        g_resp_body = (uint8_t *)b;
274
        g_resp_ctype = "application/json";
275
        break;
276
    }
277
    case REQ_PAD_SET: {
278
        const char *json;
279
        if (!g_be->set_pad) {
280
            json = "{\"error\":\"not implemented\"}"; g_resp_status = 501;
281
        } else if (g_be->set_pad(g_pad_index, g_pad_buttons, g_pad_connected)) {
282
            json = "{\"injected\":true}"; g_resp_status = 200;
283
        } else {
284
            json = "{\"error\":\"no controller\"}"; g_resp_status = 400;
285
        }
286
        size_t l = strlen(json);
287
        uint8_t *b = (uint8_t *)malloc(l + 1);
288
        if (b) memcpy(b, json, l + 1);
289
        g_resp_body = b; g_resp_len = b ? l : 0;
290
        g_resp_ctype = "application/json";
291
        break;
292
    }
293
    case REQ_PAD_GET: {
294
        char *b;
295
        if (!g_be->get_pad) {
296
            const char *json = "{\"error\":\"not implemented\"}";
297
            size_t l = strlen(json); b = (char *)malloc(l + 1);
298
            if (b) memcpy(b, json, l + 1);
299
            g_resp_len = b ? l : 0; g_resp_status = 501;
300
        } else {
301
            int buttons = 0, connected = 0;
302
            int ok = g_be->get_pad(g_pad_index, &buttons, &connected);
303
            b = (char *)malloc(96);
304
            if (!b) { g_resp_len = 0; g_resp_status = 500; }
305
            else if (ok) {
306
                g_resp_len = (size_t)snprintf(b, 96,
307
                    "{\"index\":%d,\"connected\":%s,\"buttons\":%d}",
308
                    g_pad_index, connected ? "true" : "false", buttons);
309
                g_resp_status = 200;
310
            } else {
311
                g_resp_len = (size_t)snprintf(b, 96,
312
                    "{\"error\":\"no controller\"}");
313
                g_resp_status = 400;
314
            }
315
        }
316
        g_resp_body = (uint8_t *)b;
317
        g_resp_ctype = "application/json";
318
        break;
319
    }
320
    case REQ_PERF_GET:
321
    case REQ_PERF_SET: {
322
        char *b = NULL;
323
        int st = 200;
324
        int have = (g_req == REQ_PERF_GET) ? g_be->get_perf != NULL : g_be->set_perf != NULL;
325
        if (!have) {
326
            st = 501;
327
        } else if (g_req == REQ_PERF_GET) {
328
            b = g_be->get_perf(g_perf_query);
329
        } else {
330
            b = g_be->set_perf(g_perf_body ? g_perf_body : "", &st);
331
            if (st != 200 && st != 400) st = 400;
332
        }
333
        if (!b) {                             /* not implemented, or the backend failed */
334
            const char *json = (st == 501) ? "{\"error\":\"not implemented\"}"
335
                                           : "{\"error\":\"perf failed\"}";
336
            if (st != 501) st = 500;
337
            size_t l = strlen(json);
338
            b = (char *)malloc(l + 1);
339
            if (b) memcpy(b, json, l + 1);
340
        }
341
        g_resp_body = (uint8_t *)b; g_resp_len = b ? strlen(b) : 0;
342
        g_resp_status = st;
343
        g_resp_ctype = "application/json";
344
        break;
345
    }
346
    default: break;
347
    }
348
    g_req = REQ_NONE;
349
    g_resp_ready = 1;
350
    pthread_cond_broadcast(&g_cv);
351
    pthread_mutex_unlock(&g_lock);
352
}
353
354
int retro_control_running(void)
355
{
356
    if (!g_be) return 1;
357
    /* Lock-free hot path: a word-sized atomic read, no mutex. The gate
358
     * tolerates a one-iteration-stale value; /step correctness is enforced by
359
     * the mutex + condvar in on_frame/do_step, not by this read. */
360
    return atomic_load_explicit(&g_frames_to_run, memory_order_relaxed) != 0;
361
}
362
363
void retro_control_on_frame(void)
364
{
365
    if (!g_be) return;
366
    pthread_mutex_lock(&g_lock);
367
    if (g_frames_to_run > 0) {
368
        g_frames_to_run--;
369
        if (g_frames_to_run == 0 && g_step_waiting) {
370
            g_step_waiting = 0;
371
            g_resp_ready = 1;
372
            pthread_cond_broadcast(&g_cv);
373
        }
374
    }
375
    pthread_mutex_unlock(&g_lock);
376
}
377
378
/* ---- server-thread side -------------------------------------------------- */
379
380
static void write_all(int fd, const void *buf, size_t len)
381
{
382
    const char *p = (const char *)buf;
383
    while (len) {
384
        ssize_t w = send(fd, p, len, MSG_NOSIGNAL);
385
        if (w <= 0) return;
386
        p += w; len -= (size_t)w;
387
    }
388
}
389
390
/* `extra`, if non-NULL, is a complete header line ("Name: value\r\n"). */
391
static void send_http(int fd, int status, const char *ctype,
392
                      const uint8_t *body, size_t len, const char *extra)
393
{
394
    const char *reason =
395
        status == 200 ? "OK" :
396
        status == 400 ? "Bad Request" :
397
        status == 404 ? "Not Found" :
398
        status == 405 ? "Method Not Allowed" :
399
        status == 500 ? "Internal Server Error" :
400
        status == 501 ? "Not Implemented" : "Error";
401
    char hdr[320];
402
    int hl = snprintf(hdr, sizeof hdr,
403
        "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n"
404
        "Connection: close\r\nAccess-Control-Allow-Origin: *\r\n%s\r\n",
405
        status, reason, ctype, len, extra ? extra : "");
406
    write_all(fd, hdr, hl);
407
    if (body && len) write_all(fd, body, len);
408
}
409
410
static void send_json(int fd, int status, const char *json)
411
{
412
    send_http(fd, status, "application/json", (const uint8_t *)json, strlen(json), NULL);
413
}
414
415
/* value of query key, decimal or 0x-hex (strtol base 0); def if absent. */
416
static long query_long(const char *q, const char *key, long def)
417
{
418
    size_t kl = strlen(key);
419
    const char *p = q;
420
    while (p && *p) {
421
        if (!strncmp(p, key, kl) && p[kl] == '=')
422
            return strtol(p + kl + 1, NULL, 0);
423
        p = strchr(p, '&');
424
        if (p) p++;
425
    }
426
    return def;
427
}
428
429
/* one hex nibble, or -1. */
430
static int hex_nibble(int c)
431
{
432
    if (c >= '0' && c <= '9') return c - '0';
433
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
434
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
435
    return -1;
436
}
437
438
/* copy the value of query key into out (NUL-terminated, truncated to cap),
439
 * percent-decoding it (form-urlencoded: %XX -> byte, '+' -> space). Numeric
440
 * params are read with query_long straight off the raw query, so only string
441
 * values (e.g. text=) are decoded here. Returns 1 if the key was present. */
442
static int query_str(const char *q, const char *key, char *out, size_t cap)
443
{
444
    size_t kl = strlen(key);
445
    const char *p = q;
446
    while (p && *p) {
447
        if (!strncmp(p, key, kl) && p[kl] == '=') {
448
            const char *v = p + kl + 1;
449
            const char *e = strchr(v, '&');
450
            const char *end = e ? e : v + strlen(v);
451
            size_t w = 0;
452
            for (const char *r = v; r < end && w + 1 < cap; ) {
453
                int hi, lo;
454
                if (r[0] == '%' && r + 2 < end &&
455
                    (hi = hex_nibble(r[1])) >= 0 && (lo = hex_nibble(r[2])) >= 0) {
456
                    out[w++] = (char)((hi << 4) | lo);
457
                    r += 3;
458
                } else if (r[0] == '+') {
459
                    out[w++] = ' ';
460
                    r += 1;
461
                } else {
462
                    out[w++] = *r++;
463
                }
464
            }
465
            out[w] = 0;
466
            return 1;
467
        }
468
        p = strchr(p, '&');
469
        if (p) p++;
470
    }
471
    if (cap) out[0] = 0;
472
    return 0;
473
}
474
475
/* marshal a read to the emulator thread, wait, send the response. */
476
static void do_marshalled(int fd, req_t r)
477
{
478
    pthread_mutex_lock(&g_lock);
479
    g_resp_ready = 0; g_resp_body = NULL; g_resp_len = 0;
480
    g_req = r;
481
    while (!g_resp_ready) pthread_cond_wait(&g_cv, &g_lock);
482
    uint8_t *body = g_resp_body; size_t len = g_resp_len;
483
    int st = g_resp_status; const char *ct = g_resp_ctype;
484
    char extra[64]; memcpy(extra, g_resp_extra, sizeof extra);
485
    g_resp_body = NULL;
486
    pthread_mutex_unlock(&g_lock);
487
488
    send_http(fd, st, ct, body, len, extra[0] ? extra : NULL);
489
    free(body);
490
}
491
492
static void do_status(int fd)
493
{
494
    pthread_mutex_lock(&g_lock);
495
    long f = g_frames_to_run;
496
    pthread_mutex_unlock(&g_lock);
497
    unsigned long long fc = g_be->get_frame_count ? g_be->get_frame_count() : 0;
498
    /* Advertise the highest level whose defining callbacks are all present. */
499
    const int perf = g_be->get_perf && g_be->set_perf;
500
    const char *contract =
501
        (g_be->inject_key && g_be->write_mem && g_be->capture_audio &&
502
         g_be->set_pointer && g_be->get_pointer &&
503
         g_be->set_pad && g_be->get_pad && perf) ? "0.6.0" :
504
        (g_be->inject_key && g_be->write_mem && g_be->capture_audio &&
505
         g_be->set_pointer && g_be->get_pointer &&
506
         g_be->set_pad && g_be->get_pad) ? "0.5.0" :
507
        (g_be->inject_key && g_be->write_mem && g_be->capture_audio &&
508
         g_be->set_pointer && g_be->get_pointer) ? "0.4.0" :
509
        (g_be->inject_key && g_be->write_mem && g_be->capture_audio) ? "0.3.0" :
510
        g_be->inject_key ? "0.2.0" : "0.1.0";
511
    /* 0.6: each endpoint group the backend serves, whatever level it reaches. */
512
    char caps[160];
513
    snprintf(caps, sizeof caps, "\"mem\",\"regs\",\"screenshot\",\"step\"%s%s%s%s%s%s%s",
514
        g_be->inject_key ? ",\"key\"" : "",
515
        g_be->reset ? ",\"reset\"" : "",
516
        g_be->write_mem ? ",\"mem-write\"" : "",
517
        g_be->capture_audio ? ",\"audio\"" : "",
518
        (g_be->set_pointer && g_be->get_pointer) ? ",\"pointer\"" : "",
519
        (g_be->set_pad && g_be->get_pad) ? ",\"pad\"" : "",
520
        perf ? ",\"perf\"" : "");
521
    char buf[512];
522
    snprintf(buf, sizeof buf,
523
        "{\"contract\":\"%s\",\"emulator\":\"%s\",\"platform\":\"%s\","
524
        "\"frame\":%llu,\"paused\":%s,\"running\":true,\"capabilities\":[%s]}",
525
        contract,
526
        g_be->emulator ? g_be->emulator : "",
527
        g_be->platform ? g_be->platform : "",
528
        fc, (f == 0) ? "true" : "false", caps);
529
    send_json(fd, 200, buf);
530
}
531
532
static void do_step(int fd, const char *query)
533
{
534
    long n = query_long(query, "frames", 1);
535
    if (n < 1) n = 1;
536
    pthread_mutex_lock(&g_lock);
537
    g_frames_to_run = n;
538
    g_step_waiting = 1;
539
    g_resp_ready = 0;
540
    while (!g_resp_ready) pthread_cond_wait(&g_cv, &g_lock);
541
    pthread_mutex_unlock(&g_lock);
542
543
    unsigned long long fc = g_be->get_frame_count ? g_be->get_frame_count() : 0;
544
    char buf[64];
545
    snprintf(buf, sizeof buf, "{\"frame\":%llu}", fc);
546
    send_json(fd, 200, buf);
547
}
548
549
static void do_setrun(int fd, long v, int paused)
550
{
551
    pthread_mutex_lock(&g_lock);
552
    g_frames_to_run = v;
553
    pthread_mutex_unlock(&g_lock);
554
    send_json(fd, 200, paused ? "{\"paused\":true}" : "{\"paused\":false}");
555
}
556
557
/* Read a request body (POST /mem). `req`/`n` = what handle_conn already recv'd
558
 * (headers + maybe some body); reads the rest per Content-Length. Returns a
559
 * malloc'd buffer of *out_len bytes (caller frees), capped at MEM_CAP. */
560
static uint8_t *read_body(int fd, char *req, size_t n, size_t *out_len)
561
{
562
    *out_len = 0;
563
    char *hdr_end = strstr(req, "\r\n\r\n");
564
    if (!hdr_end) return NULL;
565
    char *body_start = hdr_end + 4;
566
567
    long clen = 0;
568
    char *cl = strstr(req, "Content-Length:");
569
    if (!cl) cl = strstr(req, "content-length:");
570
    if (cl) clen = strtol(cl + 15, NULL, 10);
571
    if (clen < 0) clen = 0;
572
    if (clen > (long)MEM_CAP) clen = (long)MEM_CAP;
573
574
    uint8_t *body = (uint8_t *)malloc((size_t)clen ? (size_t)clen : 1);
575
    if (!body) return NULL;
576
577
    size_t have = (size_t)(req + n - body_start);
578
    if (have > (size_t)clen) have = (size_t)clen;
579
    memcpy(body, body_start, have);
580
    size_t got = have;
581
    while (got < (size_t)clen) {
582
        ssize_t r = recv(fd, body + got, (size_t)clen - got, 0);
583
        if (r <= 0) break;
584
        got += (size_t)r;
585
    }
586
    *out_len = got;
587
    return body;
588
}
589
590
static void handle_conn(int fd)
591
{
592
    char req[2048];
593
    size_t n = 0;
594
    while (n < sizeof(req) - 1) {
595
        ssize_t r = recv(fd, req + n, sizeof(req) - 1 - n, 0);
596
        if (r <= 0) break;
597
        n += (size_t)r;
598
        if (strstr(req, "\r\n\r\n")) break;
599
    }
600
    req[n] = 0;
601
602
    char method[8] = {0}, target[1024] = {0};
603
    if (sscanf(req, "%7s %1023s", method, target) != 2) {
604
        send_json(fd, 400, "{\"error\":\"bad request\"}");
605
        return;
606
    }
607
    char *q = strchr(target, '?');
608
    const char *query = "";
609
    if (q) { *q = 0; query = q + 1; }
610
    int is_post = !strcmp(method, "POST");
611
612
    if (!strcmp(target, "/status"))          { do_status(fd); return; }
613
    if (!strcmp(target, "/regs"))            { do_marshalled(fd, REQ_REGS); return; }
614
    if (!strcmp(target, "/screenshot"))      { do_marshalled(fd, REQ_SHOT); return; }
615
    if (!strcmp(target, "/audio"))           { do_marshalled(fd, REQ_AUDIO); return; }
616
    if (!strcmp(target, "/mem")) {
617
        g_req_addr = (uint32_t)query_long(query, "addr", 0);
618
        g_req_bank = (int32_t)query_long(query, "bank", -1);
619
        if (is_post) {                        /* 0.3: write / poke */
620
            size_t blen = 0;
621
            uint8_t *body = read_body(fd, req, n, &blen);
622
            g_req_len = (uint32_t)blen;
623
            g_write_data = body;
624
            do_marshalled(fd, REQ_WRITE);
625
            free(body);
626
        } else {                              /* read */
627
            g_req_len = (uint32_t)query_long(query, "len", 0);
628
            do_marshalled(fd, REQ_MEM);
629
        }
630
        return;
631
    }
632
    if (!strcmp(target, "/step")) {
633
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
634
        do_step(fd, query); return;
635
    }
636
    if (!strcmp(target, "/pause")) {
637
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
638
        do_setrun(fd, 0, 1); return;
639
    }
640
    if (!strcmp(target, "/resume")) {
641
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
642
        do_setrun(fd, -1, 0); return;
643
    }
644
    if (!strcmp(target, "/key")) {
645
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
646
        char text[8];
647
        long code = query_long(query, "code", -1);
648
        long down = query_long(query, "down", -1);
649
        if (query_str(query, "text", text, sizeof text) && text[0]) {
650
            g_key_is_text = 1; g_key_value = (uint32_t)(unsigned char)text[0];
651
        } else if (code >= 0) {
652
            g_key_is_text = 0; g_key_value = (uint32_t)code;
653
        } else {
654
            send_json(fd, 400, "{\"error\":\"key needs text= or code=\"}"); return;
655
        }
656
        g_key_action = (down < 0) ? RETRO_KEY_TAP
657
                     : (down != 0) ? RETRO_KEY_DOWN : RETRO_KEY_UP;
658
        do_marshalled(fd, REQ_KEY);
659
        return;
660
    }
661
    if (!strcmp(target, "/reset")) {
662
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
663
        do_marshalled(fd, REQ_RESET);
664
        return;
665
    }
666
    if (!strcmp(target, "/pointer")) {
667
        if (is_post) {                        /* 0.4: move/click the pointer */
668
            long x  = query_long(query, "x",  LONG_MIN);
669
            long y  = query_long(query, "y",  LONG_MIN);
670
            long dx = query_long(query, "dx", LONG_MIN);
671
            long dy = query_long(query, "dy", LONG_MIN);
672
            if (x != LONG_MIN && y != LONG_MIN) {
673
                g_ptr_absolute = 1; g_ptr_x = (int32_t)x; g_ptr_y = (int32_t)y;
674
            } else if (dx != LONG_MIN || dy != LONG_MIN) {
675
                g_ptr_absolute = 0;
676
                g_ptr_x = (int32_t)(dx == LONG_MIN ? 0 : dx);
677
                g_ptr_y = (int32_t)(dy == LONG_MIN ? 0 : dy);
678
            } else {
679
                send_json(fd, 400, "{\"error\":\"pointer needs x&y or dx&dy\"}"); return;
680
            }
681
            g_ptr_buttons = (int)query_long(query, "buttons", -1);   /* -1 = unchanged */
682
            do_marshalled(fd, REQ_PTR_SET);
683
        } else {                              /* 0.4: read back the pointer */
684
            do_marshalled(fd, REQ_PTR_GET);
685
        }
686
        return;
687
    }
688
    if (!strcmp(target, "/pad")) {
689
        g_pad_index = (int)query_long(query, "index", 0);
690
        if (is_post) {                        /* 0.5: present / set a pad */
691
            g_pad_buttons = (int)query_long(query, "buttons", -1);
692
            /* `connected` defaults to plugging it in whenever buttons were
693
             * given -- injecting a press on an absent pad is never what the
694
             * caller meant -- and to leaving it alone otherwise. */
695
            g_pad_connected = (int)query_long(query, "connected",
696
                                              g_pad_buttons >= 0 ? 1 : -1);
697
            do_marshalled(fd, REQ_PAD_SET);
698
        } else {                              /* 0.5: read a pad back */
699
            do_marshalled(fd, REQ_PAD_GET);
700
        }
701
        return;
702
    }
703
    if (!strcmp(target, "/perf")) {           /* 0.6: performance metrics */
704
        if (is_post) {
705
            size_t blen = 0;
706
            uint8_t *body = read_body(fd, req, n, &blen);
707
            char *text = (char *)malloc(blen + 1);
708
            if (!text) { free(body); send_json(fd, 500, "{\"error\":\"out of memory\"}"); return; }
709
            if (body && blen) memcpy(text, body, blen);
710
            text[blen] = 0;
711
            free(body);
712
            g_perf_body = text;
713
            do_marshalled(fd, REQ_PERF_SET);
714
            g_perf_body = NULL;
715
            free(text);
716
        } else {
717
            snprintf(g_perf_query, sizeof g_perf_query, "%s", query);
718
            do_marshalled(fd, REQ_PERF_GET);
719
        }
720
        return;
721
    }
722
    send_json(fd, 404, "{\"error\":\"not found\"}");
723
}
724
725
static void *server_main(void *arg)
726
{
727
    (void)arg;
728
    for (;;) {
729
        int fd = accept(g_listen_fd, NULL, NULL);
730
        if (fd < 0) {
731
            if (errno == EINTR) continue;
732
            break;
733
        }
734
        handle_conn(fd);
735
        close(fd);
736
    }
737
    return NULL;
738
}
739
740
int retro_control_start(int port, const retro_control_backend_t *backend)
741
{
742
    g_be = backend;
743
    signal(SIGPIPE, SIG_IGN);
744
745
    int fd = socket(AF_INET, SOCK_STREAM, 0);
746
    if (fd < 0) return -1;
747
    int one = 1;
748
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
749
750
    struct sockaddr_in addr;
751
    memset(&addr, 0, sizeof addr);
752
    addr.sin_family = AF_INET;
753
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);   /* 127.0.0.1 only */
754
    addr.sin_port = htons((uint16_t)port);
755
    if (bind(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
756
        fprintf(stderr, "[retro_control] bind 127.0.0.1:%d failed: %s\n",
757
                port, strerror(errno));
758
        close(fd);
759
        return -1;
760
    }
761
    if (listen(fd, 4) < 0) { close(fd); return -1; }
762
763
    g_listen_fd = fd;
764
    if (pthread_create(&g_thread, NULL, server_main, NULL) != 0) {
765
        close(fd); g_listen_fd = -1; return -1;
766
    }
767
    pthread_detach(g_thread);
768
    fprintf(stderr, "[retro_control] listening on 127.0.0.1:%d (contract %s)\n",
769
            port, RETRO_CONTROL_CONTRACT);
770
    return 0;
771
}
772
773
void retro_control_stop(void)
774
{
775
    if (g_listen_fd >= 0) { close(g_listen_fd); g_listen_fd = -1; }
776
}
777
778
#endif /* !_WIN32 */