aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-15 23:44:28 +0200
committergbc dev <gbc@localhost>2026-07-15 23:44:28 +0200
commit24a015ff047d4b7bb955ba4bc61d5793d7492878 (patch)
treeebaf323a5c530400c81f9ce13db828e9bf6b35e1
parentcontrol: video frame capture + deterministic input-movie replay (diff)
downloadsl0pboy-24a015ff047d4b7bb955ba4bc61d5793d7492878.tar.gz
sl0pboy-24a015ff047d4b7bb955ba4bc61d5793d7492878.tar.xz
sl0pboy-24a015ff047d4b7bb955ba4bc61d5793d7492878.zip
serial: headless mode tying serial port to stdio for OS debugging
- --headless: no rendering/TUI; serial TX -> stdout, RX <- stdin (nonblocking) - generalize serial: serial_out_fd/serial_in_fd on the GB; internal-clock transfer now shifts in a real RX byte (from stdin) instead of always 0xFF - paced to native speed by default, --uncapped for turbo; pipes/scripts cleanly - README: document headless serial console
-rw-r--r--README.md28
-rw-r--r--core.3058597bin11264000 -> 0 bytes
-rw-r--r--core.3061219bin11268096 -> 0 bytes
-rw-r--r--core.3063669bin11268096 -> 0 bytes
-rw-r--r--core.3068673bin11268096 -> 0 bytes
-rw-r--r--core.3072453bin11268096 -> 0 bytes
-rw-r--r--core.3073976bin11268096 -> 0 bytes
-rw-r--r--core.3076382bin11268096 -> 0 bytes
-rw-r--r--core.3080116bin11268096 -> 0 bytes
-rw-r--r--core.3081720bin11268096 -> 0 bytes
-rw-r--r--core.3089603bin11264000 -> 0 bytes
-rw-r--r--core.3092236bin11268096 -> 0 bytes
-rw-r--r--core.3094252bin11268096 -> 0 bytes
-rw-r--r--core.3095917bin11268096 -> 0 bytes
-rw-r--r--core.3096524bin11268096 -> 0 bytes
-rw-r--r--core.3096981bin11268096 -> 0 bytes
-rw-r--r--core.3103352bin11268096 -> 0 bytes
-rw-r--r--core.3104381bin11268096 -> 0 bytes
-rw-r--r--core.3107696bin11268096 -> 0 bytes
-rw-r--r--core.3112782bin11268096 -> 0 bytes
-rw-r--r--core.3114457bin11268096 -> 0 bytes
-rw-r--r--core.3115685bin11268096 -> 0 bytes
-rw-r--r--core.3117279bin11268096 -> 0 bytes
-rw-r--r--core.3118071bin11268096 -> 0 bytes
-rw-r--r--core.3309973bin11264000 -> 0 bytes
-rw-r--r--core.3311624bin11264000 -> 0 bytes
-rw-r--r--core.3316655bin11264000 -> 0 bytes
-rw-r--r--core.3321670bin11264000 -> 0 bytes
-rw-r--r--core.3326764bin11264000 -> 0 bytes
-rw-r--r--slop_drive.sh60
-rw-r--r--src/gb.c21
-rw-r--r--src/gb.h2
-rw-r--r--src/main.c39
-rw-r--r--tools/__pycache__/gbhud.cpython-314.pycbin18060 -> 0 bytes
-rw-r--r--tools/__pycache__/gbmovie.cpython-314.pycbin0 -> 6699 bytes
35 files changed, 145 insertions, 5 deletions
diff --git a/README.md b/README.md
index b93446d..e03aedc 100644
--- a/README.md
+++ b/README.md
@@ -18,12 +18,38 @@ over 2 s (~108×). Terminal resize (`SIGWINCH`) triggers a full repaint.
```sh
make
./build/gbc path/to/rom.gb # interactive
-./build/gbc rom.gb --test 30 # headless: run 30s, log serial (test roms)
+./build/gbc rom.gb --test 30 # run 30s, log serial to stderr (test roms)
./build/gbc rom.gb --shot 60 out.ppm # run 60 frames, dump a PPM screenshot
./build/gbc rom.gb --sixel 3 # sixel graphics output at 3x zoom
./build/gbc rom.gb --sock # open a control/debug socket (/tmp/gbc.sock)
+./build/gbc rom.gb --headless # no video; serial <-> stdio (OS console)
```
+### Headless serial console (`--headless`)
+
+For bring-up and debugging of an OS/monitor running on the emulated Game Boy,
+`--headless` disables all rendering and the terminal UI and instead wires the
+**serial port straight to stdio**:
+
+- transmitted bytes (the GB writing `SB`/`SC` with the internal clock) go to **stdout**
+- **stdin** is fed back as the received byte on each transfer (non-blocking;
+ `0xFF` open-bus when nothing is waiting)
+
+Emulation is paced to native speed by default; add `--uncapped` to run flat out.
+Because it's plain stdio it pipes and scripts cleanly:
+
+```sh
+./build/gbc os.gb --headless # interactive byte console
+./build/gbc os.gb --headless --uncapped # as fast as possible
+printf 'ls\n' | ./build/gbc os.gb --headless # feed input, capture output
+./build/gbc os.gb --headless | tee session.log
+```
+
+The serial console model matches what a GB program expects: write the byte to
+`SB` (`$FF01`), then kick a transfer with `SC = $81` (`$FF02`, start + internal
+clock). Output appears immediately; the transfer-complete serial interrupt still
+fires. (`--test` keeps its old behavior, logging serial to **stderr**.)
+
Requires a truecolor-capable terminal (`COLORTERM=truecolor`) at least
160 columns wide.
diff --git a/core.3058597 b/core.3058597
deleted file mode 100644
index 8e1fd6b..0000000
--- a/core.3058597
+++ /dev/null
Binary files differ
diff --git a/core.3061219 b/core.3061219
deleted file mode 100644
index 9066b5d..0000000
--- a/core.3061219
+++ /dev/null
Binary files differ
diff --git a/core.3063669 b/core.3063669
deleted file mode 100644
index 63cdb39..0000000
--- a/core.3063669
+++ /dev/null
Binary files differ
diff --git a/core.3068673 b/core.3068673
deleted file mode 100644
index f652f89..0000000
--- a/core.3068673
+++ /dev/null
Binary files differ
diff --git a/core.3072453 b/core.3072453
deleted file mode 100644
index e72709c..0000000
--- a/core.3072453
+++ /dev/null
Binary files differ
diff --git a/core.3073976 b/core.3073976
deleted file mode 100644
index 04ca0da..0000000
--- a/core.3073976
+++ /dev/null
Binary files differ
diff --git a/core.3076382 b/core.3076382
deleted file mode 100644
index 5f9535b..0000000
--- a/core.3076382
+++ /dev/null
Binary files differ
diff --git a/core.3080116 b/core.3080116
deleted file mode 100644
index a569f7e..0000000
--- a/core.3080116
+++ /dev/null
Binary files differ
diff --git a/core.3081720 b/core.3081720
deleted file mode 100644
index 0699dd4..0000000
--- a/core.3081720
+++ /dev/null
Binary files differ
diff --git a/core.3089603 b/core.3089603
deleted file mode 100644
index 493d1e5..0000000
--- a/core.3089603
+++ /dev/null
Binary files differ
diff --git a/core.3092236 b/core.3092236
deleted file mode 100644
index eab02bc..0000000
--- a/core.3092236
+++ /dev/null
Binary files differ
diff --git a/core.3094252 b/core.3094252
deleted file mode 100644
index 8da34bb..0000000
--- a/core.3094252
+++ /dev/null
Binary files differ
diff --git a/core.3095917 b/core.3095917
deleted file mode 100644
index 549133c..0000000
--- a/core.3095917
+++ /dev/null
Binary files differ
diff --git a/core.3096524 b/core.3096524
deleted file mode 100644
index 04786a5..0000000
--- a/core.3096524
+++ /dev/null
Binary files differ
diff --git a/core.3096981 b/core.3096981
deleted file mode 100644
index b3a5c51..0000000
--- a/core.3096981
+++ /dev/null
Binary files differ
diff --git a/core.3103352 b/core.3103352
deleted file mode 100644
index 230f6a2..0000000
--- a/core.3103352
+++ /dev/null
Binary files differ
diff --git a/core.3104381 b/core.3104381
deleted file mode 100644
index 4c71880..0000000
--- a/core.3104381
+++ /dev/null
Binary files differ
diff --git a/core.3107696 b/core.3107696
deleted file mode 100644
index 965f8c9..0000000
--- a/core.3107696
+++ /dev/null
Binary files differ
diff --git a/core.3112782 b/core.3112782
deleted file mode 100644
index 88d4aca..0000000
--- a/core.3112782
+++ /dev/null
Binary files differ
diff --git a/core.3114457 b/core.3114457
deleted file mode 100644
index 1c71b59..0000000
--- a/core.3114457
+++ /dev/null
Binary files differ
diff --git a/core.3115685 b/core.3115685
deleted file mode 100644
index 5d7c329..0000000
--- a/core.3115685
+++ /dev/null
Binary files differ
diff --git a/core.3117279 b/core.3117279
deleted file mode 100644
index 3973114..0000000
--- a/core.3117279
+++ /dev/null
Binary files differ
diff --git a/core.3118071 b/core.3118071
deleted file mode 100644
index f45e7a0..0000000
--- a/core.3118071
+++ /dev/null
Binary files differ
diff --git a/core.3309973 b/core.3309973
deleted file mode 100644
index 1440391..0000000
--- a/core.3309973
+++ /dev/null
Binary files differ
diff --git a/core.3311624 b/core.3311624
deleted file mode 100644
index 6558608..0000000
--- a/core.3311624
+++ /dev/null
Binary files differ
diff --git a/core.3316655 b/core.3316655
deleted file mode 100644
index a52e482..0000000
--- a/core.3316655
+++ /dev/null
Binary files differ
diff --git a/core.3321670 b/core.3321670
deleted file mode 100644
index f5cc42a..0000000
--- a/core.3321670
+++ /dev/null
Binary files differ
diff --git a/core.3326764 b/core.3326764
deleted file mode 100644
index c1a0e8c..0000000
--- a/core.3326764
+++ /dev/null
Binary files differ
diff --git a/slop_drive.sh b/slop_drive.sh
new file mode 100644
index 0000000..3ca019d
--- /dev/null
+++ b/slop_drive.sh
@@ -0,0 +1,60 @@
+#!/usr/bin/env bash
+# Robust driver for the SL0P menu: verify each state transition via WRAM
+# sidechannels before sending more input, instead of racing the menu poll.
+# sidechannels:
+# 0xCFCA wUpdateSpritesEnabled : 01 = overworld, ff = SL0P menu / EXPLORE / saver
+# 0xCC26 wCurrentMenuItem : menu cursor index (valid while a menu is open)
+# 0xD35D wCurMap 0xD360 wYCoord 0xD361 wXCoord
+SOCK=$(./gbctl list | grep -o 'sock=[^ ]*' | cut -d= -f2)
+rd(){ ./gbctl read "$1" "${2:-1}" | cut -d' ' -f5- | tr -d ' '; }
+# reliable sidechannels: player sprite present in OAM => overworld; the "SL0P
+# MENU" title glyph at wTileMap(5,0)=0xC3A5 == 0x92 ('S') => the SL0P menu is up.
+# (wUpdateSpritesEnabled is NOT reliable: the top menu leaves it at 01.)
+in_ow(){ [ "$(rd 0xC300 1)" != "00" ]; }
+menu_up(){ [ "$(rd 0xC3A5 1)" = "92" ]; }
+cursor(){ printf '%d' "0x$(rd 0xCC26 1)"; }
+
+# back out to the overworld (press B until the player sprite is drawn again)
+reset_ow(){
+ for _ in $(seq 1 10); do
+ in_ow && return 0
+ ./gbctl press b:6 >/dev/null; sleep 0.5
+ done
+ echo "reset_ow: FAILED" >&2; return 1
+}
+
+# confirm we're actually in the overworld and idle (coords stable across a beat)
+wait_ow(){
+ for _ in $(seq 1 40); do
+ if in_ow; then
+ a=$(rd 0xD360); sleep 0.25; b=$(rd 0xD360)
+ [ "$a" = "$b" ] && { echo "overworld ready: map=$(rd 0xD35D) Y=$(rd 0xD360) X=$(rd 0xD361)"; return 0; }
+ fi
+ sleep 0.25
+ done
+ echo "wait_ow: FAILED" >&2; return 1
+}
+
+# open the SL0P menu (hold SELECT, verify via the title glyph, retry if not)
+open_menu(){
+ for _ in $(seq 1 12); do
+ menu_up && { echo "menu open"; return 0; }
+ ./gbctl press select:6 >/dev/null; sleep 0.6
+ done
+ echo "open_menu: FAILED" >&2; return 1
+}
+
+# move the menu cursor to index $1, one press at a time, verifying each step
+menu_to(){
+ local target=$1 c
+ for _ in $(seq 1 50); do
+ c=$(cursor)
+ [ "$c" -eq "$target" ] && { echo "cursor=$target"; return 0; }
+ if [ "$c" -lt "$target" ]; then ./gbctl press down:4 >/dev/null; else ./gbctl press up:4 >/dev/null; fi
+ sleep 0.4
+ done
+ echo "menu_to: FAILED (cursor=$c want $target)" >&2; return 1
+}
+
+# press A on the current menu item
+menu_select(){ ./gbctl press a:5 >/dev/null; sleep 0.9; }
diff --git a/src/gb.c b/src/gb.c
index afdcb89..05429fd 100644
--- a/src/gb.c
+++ b/src/gb.c
@@ -4,6 +4,7 @@
#include "cpu.h"
#include <string.h>
#include <stdio.h>
+#include <unistd.h>
void gb_request_interrupt(GB *gb, u8 flag) {
gb->iff |= flag;
@@ -116,9 +117,21 @@ void gb_write(GB *gb, u16 addr, u8 val) {
if (addr == 0xFF02) {
gb->sc = val;
if ((val & 0x81) == 0x81) {
- // transfer with internal clock: log & complete instantly
- if (gb->serial_log) { fputc(gb->sb, stderr); fflush(stderr); }
- gb->sb = 0xFF;
+ // transfer with internal clock (GB is master): shift out gb->sb,
+ // shift in a byte from the peer. Completes instantly.
+ u8 outb = gb->sb;
+ if (gb->serial_log) { fputc(outb, stderr); fflush(stderr); }
+ if (gb->serial_out_fd >= 0) {
+ unsigned char c = outb;
+ (void)!write(gb->serial_out_fd, &c, 1);
+ }
+ // received byte: from serial_in_fd if available, else open-bus 0xFF
+ u8 inb = 0xFF;
+ if (gb->serial_in_fd >= 0) {
+ unsigned char c;
+ if (read(gb->serial_in_fd, &c, 1) == 1) inb = c;
+ }
+ gb->sb = inb;
gb->sc &= ~0x80;
gb_request_interrupt(gb, INT_SERIAL);
}
@@ -204,6 +217,8 @@ void gb_reset(GB *gb) {
c->af = 0x01B0; c->bc = 0x0013; c->de = 0x00D8; c->hl = 0x014D;
}
c->ime = false;
+ gb->serial_out_fd = -1;
+ gb->serial_in_fd = -1;
gb->wram_bank = 1;
gb->joyp_sel = 0x30;
gb->iff = 0xE1;
diff --git a/src/gb.h b/src/gb.h
index 7439615..7218648 100644
--- a/src/gb.h
+++ b/src/gb.h
@@ -114,6 +114,8 @@ struct GB {
u64 cycles; // total T-cycles elapsed
bool serial_log; // print serial output to stderr (blargg tests)
+ int serial_out_fd; // if >=0, transmitted serial bytes written here (stdout)
+ int serial_in_fd; // if >=0, received serial bytes read here (stdin), else 0xFF
};
void gb_init(GB *gb);
diff --git a/src/main.c b/src/main.c
index ecd818e..f0709c2 100644
--- a/src/main.c
+++ b/src/main.c
@@ -7,6 +7,8 @@
#include <string.h>
#include <time.h>
#include <signal.h>
+#include <fcntl.h>
+#include <unistd.h>
static volatile sig_atomic_t running = 1;
static void on_sig(int s) { (void)s; running = 0; }
@@ -29,7 +31,7 @@ int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "usage: %s <rom> [--test seconds] [--sixel [scale]] "
"[--frameskip n] [--fps n] [--uncapped] [--fifo [path]] "
- "[--sock [path]]\n",
+ "[--sock [path]] [--headless]\n",
argv[0]);
return 1;
}
@@ -44,6 +46,7 @@ int main(int argc, char **argv) {
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
+ bool headless = false; // no video; serial <-> stdio (OS debug console)
for (int i = 2; i < argc; i++) {
if (!strcmp(argv[i], "--test")) {
test = true;
@@ -66,6 +69,8 @@ int main(int argc, char **argv) {
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;
+ } else if (!strcmp(argv[i], "--headless")) {
+ headless = true;
}
}
if (frameskip < 0) frameskip = 0;
@@ -84,6 +89,38 @@ int main(int argc, char **argv) {
return 0;
}
+ // Headless: no rendering, no terminal UI. Tie the serial port to stdio so
+ // an OS on the GB gets a plain byte console (TX -> stdout, RX <- stdin).
+ // Paced to native speed unless --uncapped. Piping works for scripted tests:
+ // printf 'ls\n' | gbc rom.gb --headless
+ if (headless) {
+ gb->serial_out_fd = STDOUT_FILENO;
+ gb->serial_in_fd = STDIN_FILENO;
+ int fl = fcntl(STDIN_FILENO, F_GETFL, 0);
+ if (fl != -1) fcntl(STDIN_FILENO, F_SETFL, fl | O_NONBLOCK);
+ signal(SIGINT, on_sig);
+ signal(SIGTERM, on_sig);
+ bool uncapped = (target_fps <= 0);
+ double start_t = now_sec();
+ u64 start_cyc = gb->cycles;
+ const double HZ = 4194304.0;
+ while (running) {
+ run_frame(gb); // advance ~a frame's worth of cycles
+ if (!uncapped) {
+ double target = start_t + (gb->cycles - start_cyc) / HZ;
+ double sleep = target - now_sec();
+ if (sleep > 0) {
+ struct timespec ts = { (time_t)sleep,
+ (long)((sleep - (time_t)sleep) * 1e9) };
+ nanosleep(&ts, NULL);
+ } else if (sleep < -0.5) {
+ start_t = now_sec(); start_cyc = gb->cycles; // resync
+ }
+ }
+ }
+ cart_free(&gb->cart); free(gb); return 0;
+ }
+
if (shot_frames >= 0) {
for (int f = 0; f < shot_frames; f++) run_frame(gb);
FILE *pf = fopen(shot_path, "wb");
diff --git a/tools/__pycache__/gbhud.cpython-314.pyc b/tools/__pycache__/gbhud.cpython-314.pyc
deleted file mode 100644
index 3097503..0000000
--- a/tools/__pycache__/gbhud.cpython-314.pyc
+++ /dev/null
Binary files differ
diff --git a/tools/__pycache__/gbmovie.cpython-314.pyc b/tools/__pycache__/gbmovie.cpython-314.pyc
new file mode 100644
index 0000000..ebe547c
--- /dev/null
+++ b/tools/__pycache__/gbmovie.cpython-314.pyc
Binary files differ