#include "control.h" #include "cpu.h" #include "render.h" #include #include #include #include #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // Socket-based control + debug channel. See control.h for the protocol summary. // --------------------------------------------------------------------------- #define MAX_CLIENTS 8 #define MAX_BP 64 #define MAX_WP 32 #define LINEBUF 512 #define READ_CAP 4096 // max bytes per `read` command typedef struct { int fd; char line[LINEBUF]; int len; } Client; static bool active = false; static int listen_fd = -1; static char sock_path[512]; static Client clients[MAX_CLIENTS]; static bool quit_requested = false; // ---- debugger execution state ---- enum { RUN, PAUSED }; static int run_state = RUN; static int step_remaining = 0; // synchronous stepping is done inline, but // a nonzero value also lets run_frame step static bool ignore_bp_once = false; // don't re-break on the addr we resumed from static u16 bp_addr[MAX_BP]; static bool bp_used[MAX_BP]; static u16 wp_addr[MAX_WP]; static bool wp_used[MAX_WP]; static u8 wp_last[MAX_WP]; // Last reason execution halted ("breakpoint 0x0150", "watch 0xFF44", "step", // "pause"), so a per-request client that wasn't connected when an async event // fired can still recover it via the `stopinfo` command. static char last_stop[64] = "none"; // ---- frame recording ---- // Dump the RGB888 framebuffer of each *produced* frame to a flat capture file: // a 10-byte header ("GBCV", u16 width, u16 height, u16 ms-per-frame, all LE) // followed by raw width*height*3 byte frames. tools/gbgif.py turns it into a // GIF. Downsample with everyN (capture 1 of every N frames) for smaller files. static FILE *rec_fp = NULL; static u32 rec_every = 1; static u32 rec_phase = 0; static u64 rec_written = 0; static char rec_path[512]; // ---- input replay (TAS-style movie playback) ---- // A movie is a text file of ` [buttons...]` lines (see input_btn_bit); // while playing, each emulated frame's joypad state is driven from the movie // instead of live input, so a synthesized demo replays deterministically. Pair // with `record` (and a reset) to produce reproducible showcase GIFs. static FILE *movie_fp = NULL; static int movie_hold = 0; // frames left to hold the current mask static u8 movie_mask = 0; static u64 movie_frame = 0; static char movie_path[512]; // --------------------------------------------------------------------------- // small helpers // --------------------------------------------------------------------------- static void set_nonblock(int fd) { int fl = fcntl(fd, F_GETFL, 0); if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK); } // Parse a decimal or 0x-hex number. On success returns value and *ok=true. static long parse_num(const char *s, bool *ok) { if (!s || !*s) { *ok = false; return 0; } char *end = NULL; errno = 0; long v = strtol(s, &end, 0); // base 0: handles 0x.. and decimal *ok = (errno == 0 && end && *end == '\0'); return v; } static void reply(int fd, const char *fmt, ...) { char buf[READ_CAP * 2 + 256]; va_list ap; va_start(ap, fmt); int n = vsnprintf(buf, sizeof(buf) - 2, fmt, ap); va_end(ap); if (n < 0) return; if (n > (int)sizeof(buf) - 2) n = sizeof(buf) - 2; buf[n++] = '\n'; // best-effort; ignore short writes / EPIPE ssize_t w = write(fd, buf, n); (void)w; } static void broadcast(const char *fmt, ...) { char buf[512]; va_list ap; va_start(ap, fmt); int n = vsnprintf(buf, sizeof(buf) - 2, fmt, ap); va_end(ap); if (n < 0) return; if (n > (int)sizeof(buf) - 2) n = sizeof(buf) - 2; buf[n++] = '\n'; for (int i = 0; i < MAX_CLIENTS; i++) if (clients[i].fd >= 0) { ssize_t w = write(clients[i].fd, buf, n); (void)w; } } // Format the CPU/machine context into a single line of key=value pairs. static int fmt_cpu(GB *gb, char *buf, size_t n) { CPU *c = &gb->cpu; return snprintf(buf, n, "af=0x%04X bc=0x%04X de=0x%04X hl=0x%04X sp=0x%04X pc=0x%04X " "ime=%d halted=%d cycles=%llu", c->af, c->bc, c->de, c->hl, c->sp, c->pc, c->ime ? 1 : 0, c->halted ? 1 : 0, (unsigned long long)gb->cycles); } // --------------------------------------------------------------------------- // breakpoints / watchpoints // --------------------------------------------------------------------------- static bool bp_hit(u16 pc) { for (int i = 0; i < MAX_BP; i++) if (bp_used[i] && bp_addr[i] == pc) return true; return false; } // Return watchpoint index whose byte changed since last check (and update its // remembered value), or -1 if none. static int wp_changed(GB *gb) { for (int i = 0; i < MAX_WP; i++) { if (!wp_used[i]) continue; u8 v = gb_read(gb, wp_addr[i]); if (v != wp_last[i]) { u8 old = wp_last[i]; wp_last[i] = v; (void)old; return i; } } return -1; } static void enter_paused(void) { run_state = PAUSED; step_remaining = 0; } static void set_stop(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vsnprintf(last_stop, sizeof(last_stop), fmt, ap); va_end(ap); } // --------------------------------------------------------------------------- // synchronous single-stepping (used by the `step` command for an immediate // reply). Executes up to `n` instructions, stopping early on a breakpoint or // watchpoint. Writes a reason string ("step"/"breakpoint"/"watch") and, for a // watch hit, the watched address into *waddr. // --------------------------------------------------------------------------- static const char *do_steps(GB *gb, int n, u16 *waddr) { const char *reason = "step"; bool first = true; for (int i = 0; i < n; i++) { // honor a breakpoint we land on, but never on the very first step off // the current address (otherwise we could never leave it) if (!first && bp_hit(gb->cpu.pc)) { set_stop("breakpoint 0x%04X", gb->cpu.pc); return "breakpoint"; } first = false; cpu_step(gb); int w = wp_changed(gb); if (w >= 0) { if (waddr) *waddr = wp_addr[w]; set_stop("watch 0x%04X", wp_addr[w]); return "watch"; } } set_stop("step 0x%04X", gb->cpu.pc); return reason; } // --------------------------------------------------------------------------- // command dispatch // --------------------------------------------------------------------------- static void cmd_break_list(int fd) { char buf[400]; int o = 0; o += snprintf(buf + o, sizeof(buf) - o, "ok breaks"); for (int i = 0; i < MAX_BP; i++) if (bp_used[i]) o += snprintf(buf + o, sizeof(buf) - o, " #%d=0x%04X", i, bp_addr[i]); reply(fd, "%s", buf); } static void cmd_watch_list(int fd) { char buf[400]; int o = 0; o += snprintf(buf + o, sizeof(buf) - o, "ok watches"); for (int i = 0; i < MAX_WP; i++) if (wp_used[i]) o += snprintf(buf + o, sizeof(buf) - o, " #%d=0x%04X:0x%02X", i, wp_addr[i], wp_last[i]); reply(fd, "%s", buf); } // Set a named register/flag. Returns true on success. static bool set_reg(GB *gb, const char *name, long v) { CPU *c = &gb->cpu; u16 w = (u16)v; u8 b = (u8)v; if (!strcasecmp(name, "a")) { c->a = b; return true; } if (!strcasecmp(name, "f")) { c->f = b & 0xF0; return true; } if (!strcasecmp(name, "b")) { c->b = b; return true; } if (!strcasecmp(name, "c")) { c->c = b; return true; } if (!strcasecmp(name, "d")) { c->d = b; return true; } if (!strcasecmp(name, "e")) { c->e = b; return true; } if (!strcasecmp(name, "h")) { c->h = b; return true; } if (!strcasecmp(name, "l")) { c->l = b; return true; } if (!strcasecmp(name, "af")) { c->af = w & 0xFFF0; return true; } if (!strcasecmp(name, "bc")) { c->bc = w; return true; } if (!strcasecmp(name, "de")) { c->de = w; return true; } if (!strcasecmp(name, "hl")) { c->hl = w; return true; } if (!strcasecmp(name, "sp")) { c->sp = w; return true; } if (!strcasecmp(name, "pc")) { c->pc = w; return true; } if (!strcasecmp(name, "ime")) { c->ime = (v != 0); return true; } return false; } static int hexval(int ch) { if (ch >= '0' && ch <= '9') return ch - '0'; if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10; return -1; } // Parse a single byte written as hex ("de", "0xDE", "FF", "7"). Memory writes // default to hex so they round-trip with `read` output. Returns 0..255 or -1. static int parse_byte_hex(const char *s) { if (!s || !*s) return -1; if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2; int val = 0, digits = 0; for (; *s; s++) { int h = hexval((unsigned char)*s); if (h < 0) return -1; val = (val << 4) | h; if (++digits > 2) return -1; } return digits ? val : -1; } // Dispatch one command line from client `fd`. `orig` is the untouched line // (used for the button-command fallback); `line` is a mutable copy. // movie button name -> our pressed-state bit (see gb.h BTN_*). Anything else // ("-", "none", "idle", unknown) contributes nothing = released. static u8 input_btn_bit(const char *s) { if (!strcasecmp(s, "a")) return BTN_A; if (!strcasecmp(s, "b")) return BTN_B; if (!strcasecmp(s, "start")) return BTN_START; if (!strcasecmp(s, "select")) return BTN_SELECT; if (!strcasecmp(s, "up")) return BTN_UP; if (!strcasecmp(s, "down")) return BTN_DOWN; if (!strcasecmp(s, "left")) return BTN_LEFT; if (!strcasecmp(s, "right")) return BTN_RIGHT; return 0; } // Exact, repeatable power-on state so a movie replays identically every time. // Startup is calloc(zero) -> cart_load -> gb_reset, so we reproduce it: preserve // the loaded cart (ROM + SRAM, so the save is intact), zero *all* other emulator // state, then the normal post-boot reset. Zeroing only some RAM (and leaving // e.g. double-speed / timer / DMA residue from the prior session) makes the boot // diverge run to run -- this wipes the lot. static void replay_reset(GB *gb) { Cart cart = gb->cart; // struct of pointers into the loaded ROM/SRAM memset(gb, 0, sizeof(*gb)); gb->cart = cart; gb_reset(gb); } static void dispatch(GB *gb, int fd, const char *orig, char *line) { // tokenize (whitespace); keep argv for structured commands char *argv[68]; int argc = 0; for (char *t = strtok(line, " \t\r\n"); t && argc < 68; t = strtok(NULL, " \t\r\n")) argv[argc++] = t; if (argc == 0) return; const char *v = argv[0]; char cbuf[256]; if (!strcasecmp(v, "ping")) { reply(fd, "pong"); return; } if (!strcasecmp(v, "help")) { reply(fd, "ok commands: ping state stopinfo cpu reg read write step " "continue pause break watch delete unwatch quit | button tokens " "(a b start select up down left right, +x -x, name:N, release)"); return; } if (!strcasecmp(v, "quit") || !strcasecmp(v, "exit")) { quit_requested = true; reply(fd, "ok bye"); return; } if (!strcasecmp(v, "state")) { reply(fd, "ok state %s", run_state == PAUSED ? "paused" : "running"); return; } if (!strcasecmp(v, "stopinfo") || !strcasecmp(v, "laststop") || !strcasecmp(v, "why")) { fmt_cpu(gb, cbuf, sizeof(cbuf)); if (run_state == PAUSED) reply(fd, "ok stop %s %s", last_stop, cbuf); else reply(fd, "ok running %s", cbuf); return; } if (!strcasecmp(v, "cpu") || !strcasecmp(v, "regs") || !strcasecmp(v, "context")) { fmt_cpu(gb, cbuf, sizeof(cbuf)); reply(fd, "ok cpu %s", cbuf); return; } if (!strcasecmp(v, "reg")) { bool ok; long val; if (argc < 3) { reply(fd, "err reg "); return; } val = parse_num(argv[2], &ok); if (!ok) { reply(fd, "err bad value"); return; } if (!set_reg(gb, argv[1], val)) { reply(fd, "err unknown reg '%s'", argv[1]); return; } reply(fd, "ok reg %s=0x%04X", argv[1], (unsigned)val); return; } if (!strcasecmp(v, "read") || !strcasecmp(v, "r") || !strcasecmp(v, "mem")) { bool ok; long addr, len = 1; if (argc < 2) { reply(fd, "err read [len]"); return; } addr = parse_num(argv[1], &ok); if (!ok) { reply(fd, "err bad addr"); return; } if (argc >= 3) { len = parse_num(argv[2], &ok); if (!ok) { reply(fd, "err bad len"); return; } } if (len < 1) len = 1; if (len > READ_CAP) len = READ_CAP; static char hex[READ_CAP * 2 + 1]; int o = 0; for (long i = 0; i < len; i++) { u8 val = gb_read(gb, (u16)(addr + i)); static const char *H = "0123456789abcdef"; hex[o++] = H[val >> 4]; hex[o++] = H[val & 0xF]; } hex[o] = 0; reply(fd, "ok read 0x%04X %ld %s", (unsigned)addr, len, hex); return; } if (!strcasecmp(v, "write") || !strcasecmp(v, "w")) { bool ok; if (argc < 3) { reply(fd, "err write "); return; } long addr = parse_num(argv[1], &ok); if (!ok) { reply(fd, "err bad addr"); return; } int count = 0; // form 1: a single contiguous even-length hex string "deadbeef" if (argc == 3) { const char *s = argv[2]; size_t sl = strlen(s); bool allhex = (sl >= 2 && sl % 2 == 0); for (size_t i = 0; i < sl && allhex; i++) if (hexval(s[i]) < 0) allhex = false; if (allhex) { for (size_t i = 0; i + 1 < sl; i += 2) { u8 val = (u8)((hexval(s[i]) << 4) | hexval(s[i + 1])); gb_write(gb, (u16)(addr + count), val); count++; } reply(fd, "ok write 0x%04X %d", (unsigned)addr, count); return; } } // form 2: space-separated hex bytes ("de ad be ef", 0x-prefix ok) (void)ok; for (int i = 2; i < argc; i++) { int b = parse_byte_hex(argv[i]); if (b < 0) { reply(fd, "err bad byte '%s'", argv[i]); return; } gb_write(gb, (u16)(addr + count), (u8)b); count++; } reply(fd, "ok write 0x%04X %d", (unsigned)addr, count); return; } if (!strcasecmp(v, "step") || !strcasecmp(v, "s") || !strcasecmp(v, "si")) { bool ok; long n = 1; if (argc >= 2) { n = parse_num(argv[1], &ok); if (!ok || n < 1) n = 1; } ignore_bp_once = true; u16 waddr = 0; const char *reason = do_steps(gb, (int)n, &waddr); enter_paused(); fmt_cpu(gb, cbuf, sizeof(cbuf)); if (!strcmp(reason, "watch")) reply(fd, "ok stop watch 0x%04X %s", waddr, cbuf); else reply(fd, "ok stop %s %s", reason, cbuf); return; } if (!strcasecmp(v, "continue") || !strcasecmp(v, "cont") || !strcasecmp(v, "c") || !strcasecmp(v, "run")) { run_state = RUN; step_remaining = 0; ignore_bp_once = true; // step off current bp/pc before re-checking reply(fd, "ok running"); return; } if (!strcasecmp(v, "pause") || !strcasecmp(v, "stop") || !strcasecmp(v, "halt") || !strcasecmp(v, "break!")) { enter_paused(); set_stop("pause 0x%04X", gb->cpu.pc); fmt_cpu(gb, cbuf, sizeof(cbuf)); reply(fd, "ok paused %s", cbuf); return; } if (!strcasecmp(v, "break") || !strcasecmp(v, "bp") || !strcasecmp(v, "b")) { if (argc < 2) { cmd_break_list(fd); return; } bool ok; long addr = parse_num(argv[1], &ok); if (!ok) { reply(fd, "err bad addr"); return; } for (int i = 0; i < MAX_BP; i++) // dedup if (bp_used[i] && bp_addr[i] == (u16)addr) { reply(fd, "ok break #%d 0x%04X", i, (unsigned)addr); return; } for (int i = 0; i < MAX_BP; i++) if (!bp_used[i]) { bp_used[i] = true; bp_addr[i] = (u16)addr; reply(fd, "ok break #%d 0x%04X", i, (unsigned)addr); return; } reply(fd, "err breakpoint table full"); return; } if (!strcasecmp(v, "delete") || !strcasecmp(v, "del") || !strcasecmp(v, "unbreak") || !strcasecmp(v, "d")) { if (argc < 2) { reply(fd, "err delete <0xaddr|#idx|all>"); return; } if (!strcasecmp(argv[1], "all")) { memset(bp_used, 0, sizeof(bp_used)); reply(fd, "ok deleted all"); return; } if (argv[1][0] == '#') { int idx = atoi(argv[1] + 1); if (idx >= 0 && idx < MAX_BP && bp_used[idx]) { bp_used[idx] = false; reply(fd, "ok deleted #%d", idx); return; } reply(fd, "err no such breakpoint"); return; } bool ok; long addr = parse_num(argv[1], &ok); if (!ok) { reply(fd, "err bad addr"); return; } for (int i = 0; i < MAX_BP; i++) if (bp_used[i] && bp_addr[i] == (u16)addr) { bp_used[i] = false; reply(fd, "ok deleted #%d 0x%04X", i, (unsigned)addr); return; } reply(fd, "err no breakpoint at 0x%04X", (unsigned)addr); return; } if (!strcasecmp(v, "watch") || !strcasecmp(v, "wp")) { if (argc < 2) { cmd_watch_list(fd); return; } bool ok; long addr = parse_num(argv[1], &ok); if (!ok) { reply(fd, "err bad addr"); return; } for (int i = 0; i < MAX_WP; i++) if (!wp_used[i]) { wp_used[i] = true; wp_addr[i] = (u16)addr; wp_last[i] = gb_read(gb, (u16)addr); reply(fd, "ok watch #%d 0x%04X:0x%02X", i, (unsigned)addr, wp_last[i]); return; } reply(fd, "err watchpoint table full"); return; } if (!strcasecmp(v, "unwatch")) { if (argc < 2) { reply(fd, "err unwatch <0xaddr|#idx|all>"); return; } if (!strcasecmp(argv[1], "all")) { memset(wp_used, 0, sizeof(wp_used)); reply(fd, "ok unwatched all"); return; } if (argv[1][0] == '#') { int idx = atoi(argv[1] + 1); if (idx >= 0 && idx < MAX_WP && wp_used[idx]) { wp_used[idx] = false; reply(fd, "ok unwatched #%d", idx); return; } reply(fd, "err no such watchpoint"); return; } bool ok; long addr = parse_num(argv[1], &ok); if (!ok) { reply(fd, "err bad addr"); return; } for (int i = 0; i < MAX_WP; i++) if (wp_used[i] && wp_addr[i] == (u16)addr) { wp_used[i] = false; reply(fd, "ok unwatched 0x%04X", (unsigned)addr); return; } reply(fd, "err no watchpoint at 0x%04X", (unsigned)addr); return; } if (!strcasecmp(v, "record") || !strcasecmp(v, "rec")) { const char *sub = argc >= 2 ? argv[1] : "status"; if (!strcasecmp(sub, "start")) { if (argc < 3) { reply(fd, "err record start [everyN]"); return; } if (rec_fp) { fclose(rec_fp); rec_fp = NULL; } u32 every = 1; if (argc >= 4) { bool ok; long n = parse_num(argv[3], &ok); if (ok && n > 0) every = (u32)n; } FILE *f = fopen(argv[2], "wb"); if (!f) { reply(fd, "err cannot open '%s'", argv[2]); return; } // Capture what the user sees: chrome frame + LCD recolor if enabled. int cw, ch; render_capture_size(&cw, &ch); u16 w = (u16)cw, h = (u16)ch; // playback interval per captured frame (the GB runs ~59.73 fps) u16 ms = (u16)(1000.0 * every / 59.73 + 0.5); u8 hdr[10] = { 'G','B','C','V', (u8)(w & 0xff), (u8)(w >> 8), (u8)(h & 0xff), (u8)(h >> 8), (u8)(ms & 0xff), (u8)(ms >> 8) }; fwrite(hdr, 1, sizeof(hdr), f); rec_fp = f; rec_every = every; rec_phase = 0; rec_written = 0; snprintf(rec_path, sizeof(rec_path), "%s", argv[2]); reply(fd, "ok record start %s every=%u", rec_path, (unsigned)every); return; } if (!strcasecmp(sub, "stop")) { if (!rec_fp) { reply(fd, "err not recording"); return; } fclose(rec_fp); rec_fp = NULL; reply(fd, "ok record stop %s frames=%llu", rec_path, (unsigned long long)rec_written); return; } // status if (rec_fp) reply(fd, "ok record active %s frames=%llu every=%u", rec_path, (unsigned long long)rec_written, (unsigned)rec_every); else reply(fd, "ok record inactive"); return; } if (!strcasecmp(v, "input") || !strcasecmp(v, "movie") || !strcasecmp(v, "play")) { const char *sub = argc >= 2 ? argv[1] : "status"; if (!strcasecmp(sub, "play") || !strcasecmp(sub, "load")) { if (argc < 3) { reply(fd, "err input play [reset]"); return; } if (movie_fp) { fclose(movie_fp); movie_fp = NULL; } FILE *f = fopen(argv[2], "r"); if (!f) { reply(fd, "err cannot open '%s'", argv[2]); return; } bool do_reset = (argc >= 4 && !strcasecmp(argv[3], "reset")); if (do_reset) replay_reset(gb); movie_fp = f; movie_hold = 0; movie_mask = 0; movie_frame = 0; snprintf(movie_path, sizeof movie_path, "%s", argv[2]); reply(fd, "ok input play %s%s", movie_path, do_reset ? " reset" : ""); return; } if (!strcasecmp(sub, "stop")) { if (!movie_fp) { reply(fd, "err not playing"); return; } fclose(movie_fp); movie_fp = NULL; movie_hold = 0; gb->buttons = 0; reply(fd, "ok input stop %s frame=%llu", movie_path, (unsigned long long)movie_frame); return; } if (movie_fp) reply(fd, "ok input playing %s frame=%llu", movie_path, (unsigned long long)movie_frame); else reply(fd, "ok input idle"); return; } // fallback: treat the whole line as button-command tokens (legacy FIFO // vocabulary). Reply ok so scripted callers can synchronize. input_handle_command(orig); reply(fd, "ok"); } // --------------------------------------------------------------------------- // connection handling // --------------------------------------------------------------------------- static void drop_client(int i) { if (clients[i].fd >= 0) { close(clients[i].fd); clients[i].fd = -1; } clients[i].len = 0; } static void accept_new(void) { for (;;) { int cf = accept(listen_fd, NULL, NULL); if (cf < 0) break; // EAGAIN / no pending set_nonblock(cf); int slot = -1; for (int i = 0; i < MAX_CLIENTS; i++) if (clients[i].fd < 0) { slot = i; break; } if (slot < 0) { close(cf); continue; } // table full clients[slot].fd = cf; clients[slot].len = 0; reply(cf, "ok sl0pboy control channel; 'help' for commands"); } } static void service_client(GB *gb, int i) { char buf[512]; for (;;) { int n = (int)read(clients[i].fd, buf, sizeof(buf)); if (n == 0) { drop_client(i); return; } // peer closed if (n < 0) break; // EAGAIN for (int k = 0; k < n; k++) { char c = buf[k]; if (c == '\n' || c == '\r') { clients[i].line[clients[i].len] = 0; if (clients[i].len) { char orig[LINEBUF]; memcpy(orig, clients[i].line, clients[i].len + 1); dispatch(gb, clients[i].fd, orig, clients[i].line); } clients[i].len = 0; } else if (clients[i].len < LINEBUF - 1) { clients[i].line[clients[i].len++] = c; } } } } // --------------------------------------------------------------------------- // public API // --------------------------------------------------------------------------- void control_open(const char *path) { // A client can connect and vanish before we finish writing (e.g. a liveness // probe that connects and closes without reading the greeting). Ignore // SIGPIPE so those writes fail with EPIPE instead of killing the emulator. signal(SIGPIPE, SIG_IGN); for (int i = 0; i < MAX_CLIENTS; i++) clients[i].fd = -1; listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (listen_fd < 0) { perror("socket"); return; } struct sockaddr_un sa; memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX; snprintf(sa.sun_path, sizeof(sa.sun_path), "%s", path); snprintf(sock_path, sizeof(sock_path), "%s", path); unlink(sock_path); // clear a stale socket if (bind(listen_fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("bind"); close(listen_fd); listen_fd = -1; return; } if (listen(listen_fd, 4) < 0) { perror("listen"); close(listen_fd); listen_fd = -1; unlink(sock_path); return; } set_nonblock(listen_fd); active = true; } void control_close(void) { if (rec_fp) { fclose(rec_fp); rec_fp = NULL; } if (movie_fp) { fclose(movie_fp); movie_fp = NULL; } if (!active) return; for (int i = 0; i < MAX_CLIENTS; i++) drop_client(i); if (listen_fd >= 0) { close(listen_fd); listen_fd = -1; } unlink(sock_path); active = false; } // Called once per main-loop iteration, right after control_run_frame(). Appends // the just-produced frame to the capture file (honoring the everyN downsample). // Gated on frame_ready so a paused/stepping machine doesn't re-record a still. void control_record_frame(GB *gb) { if (!rec_fp || !gb->ppu.frame_ready) return; gb->ppu.frame_ready = false; // consume: don't re-capture while paused if ((rec_phase++ % rec_every) != 0) return; int w, h; const u8 *frame = render_capture_frame(gb, &w, &h); // matches the display fwrite(frame, 1, (size_t)w * h * 3, rec_fp); rec_written++; } // Called once per main-loop iteration BEFORE control_run_frame(): if a movie is // playing, drive this frame's joypad state from it (overriding live input). // Frame-locked, so a synthesized demo replays deterministically. No-op (live // input passes through) when idle or while the debugger holds execution. void control_input_frame(GB *gb) { if (!movie_fp || run_state == PAUSED) return; while (movie_hold <= 0) { char buf[256]; if (!fgets(buf, sizeof buf, movie_fp)) { // end of movie fclose(movie_fp); movie_fp = NULL; gb->buttons = 0; // release everything return; } char *hash = strchr(buf, '#'); if (hash) *hash = 0; char *tok = strtok(buf, " \t\r\n"); if (!tok) continue; // blank / comment-only line bool ok; long n = parse_num(tok, &ok); if (!ok || n <= 0) continue; // need a positive frame count u8 mask = 0; for (char *t = strtok(NULL, " \t\r\n"); t; t = strtok(NULL, " \t\r\n")) mask |= input_btn_bit(t); movie_hold = (int)n; movie_mask = mask; } gb->buttons = movie_mask; movie_hold--; movie_frame++; } bool control_active(void) { return active; } bool control_paused(void) { return active && run_state == PAUSED; } bool control_poll(GB *gb) { if (!active) return true; accept_new(); for (int i = 0; i < MAX_CLIENTS; i++) if (clients[i].fd >= 0) service_client(gb, i); return !quit_requested; } void control_run_frame(GB *gb) { // Plain full-frame emulation when there's no active debug channel. if (!active) { gb->ppu.frame_ready = false; u64 budget = gb->cycles + 70224 * 2; while (!gb->ppu.frame_ready && gb->cycles < budget) cpu_step(gb); return; } // Fully paused (not stepping): freeze the machine entirely. if (run_state == PAUSED) return; gb->ppu.frame_ready = false; u64 budget = gb->cycles + 70224 * 2; while (gb->cycles < budget) { // breakpoint check before executing the next instruction if (!ignore_bp_once && bp_hit(gb->cpu.pc)) { enter_paused(); set_stop("breakpoint 0x%04X", gb->cpu.pc); char cbuf[256]; fmt_cpu(gb, cbuf, sizeof(cbuf)); broadcast("event stop breakpoint %s", cbuf); return; } ignore_bp_once = false; cpu_step(gb); int w = wp_changed(gb); if (w >= 0) { enter_paused(); set_stop("watch 0x%04X", wp_addr[w]); char cbuf[256]; fmt_cpu(gb, cbuf, sizeof(cbuf)); broadcast("event stop watch 0x%04X %s", wp_addr[w], cbuf); return; } if (gb->ppu.frame_ready) return; } }