From 24a015ff047d4b7bb955ba4bc61d5793d7492878 Mon Sep 17 00:00:00 2001 From: gbc dev Date: Wed, 15 Jul 2026 23:44:28 +0200 Subject: 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 --- README.md | 28 +++++++++++++- core.3058597 | Bin 11264000 -> 0 bytes core.3061219 | Bin 11268096 -> 0 bytes core.3063669 | Bin 11268096 -> 0 bytes core.3068673 | Bin 11268096 -> 0 bytes core.3072453 | Bin 11268096 -> 0 bytes core.3073976 | Bin 11268096 -> 0 bytes core.3076382 | Bin 11268096 -> 0 bytes core.3080116 | Bin 11268096 -> 0 bytes core.3081720 | Bin 11268096 -> 0 bytes core.3089603 | Bin 11264000 -> 0 bytes core.3092236 | Bin 11268096 -> 0 bytes core.3094252 | Bin 11268096 -> 0 bytes core.3095917 | Bin 11268096 -> 0 bytes core.3096524 | Bin 11268096 -> 0 bytes core.3096981 | Bin 11268096 -> 0 bytes core.3103352 | Bin 11268096 -> 0 bytes core.3104381 | Bin 11268096 -> 0 bytes core.3107696 | Bin 11268096 -> 0 bytes core.3112782 | Bin 11268096 -> 0 bytes core.3114457 | Bin 11268096 -> 0 bytes core.3115685 | Bin 11268096 -> 0 bytes core.3117279 | Bin 11268096 -> 0 bytes core.3118071 | Bin 11268096 -> 0 bytes core.3309973 | Bin 11264000 -> 0 bytes core.3311624 | Bin 11264000 -> 0 bytes core.3316655 | Bin 11264000 -> 0 bytes core.3321670 | Bin 11264000 -> 0 bytes core.3326764 | Bin 11264000 -> 0 bytes slop_drive.sh | 60 ++++++++++++++++++++++++++++++ src/gb.c | 21 +++++++++-- src/gb.h | 2 + src/main.c | 39 ++++++++++++++++++- tools/__pycache__/gbhud.cpython-314.pyc | Bin 18060 -> 0 bytes tools/__pycache__/gbmovie.cpython-314.pyc | Bin 0 -> 6699 bytes 35 files changed, 145 insertions(+), 5 deletions(-) delete mode 100644 core.3058597 delete mode 100644 core.3061219 delete mode 100644 core.3063669 delete mode 100644 core.3068673 delete mode 100644 core.3072453 delete mode 100644 core.3073976 delete mode 100644 core.3076382 delete mode 100644 core.3080116 delete mode 100644 core.3081720 delete mode 100644 core.3089603 delete mode 100644 core.3092236 delete mode 100644 core.3094252 delete mode 100644 core.3095917 delete mode 100644 core.3096524 delete mode 100644 core.3096981 delete mode 100644 core.3103352 delete mode 100644 core.3104381 delete mode 100644 core.3107696 delete mode 100644 core.3112782 delete mode 100644 core.3114457 delete mode 100644 core.3115685 delete mode 100644 core.3117279 delete mode 100644 core.3118071 delete mode 100644 core.3309973 delete mode 100644 core.3311624 delete mode 100644 core.3316655 delete mode 100644 core.3321670 delete mode 100644 core.3326764 create mode 100644 slop_drive.sh delete mode 100644 tools/__pycache__/gbhud.cpython-314.pyc create mode 100644 tools/__pycache__/gbmovie.cpython-314.pyc 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 Binary files a/core.3058597 and /dev/null differ diff --git a/core.3061219 b/core.3061219 deleted file mode 100644 index 9066b5d..0000000 Binary files a/core.3061219 and /dev/null differ diff --git a/core.3063669 b/core.3063669 deleted file mode 100644 index 63cdb39..0000000 Binary files a/core.3063669 and /dev/null differ diff --git a/core.3068673 b/core.3068673 deleted file mode 100644 index f652f89..0000000 Binary files a/core.3068673 and /dev/null differ diff --git a/core.3072453 b/core.3072453 deleted file mode 100644 index e72709c..0000000 Binary files a/core.3072453 and /dev/null differ diff --git a/core.3073976 b/core.3073976 deleted file mode 100644 index 04ca0da..0000000 Binary files a/core.3073976 and /dev/null differ diff --git a/core.3076382 b/core.3076382 deleted file mode 100644 index 5f9535b..0000000 Binary files a/core.3076382 and /dev/null differ diff --git a/core.3080116 b/core.3080116 deleted file mode 100644 index a569f7e..0000000 Binary files a/core.3080116 and /dev/null differ diff --git a/core.3081720 b/core.3081720 deleted file mode 100644 index 0699dd4..0000000 Binary files a/core.3081720 and /dev/null differ diff --git a/core.3089603 b/core.3089603 deleted file mode 100644 index 493d1e5..0000000 Binary files a/core.3089603 and /dev/null differ diff --git a/core.3092236 b/core.3092236 deleted file mode 100644 index eab02bc..0000000 Binary files a/core.3092236 and /dev/null differ diff --git a/core.3094252 b/core.3094252 deleted file mode 100644 index 8da34bb..0000000 Binary files a/core.3094252 and /dev/null differ diff --git a/core.3095917 b/core.3095917 deleted file mode 100644 index 549133c..0000000 Binary files a/core.3095917 and /dev/null differ diff --git a/core.3096524 b/core.3096524 deleted file mode 100644 index 04786a5..0000000 Binary files a/core.3096524 and /dev/null differ diff --git a/core.3096981 b/core.3096981 deleted file mode 100644 index b3a5c51..0000000 Binary files a/core.3096981 and /dev/null differ diff --git a/core.3103352 b/core.3103352 deleted file mode 100644 index 230f6a2..0000000 Binary files a/core.3103352 and /dev/null differ diff --git a/core.3104381 b/core.3104381 deleted file mode 100644 index 4c71880..0000000 Binary files a/core.3104381 and /dev/null differ diff --git a/core.3107696 b/core.3107696 deleted file mode 100644 index 965f8c9..0000000 Binary files a/core.3107696 and /dev/null differ diff --git a/core.3112782 b/core.3112782 deleted file mode 100644 index 88d4aca..0000000 Binary files a/core.3112782 and /dev/null differ diff --git a/core.3114457 b/core.3114457 deleted file mode 100644 index 1c71b59..0000000 Binary files a/core.3114457 and /dev/null differ diff --git a/core.3115685 b/core.3115685 deleted file mode 100644 index 5d7c329..0000000 Binary files a/core.3115685 and /dev/null differ diff --git a/core.3117279 b/core.3117279 deleted file mode 100644 index 3973114..0000000 Binary files a/core.3117279 and /dev/null differ diff --git a/core.3118071 b/core.3118071 deleted file mode 100644 index f45e7a0..0000000 Binary files a/core.3118071 and /dev/null differ diff --git a/core.3309973 b/core.3309973 deleted file mode 100644 index 1440391..0000000 Binary files a/core.3309973 and /dev/null differ diff --git a/core.3311624 b/core.3311624 deleted file mode 100644 index 6558608..0000000 Binary files a/core.3311624 and /dev/null differ diff --git a/core.3316655 b/core.3316655 deleted file mode 100644 index a52e482..0000000 Binary files a/core.3316655 and /dev/null differ diff --git a/core.3321670 b/core.3321670 deleted file mode 100644 index f5cc42a..0000000 Binary files a/core.3321670 and /dev/null differ diff --git a/core.3326764 b/core.3326764 deleted file mode 100644 index c1a0e8c..0000000 Binary files a/core.3326764 and /dev/null 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 #include +#include 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 #include #include +#include +#include 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 [--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 Binary files a/tools/__pycache__/gbhud.cpython-314.pyc and /dev/null differ diff --git a/tools/__pycache__/gbmovie.cpython-314.pyc b/tools/__pycache__/gbmovie.cpython-314.pyc new file mode 100644 index 0000000..ebe547c Binary files /dev/null and b/tools/__pycache__/gbmovie.cpython-314.pyc differ -- cgit v1.3.1-sl0p