OXYGENHUB
# 4c0b050e9a25 retro-remote-debug-controller / core / retro_control.c
11.1 KiB · C · last changed HTTP control contract, portable server core, and shared client 3 months ago source blame history raw
961b3c61 Andy McCall 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)
15
/* No server on Windows for now — stub out so the fork still builds. */
16
int  retro_control_start(int port, const retro_control_backend_t *b) { (void)port; (void)b; return -1; }
17
void retro_control_service(void) {}
18
int  retro_control_running(void) { return 1; }
19
void retro_control_on_frame(void) {}
20
void retro_control_stop(void) {}
21
#else
22
23
#include <pthread.h>
24
#include <unistd.h>
25
#include <errno.h>
26
#include <signal.h>
27
#include <sys/socket.h>
28
#include <netinet/in.h>
29
#include <arpa/inet.h>
30
31
#ifndef MSG_NOSIGNAL
32
#define MSG_NOSIGNAL 0
33
#endif
34
35
#define MEM_CAP (1u << 20)   /* /mem hard cap: 1 MiB per request */
36
37
static const retro_control_backend_t *g_be = NULL;
38
static int       g_listen_fd = -1;
39
static pthread_t g_thread;
40
41
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
42
static pthread_cond_t  g_cv   = PTHREAD_COND_INITIALIZER;
43
44
/* run control: -1 free-run, 0 halted, >0 run this many frames then halt */
45
static long g_frames_to_run = -1;
46
static int  g_step_waiting  = 0;
47
48
/* single-slot marshalled request (mem / regs / screenshot) */
49
typedef enum { REQ_NONE = 0, REQ_MEM, REQ_REGS, REQ_SHOT } req_t;
50
static volatile req_t g_req = REQ_NONE;
51
static uint32_t g_req_addr, g_req_len;
52
static int32_t  g_req_bank;
53
54
static uint8_t    *g_resp_body  = NULL;
55
static size_t      g_resp_len   = 0;
56
static int         g_resp_status = 200;
57
static const char *g_resp_ctype = "application/octet-stream";
58
static int         g_resp_ready = 0;
59
60
/* ---- emulator-thread side ------------------------------------------------ */
61
62
static void build_ppm(void)
63
{
64
    retro_framebuffer_t fb;
65
    memset(&fb, 0, sizeof fb);
66
    g_be->get_framebuffer(&fb);
67
    int w = fb.width, h = fb.height;
68
    int bpp = (fb.fmt == RETRO_PIX_RGB888) ? 3 : 4;
69
70
    char hdr[64];
71
    int hl = snprintf(hdr, sizeof hdr, "P6\n%d %d\n255\n", w, h);
72
    size_t total = (size_t)hl + (size_t)3 * w * h;
73
    uint8_t *out = (uint8_t *)malloc(total ? total : 1);
74
    if (!out) { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 500; return; }
75
76
    memcpy(out, hdr, hl);
77
    uint8_t *p = out + hl;
78
    const uint8_t *s = fb.pixels;
79
    for (int i = 0; i < w * h; i++) {
80
        const uint8_t *px = s + (size_t)i * bpp;
81
        if (fb.fmt == RETRO_PIX_BGRA8888) {
82
            p[0] = px[2]; p[1] = px[1]; p[2] = px[0];
83
        } else {                       /* RGB888 / RGBA8888 */
84
            p[0] = px[0]; p[1] = px[1]; p[2] = px[2];
85
        }
86
        p += 3;
87
    }
88
    g_resp_body = out;
89
    g_resp_len = total;
90
    g_resp_status = 200;
91
    g_resp_ctype = "image/x-portable-pixmap";
92
}
93
94
void retro_control_service(void)
95
{
96
    if (!g_be || g_req == REQ_NONE) return;   /* cheap unlocked fast-path */
97
98
    pthread_mutex_lock(&g_lock);
99
    switch (g_req) {
100
    case REQ_MEM: {
101
        uint32_t len = g_req_len > MEM_CAP ? MEM_CAP : g_req_len;
102
        uint8_t *buf = (uint8_t *)malloc(len ? len : 1);
103
        uint32_t got = 0;
104
        if (buf && g_be->read_mem) got = g_be->read_mem(g_req_addr, g_req_bank, len, buf, len);
105
        g_resp_body = buf; g_resp_len = got;
106
        g_resp_status = g_be->read_mem ? 200 : 501;
107
        g_resp_ctype = "application/octet-stream";
108
        break;
109
    }
110
    case REQ_REGS: {
111
        char *buf = (char *)malloc(1024);
112
        if (buf && g_be->get_regs_json) { g_be->get_regs_json(buf, 1024); }
113
        else if (buf) { buf[0] = 0; }
114
        g_resp_body = (uint8_t *)buf; g_resp_len = buf ? strlen(buf) : 0;
115
        g_resp_status = g_be->get_regs_json ? 200 : 501;
116
        g_resp_ctype = "application/json";
117
        break;
118
    }
119
    case REQ_SHOT:
120
        if (g_be->get_framebuffer) build_ppm();
121
        else { g_resp_body = NULL; g_resp_len = 0; g_resp_status = 501; }
122
        break;
123
    default: break;
124
    }
125
    g_req = REQ_NONE;
126
    g_resp_ready = 1;
127
    pthread_cond_broadcast(&g_cv);
128
    pthread_mutex_unlock(&g_lock);
129
}
130
131
int retro_control_running(void)
132
{
133
    if (!g_be) return 1;
134
    pthread_mutex_lock(&g_lock);
135
    long f = g_frames_to_run;
136
    pthread_mutex_unlock(&g_lock);
137
    return f != 0;
138
}
139
140
void retro_control_on_frame(void)
141
{
142
    if (!g_be) return;
143
    pthread_mutex_lock(&g_lock);
144
    if (g_frames_to_run > 0) {
145
        g_frames_to_run--;
146
        if (g_frames_to_run == 0 && g_step_waiting) {
147
            g_step_waiting = 0;
148
            g_resp_ready = 1;
149
            pthread_cond_broadcast(&g_cv);
150
        }
151
    }
152
    pthread_mutex_unlock(&g_lock);
153
}
154
155
/* ---- server-thread side -------------------------------------------------- */
156
157
static void write_all(int fd, const void *buf, size_t len)
158
{
159
    const char *p = (const char *)buf;
160
    while (len) {
161
        ssize_t w = send(fd, p, len, MSG_NOSIGNAL);
162
        if (w <= 0) return;
163
        p += w; len -= (size_t)w;
164
    }
165
}
166
167
static void send_http(int fd, int status, const char *ctype,
168
                      const uint8_t *body, size_t len)
