aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-14 23:26:49 +0200
committergbc dev <gbc@localhost>2026-07-14 23:26:49 +0200
commitc879adc43b91a53eb7e76f232a39abb56d8144c1 (patch)
tree3bd8cd77fed46ceb62504d2fb067f349bad2cf0e /src
parentspeed control: frame skipping to decouple terminal draw from emulation (diff)
downloadsl0pboy-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')
-rw-r--r--src/control.c591
-rw-r--r--src/control.h36
-rw-r--r--src/main.c29
-rw-r--r--src/render.c135
-rw-r--r--src/render.h8
5 files changed, 797 insertions, 2 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;
+ }
+}
diff --git a/src/control.h b/src/control.h
new file mode 100644
index 0000000..a625853
--- /dev/null
+++ b/src/control.h
@@ -0,0 +1,36 @@
+#ifndef GBC_CONTROL_H
+#define GBC_CONTROL_H
+
+#include "gb.h"
+
+// Socket-based external control + debug channel.
+//
+// This is a superset of the legacy input FIFO: a Unix-domain stream socket that
+// accepts the same button-command vocabulary (see render.h / README) *plus*
+// emulator-introspection commands (memory read/write, CPU context, single-step,
+// breakpoints, watchpoints, run/pause). Because it is a socket it can reply to
+// each command and push asynchronous "event stop ..." lines when execution
+// halts at a breakpoint / step boundary / watchpoint.
+//
+// The protocol is line based (one command per line, '\n' terminated). Replies
+// begin with "ok" or "err"; asynchronous notifications begin with "event".
+
+// Create and start listening on a Unix-domain socket at `path`. Safe to call
+// once at startup. On failure prints to stderr and leaves the channel inactive.
+void control_open(const char *path);
+void control_close(void);
+bool control_active(void);
+
+// Accept new connections and dispatch any pending commands against `gb`.
+// Returns false if a client requested the emulator to quit.
+bool control_poll(GB *gb);
+
+// True while the debugger is holding execution (no CPU advancement).
+bool control_paused(void);
+
+// Advance emulation by up to one video frame, honoring the debugger's
+// pause / single-step / breakpoint / watchpoint state. When the control channel
+// is inactive this simply runs a full frame like the plain emulator loop.
+void control_run_frame(GB *gb);
+
+#endif
diff --git a/src/main.c b/src/main.c
index 5ce4fb5..541ce44 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,6 +1,7 @@
#include "gb.h"
#include "cpu.h"
#include "render.h"
+#include "control.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -26,7 +27,10 @@ static void run_frame(GB *gb) {
int main(int argc, char **argv) {
if (argc < 2) {
- fprintf(stderr, "usage: %s <rom> [--test seconds]\n", argv[0]);
+ fprintf(stderr, "usage: %s <rom> [--test seconds] [--sixel [scale]] "
+ "[--frameskip n] [--fps n] [--uncapped] [--fifo [path]] "
+ "[--sock [path]]\n",
+ argv[0]);
return 1;
}
const char *rom = argv[1];
@@ -35,8 +39,11 @@ int main(int argc, char **argv) {
int shot_frames = -1;
const char *shot_path = "shot.ppm";
const char *fifo_path = NULL;
+ const char *sock_path = NULL;
double target_fps = 59.73; // emulation speed cap; 0 = uncapped
int frameskip = 0; // draw 1 of every (frameskip+1) frames
+ bool sixel = false; // use sixel graphics output
+ int sixel_scale = 2; // integer pixel zoom for sixel
for (int i = 2; i < argc; i++) {
if (!strcmp(argv[i], "--test")) {
test = true;
@@ -47,10 +54,16 @@ int main(int argc, char **argv) {
} else if (!strcmp(argv[i], "--fifo")) {
fifo_path = (i + 1 < argc && argv[i+1][0] != '-')
? argv[++i] : "/tmp/gbc.fifo";
+ } else if (!strcmp(argv[i], "--sock") || !strcmp(argv[i], "--socket")) {
+ sock_path = (i + 1 < argc && argv[i+1][0] != '-')
+ ? argv[++i] : "/tmp/gbc.sock";
} else if (!strcmp(argv[i], "--fps")) {
if (i + 1 < argc) target_fps = atof(argv[++i]);
} else if (!strcmp(argv[i], "--frameskip")) {
if (i + 1 < argc) frameskip = atoi(argv[++i]);
+ } else if (!strcmp(argv[i], "--sixel")) {
+ sixel = true;
+ if (i + 1 < argc && argv[i+1][0] != '-') sixel_scale = atoi(argv[++i]);
} else if (!strcmp(argv[i], "--uncapped") || !strcmp(argv[i], "--turbo")) {
target_fps = 0;
}
@@ -90,6 +103,14 @@ int main(int argc, char **argv) {
fprintf(stderr, "control fifo: echo 'a' > %s (a b start select "
"up down left right; +x/-x hold/release; release)\n", fifo_path);
}
+ if (sock_path) {
+ control_open(sock_path);
+ if (control_active())
+ fprintf(stderr, "control socket: %s (buttons + debug: cpu read "
+ "write step break watch continue pause; 'help')\n",
+ sock_path);
+ }
+ if (sixel) render_set_sixel(true, sixel_scale);
term_init();
// The emulator always advances every GB frame at the paced native rate so
@@ -107,13 +128,16 @@ int main(int argc, char **argv) {
while (running) {
frames++;
if (!input_poll(gb)) break;
+ if (!control_poll(gb)) break; // socket may inject input / debug cmds
if (input_take_turbo()) turbo = !turbo;
frameskip += input_take_frameskip_delta();
if (frameskip < 0) frameskip = 0;
if (frameskip > 30) frameskip = 30;
render_set_hud(frameskip, turbo);
- run_frame(gb); // always emulate a full frame
+ // Emulate a full frame, honoring debugger pause/step/breakpoints when
+ // the control socket is active (otherwise a plain full frame).
+ control_run_frame(gb);
// decide whether to draw this frame
bool do_render = (since_render >= frameskip);
@@ -148,6 +172,7 @@ int main(int argc, char **argv) {
term_restore();
input_close_fifo();
+ control_close();
double elapsed = now_sec() - start_time;
if (elapsed > 0)
fprintf(stderr, "emulated %llu frames (%.1f fps), drew %llu (%.1f fps) "
diff --git a/src/render.c b/src/render.c
index df39c99..2320f02 100644
--- a/src/render.c
+++ b/src/render.c
@@ -54,6 +54,31 @@ static volatile sig_atomic_t resized = 0; // set by SIGWINCH
static int hud_frameskip = 0;
static bool hud_turbo = false;
+// ---- sixel output ----
+static bool sixel_enabled = false;
+static int sixel_scale = 2;
+static char *sixel_buf = NULL; // reusable output buffer
+static size_t sixel_buf_cap = 0;
+
+void render_set_sixel(bool on, int scale) {
+ if (scale < 1) scale = 1;
+ if (scale > 6) scale = 6;
+ sixel_enabled = on;
+ sixel_scale = scale;
+ if (on) {
+ int W2 = SCREEN_W * scale, H2 = SCREEN_H * scale;
+ int bands = (H2 + 5) / 6;
+ // worst case: every band emits every palette color across full width
+ size_t cap = (size_t)bands * 256 * (W2 + 8) + 65536;
+ if (cap > sixel_buf_cap) {
+ free(sixel_buf);
+ sixel_buf = malloc(cap);
+ sixel_buf_cap = sixel_buf ? cap : 0;
+ }
+ }
+ need_full = true;
+}
+
void render_force_full(void) { need_full = true; }
// Update the on-screen HUD values; forces a status-line repaint on change.
@@ -66,7 +91,110 @@ void render_set_hud(int frameskip, bool turbo) {
}
static void on_winch(int s) { (void)s; resized = 1; }
+// ---- sixel encoder ---------------------------------------------------------
+// The GB framebuffer is truecolor RGB but any given frame uses few distinct
+// colors (4 shades on DMG, a modest set on CGB). We build a per-frame palette
+// (<=256 entries), quantizing more coarsely only if a frame overflows, then
+// emit standard sixel bands.
+static int pal_r[256], pal_g[256], pal_b[256];
+static int pal_key[256];
+static u8 idxmap[SCREEN_H][SCREEN_W]; // palette index per source pixel
+
+static int build_palette(PPU *p) {
+ static int htab[8192];
+ for (int shift = 0; ; shift++) {
+ memset(htab, -1, sizeof(htab));
+ int mask = (0xFF << shift) & 0xFF;
+ int n = 0, overflow = 0;
+ for (int y = 0; y < SCREEN_H && !overflow; y++) {
+ for (int x = 0; x < SCREEN_W; x++) {
+ int r = p->fb[y][x][0] & mask;
+ int g = p->fb[y][x][1] & mask;
+ int b = p->fb[y][x][2] & mask;
+ int key = (r << 16) | (g << 8) | b;
+ unsigned h = ((unsigned)key * 2654435761u) & 8191u;
+ while (htab[h] != -1 && pal_key[htab[h]] != key)
+ h = (h + 1) & 8191u;
+ if (htab[h] == -1) {
+ if (n >= 256) { overflow = 1; break; }
+ htab[h] = n;
+ pal_key[n] = key;
+ pal_r[n] = r; pal_g[n] = g; pal_b[n] = b;
+ n++;
+ }
+ idxmap[y][x] = (u8)htab[h];
+ }
+ }
+ if (!overflow || shift >= 7) return n;
+ }
+}
+
+static void render_frame_sixel(GB *gb) {
+ PPU *p = &gb->ppu;
+ if (!sixel_buf) return;
+ int sc = sixel_scale;
+ int W2 = SCREEN_W * sc, H2 = SCREEN_H * sc;
+ int ncol = build_palette(p);
+ char *o = sixel_buf;
+
+ // home cursor, start sixel with 1:1 pixel aspect + raster size
+ o += sprintf(o, "\x1b[H\x1bPq\"1;1;%d;%d", W2, H2);
+ for (int i = 0; i < ncol; i++) // palette (0-100 percent)
+ o += sprintf(o, "#%d;2;%d;%d;%d", i,
+ (pal_r[i] * 100 + 127) / 255,
+ (pal_g[i] * 100 + 127) / 255,
+ (pal_b[i] * 100 + 127) / 255);
+
+ static u8 present[256];
+ static u8 sx[SCREEN_W * 6]; // sixel value per column for the active color
+ for (int by = 0; by < H2; by += 6) {
+ // which palette colors appear in this 6-row band?
+ memset(present, 0, ncol);
+ int rows = (H2 - by < 6) ? (H2 - by) : 6;
+ for (int k = 0; k < rows; k++) {
+ int sy = (by + k) / sc;
+ for (int x = 0; x < SCREEN_W; x++) present[idxmap[sy][x]] = 1;
+ }
+ int emitted = 0;
+ for (int c = 0; c < ncol; c++) {
+ if (!present[c]) continue;
+ if (emitted) *o++ = '$'; // graphics CR: back to band start
+ emitted = 1;
+ // build this color's 6-bit column values
+ for (int X2 = 0; X2 < W2; X2++) {
+ int sx0 = X2 / sc, bits = 0;
+ for (int k = 0; k < rows; k++)
+ if (idxmap[(by + k) / sc][sx0] == c) bits |= (1 << k);
+ sx[X2] = (u8)(0x3f + bits);
+ }
+ o += sprintf(o, "#%d", c);
+ // run-length encode
+ int X2 = 0;
+ while (X2 < W2) {
+ int run = 1;
+ while (X2 + run < W2 && sx[X2 + run] == sx[X2]) run++;
+ if (run >= 4) o += sprintf(o, "!%d%c", run, sx[X2]);
+ else for (int j = 0; j < run; j++) *o++ = sx[X2];
+ X2 += run;
+ }
+ }
+ *o++ = '-'; // next band
+ }
+ o += sprintf(o, "\x1b\\"); // ST: end sixel
+
+ // status line just below the image
+ o += sprintf(o, "\r\n\x1b[38;2;150;150;150m %.*s skip:%d%s "
+ "[f]ast [ ][ ] q:quit\x1b[0m\x1b[K",
+ 12, gb->cart.title, hud_frameskip,
+ hud_turbo ? " TURBO" : "");
+
+ need_full = false;
+ fwrite(sixel_buf, 1, o - sixel_buf, stdout);
+ fflush(stdout);
+}
+
void render_frame(GB *gb) {
+ if (sixel_enabled) { render_frame_sixel(gb); return; }
PPU *p = &gb->ppu;
char *o = outbuf;
@@ -190,6 +318,13 @@ static void handle_line(char *line) {
handle_token(t);
}
+// Public entry: process a line of button tokens from any control channel.
+void input_handle_command(const char *line) {
+ char tmp[256];
+ snprintf(tmp, sizeof(tmp), "%s", line);
+ handle_line(tmp);
+}
+
void input_open_fifo(const char *path) {
snprintf(fifo_path, sizeof(fifo_path), "%s", path);
if (mkfifo(fifo_path, 0666) == 0) fifo_created = true;
diff --git a/src/render.h b/src/render.h
index 3b6c7e4..03d6c7c 100644
--- a/src/render.h
+++ b/src/render.h
@@ -8,6 +8,9 @@ void term_restore(void);
void render_frame(GB *gb);
void render_force_full(void); // force a complete repaint next frame
void render_set_hud(int frameskip, bool turbo); // update HUD readout
+// Switch terminal output to sixel graphics (scale = integer pixel zoom, >=1).
+// Pass on=false to use the default Unicode half-block renderer.
+void render_set_sixel(bool on, int scale);
// Poll keyboard + control FIFO, update gb->buttons. False if quit requested.
bool input_poll(GB *gb);
@@ -16,6 +19,11 @@ bool input_poll(GB *gb);
void input_open_fifo(const char *path);
void input_close_fifo(void);
+// Process one line of button-command tokens (the FIFO/socket input vocabulary:
+// a b start select up down left right; name:N; +name / -name; release). Shared
+// by the FIFO and the richer socket control channel.
+void input_handle_command(const char *line);
+
// Returns true once for each pending fast-forward (turbo) toggle keypress.
bool input_take_turbo(void);
// Net frameskip adjustment requested via '[' / ']' since last call.