diff options
| author | gbc dev <gbc@localhost> | 2026-07-14 23:26:49 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-07-14 23:26:49 +0200 |
| commit | c879adc43b91a53eb7e76f232a39abb56d8144c1 (patch) | |
| tree | 3bd8cd77fed46ceb62504d2fb067f349bad2cf0e /src/control.c | |
| parent | speed control: frame skipping to decouple terminal draw from emulation (diff) | |
| download | sl0pboy-c879adc43b91a53eb7e76f232a39abb56d8144c1.tar.gz sl0pboy-c879adc43b91a53eb7e76f232a39abb56d8144c1.tar.xz sl0pboy-c879adc43b91a53eb7e76f232a39abb56d8144c1.zip | |
control: socket-based debug channel + gbctl CLI driver
Replace the input-only FIFO's limitations with a Unix-domain socket control
channel (--sock) that is a superset of the button vocabulary plus emulator
introspection: read/write bus memory, get/set CPU context, single-step, PC
breakpoints, value-change watchpoints, run/pause. Being a socket it replies to
each command and broadcasts async 'event stop ...' lines; a stored last-stop
record (stopinfo) lets per-request clients recover a missed event.
Add gbctl: a stdlib-python CLI that spawns a tmux pane running the emulator on
a ROM, auto-resolves the socket, and drives it with terse verbs (cpu/read/write/
reg/step/break/watch/continue/pause/press/hold/screen/stop). The FIFO stays as
legacy input-only.
Diffstat (limited to 'src/control.c')
| -rw-r--r-- | src/control.c | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/src/control.c b/src/control.c new file mode 100644 index 0000000..322646b --- /dev/null +++ b/src/control.c @@ -0,0 +1,591 @@ +#include "control.h" +#include "cpu.h" +#include "render.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <stdarg.h> +#include <ctype.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/socket.h> +#include <sys/un.h> + +// --------------------------------------------------------------------------- +// 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"; + +// --------------------------------------------------------------------------- +// 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. +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 <name> <value>"); 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 <addr> [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 <addr> <byte...|hexstring>"); + 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; + } + + // 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 gbc 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) { + 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 (!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; +} + +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; + } +} |