169
{
170
    const char *reason =
171
        status == 200 ? "OK" :
172
        status == 400 ? "Bad Request" :
173
        status == 404 ? "Not Found" :
174
        status == 405 ? "Method Not Allowed" :
175
        status == 501 ? "Not Implemented" : "Error";
176
    char hdr[256];
177
    int hl = snprintf(hdr, sizeof hdr,
178
        "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n"
179
        "Connection: close\r\nAccess-Control-Allow-Origin: *\r\n\r\n",
180
        status, reason, ctype, len);
181
    write_all(fd, hdr, hl);
182
    if (body && len) write_all(fd, body, len);
183
}
184
185
static void send_json(int fd, int status, const char *json)
186
{
187
    send_http(fd, status, "application/json", (const uint8_t *)json, strlen(json));
188
}
189
190
/* value of query key, decimal or 0x-hex (strtol base 0); def if absent. */
191
static long query_long(const char *q, const char *key, long def)
192
{
193
    size_t kl = strlen(key);
194
    const char *p = q;
195
    while (p && *p) {
196
        if (!strncmp(p, key, kl) && p[kl] == '=')
197
            return strtol(p + kl + 1, NULL, 0);
198
        p = strchr(p, '&');
199
        if (p) p++;
200
    }
201
    return def;
202
}
203
204
/* marshal a read to the emulator thread, wait, send the response. */
205
static void do_marshalled(int fd, req_t r)
206
{
207
    pthread_mutex_lock(&g_lock);
208
    g_resp_ready = 0; g_resp_body = NULL; g_resp_len = 0;
209
    g_req = r;
210
    while (!g_resp_ready) pthread_cond_wait(&g_cv, &g_lock);
211
    uint8_t *body = g_resp_body; size_t len = g_resp_len;
212
    int st = g_resp_status; const char *ct = g_resp_ctype;
213
    g_resp_body = NULL;
214
    pthread_mutex_unlock(&g_lock);
215
216
    send_http(fd, st, ct, body, len);
217
    free(body);
218
}
219
220
static void do_status(int fd)
221
{
222
    pthread_mutex_lock(&g_lock);
223
    long f = g_frames_to_run;
224
    pthread_mutex_unlock(&g_lock);
225
    unsigned long long fc = g_be->get_frame_count ? g_be->get_frame_count() : 0;
226
    char buf[320];
227
    snprintf(buf, sizeof buf,
228
        "{\"contract\":\"%s\",\"emulator\":\"%s\",\"platform\":\"%s\","
229
        "\"frame\":%llu,\"paused\":%s,\"running\":true}",
230
        RETRO_CONTROL_CONTRACT,
231
        g_be->emulator ? g_be->emulator : "",
232
        g_be->platform ? g_be->platform : "",
233
        fc, (f == 0) ? "true" : "false");
234
    send_json(fd, 200, buf);
235
}
236
237
static void do_step(int fd, const char *query)
238
{
239
    long n = query_long(query, "frames", 1);
240
    if (n < 1) n = 1;
241
    pthread_mutex_lock(&g_lock);
242
    g_frames_to_run = n;
243
    g_step_waiting = 1;
244
    g_resp_ready = 0;
245
    while (!g_resp_ready) pthread_cond_wait(&g_cv, &g_lock);
246
    pthread_mutex_unlock(&g_lock);
247
248
    unsigned long long fc = g_be->get_frame_count ? g_be->get_frame_count() : 0;
249
    char buf[64];
250
    snprintf(buf, sizeof buf, "{\"frame\":%llu}", fc);
251
    send_json(fd, 200, buf);
252
}
253
254
static void do_setrun(int fd, long v, int paused)
255
{
256
    pthread_mutex_lock(&g_lock);
257
    g_frames_to_run = v;
258
    pthread_mutex_unlock(&g_lock);
259
    send_json(fd, 200, paused ? "{\"paused\":true}" : "{\"paused\":false}");
260
}
261
262
static void handle_conn(int fd)
263
{
264
    char req[2048];
265
    size_t n = 0;
266
    while (n < sizeof(req) - 1) {
267
        ssize_t r = recv(fd, req + n, sizeof(req) - 1 - n, 0);
268
        if (r <= 0) break;
269
        n += (size_t)r;
270
        if (strstr(req, "\r\n\r\n")) break;
271
    }
272
    req[n] = 0;
273
274
    char method[8] = {0}, target[1024] = {0};
275
    if (sscanf(req, "%7s %1023s", method, target) != 2) {
276
        send_json(fd, 400, "{\"error\":\"bad request\"}");
277
        return;
278
    }
279
    char *q = strchr(target, '?');
280
    const char *query = "";
281
    if (q) { *q = 0; query = q + 1; }
282
    int is_post = !strcmp(method, "POST");
283
284
    if (!strcmp(target, "/status"))          { do_status(fd); return; }
285
    if (!strcmp(target, "/regs"))            { do_marshalled(fd, REQ_REGS); return; }
286
    if (!strcmp(target, "/screenshot"))      { do_marshalled(fd, REQ_SHOT); return; }
287
    if (!strcmp(target, "/mem")) {
288
        g_req_addr = (uint32_t)query_long(query, "addr", 0);
289
        g_req_len  = (uint32_t)query_long(query, "len", 0);
290
        g_req_bank = (int32_t)query_long(query, "bank", -1);
291
        do_marshalled(fd, REQ_MEM);
292
        return;
293
    }
294
    if (!strcmp(target, "/step")) {
295
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
296
        do_step(fd, query); return;
297
    }
298
    if (!strcmp(target, "/pause")) {
299
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
300
        do_setrun(fd, 0, 1); return;
301
    }
302
    if (!strcmp(target, "/resume")) {
303
        if (!is_post) { send_json(fd, 405, "{\"error\":\"POST only\"}"); return; }
304
        do_setrun(fd, -1, 0); return;
305
    }
306
    send_json(fd, 404, "{\"error\":\"not found\"}");
307
}
308
309
static void *server_main(void *arg)
310
{
311
    (void)arg;
312
    for (;;) {
313
        int fd = accept(g_listen_fd, NULL, NULL);
314
        if (fd < 0) {
315
            if (errno == EINTR) continue;
316
            break;
317
        }
318
        handle_conn(fd);
319
        close(fd);
320
    }
321
    return NULL;
322
}
323
324
int retro_control_start(int port, const retro_control_backend_t *backend)
325
{
326
    g_be = backend;
327
    signal(SIGPIPE, SIG_IGN);
328
329
    int fd = socket(AF_INET, SOCK_STREAM, 0);
330
    if (fd < 0) return -1;
331
    int one = 1;
332
    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
333
334
    struct sockaddr_in addr;
335
    memset(&addr, 0, sizeof addr);
336
    addr.sin_family = AF_INET;
337
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);   /* 127.0.0.1 only */
338
    addr.sin_port = htons((uint16_t)port);
339
    if (bind(fd, (struct sockaddr *)&addr, sizeof addr) < 0) {
340
        fprintf(stderr, "[retro_control] bind 127.0.0.1:%d failed: %s\n",
341
                port, strerror(errno));
342
        close(fd);
343
        return -1;
344
    }
345
    if (listen(fd, 4) < 0) { close(fd); return -1; }
346
347
    g_listen_fd = fd;
348
    if (pthread_create(&g_thread, NULL, server_main, NULL) != 0) {
349
        close(fd); g_listen_fd = -1; return -1;
350
    }
351
    pthread_detach(g_thread);
352
    fprintf(stderr, "[retro_control] listening on 127.0.0.1:%d (contract %s)\n",
353
            port, RETRO_CONTROL_CONTRACT);
354
    return 0;
355
}
356
357
void retro_control_stop(void)
358
{
359
    if (g_listen_fd >= 0) { close(g_listen_fd); g_listen_fd = -1; }
360
}
361
362
#endif /* !_WIN32 */