diff options
| author | gbc dev <gbc@localhost> | 2026-07-15 23:44:28 +0200 |
|---|---|---|
| committer | gbc dev <gbc@localhost> | 2026-07-15 23:44:28 +0200 |
| commit | 24a015ff047d4b7bb955ba4bc61d5793d7492878 (patch) | |
| tree | ebaf323a5c530400c81f9ce13db828e9bf6b35e1 | |
| parent | control: video frame capture + deterministic input-movie replay (diff) | |
| download | sl0pboy-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.md | 28 | ||||
| -rw-r--r-- | core.3058597 | bin | 11264000 -> 0 bytes | |||
| -rw-r--r-- | core.3061219 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3063669 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3068673 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3072453 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3073976 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3076382 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3080116 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3081720 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3089603 | bin | 11264000 -> 0 bytes | |||
| -rw-r--r-- | core.3092236 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3094252 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3095917 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3096524 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3096981 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3103352 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3104381 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3107696 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3112782 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3114457 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3115685 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3117279 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3118071 | bin | 11268096 -> 0 bytes | |||
| -rw-r--r-- | core.3309973 | bin | 11264000 -> 0 bytes | |||
| -rw-r--r-- | core.3311624 | bin | 11264000 -> 0 bytes | |||
| -rw-r--r-- | core.3316655 | bin | 11264000 -> 0 bytes | |||
| -rw-r--r-- | core.3321670 | bin | 11264000 -> 0 bytes | |||
| -rw-r--r-- | core.3326764 | bin | 11264000 -> 0 bytes | |||
| -rw-r--r-- | slop_drive.sh | 60 | ||||
| -rw-r--r-- | src/gb.c | 21 | ||||
| -rw-r--r-- | src/gb.h | 2 | ||||
| -rw-r--r-- | src/main.c | 39 | ||||
| -rw-r--r-- | tools/__pycache__/gbhud.cpython-314.pyc | bin | 18060 -> 0 bytes | |||
| -rw-r--r-- | tools/__pycache__/gbmovie.cpython-314.pyc | bin | 0 -> 6699 bytes |
35 files changed, 145 insertions, 5 deletions
@@ -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 Binary files differdeleted file mode 100644 index 8e1fd6b..0000000 --- a/core.3058597 +++ /dev/null diff --git a/core.3061219 b/core.3061219 Binary files differdeleted file mode 100644 index 9066b5d..0000000 --- a/core.3061219 +++ /dev/null diff --git a/core.3063669 b/core.3063669 Binary files differdeleted file mode 100644 index 63cdb39..0000000 --- a/core.3063669 +++ /dev/null diff --git a/core.3068673 b/core.3068673 Binary files differdeleted file mode 100644 index f652f89..0000000 --- a/core.3068673 +++ /dev/null diff --git a/core.3072453 b/core.3072453 Binary files differdeleted file mode 100644 index e72709c..0000000 --- a/core.3072453 +++ /dev/null diff --git a/core.3073976 b/core.3073976 Binary files differdeleted file mode 100644 index 04ca0da..0000000 --- a/core.3073976 +++ /dev/null diff --git a/core.3076382 b/core.3076382 Binary files differdeleted file mode 100644 index 5f9535b..0000000 --- a/core.3076382 +++ /dev/null diff --git a/core.3080116 b/core.3080116 Binary files differdeleted file mode 100644 index a569f7e..0000000 --- a/core.3080116 +++ /dev/null diff --git a/core.3081720 b/core.3081720 Binary files differdeleted file mode 100644 index 0699dd4..0000000 --- a/core.3081720 +++ /dev/null diff --git a/core.3089603 b/core.3089603 Binary files differdeleted file mode 100644 index 493d1e5..0000000 --- a/core.3089603 +++ /dev/null diff --git a/core.3092236 b/core.3092236 Binary files differdeleted file mode 100644 index eab02bc..0000000 --- a/core.3092236 +++ /dev/null diff --git a/core.3094252 b/core.3094252 Binary files differdeleted file mode 100644 index 8da34bb..0000000 --- a/core.3094252 +++ /dev/null diff --git a/core.3095917 b/core.3095917 Binary files differdeleted file mode 100644 index 549133c..0000000 --- a/core.3095917 +++ /dev/null diff --git a/core.3096524 b/core.3096524 Binary files differdeleted file mode 100644 index 04786a5..0000000 --- a/core.3096524 +++ /dev/null diff --git a/core.3096981 b/core.3096981 Binary files differdeleted file mode 100644 index b3a5c51..0000000 --- a/core.3096981 +++ /dev/null diff --git a/core.3103352 b/core.3103352 Binary files differdeleted file mode 100644 index 230f6a2..0000000 --- a/core.3103352 +++ /dev/null diff --git a/core.3104381 b/core.3104381 Binary files differdeleted file mode 100644 index 4c71880..0000000 --- a/core.3104381 +++ /dev/null diff --git a/core.3107696 b/core.3107696 Binary files differdeleted file mode 100644 index 965f8c9..0000000 --- a/core.3107696 +++ /dev/null diff --git a/core.3112782 b/core.3112782 Binary files differdeleted file mode 100644 index 88d4aca..0000000 --- a/core.3112782 +++ /dev/null diff --git a/core.3114457 b/core.3114457 Binary files differdeleted file mode 100644 index 1c71b59..0000000 --- a/core.3114457 +++ /dev/null diff --git a/core.3115685 b/core.3115685 Binary files differdeleted file mode 100644 index 5d7c329..0000000 --- a/core.3115685 +++ /dev/null diff --git a/core.3117279 b/core.3117279 Binary files differdeleted file mode 100644 index 3973114..0000000 --- a/core.3117279 +++ /dev/null diff --git a/core.3118071 b/core.3118071 Binary files differdeleted file mode 100644 index f45e7a0..0000000 --- a/core.3118071 +++ /dev/null diff --git a/core.3309973 b/core.3309973 Binary files differdeleted file mode 100644 index 1440391..0000000 --- a/core.3309973 +++ /dev/null diff --git a/core.3311624 b/core.3311624 Binary files differdeleted file mode 100644 index 6558608..0000000 --- a/core.3311624 +++ /dev/null diff --git a/core.3316655 b/core.3316655 Binary files differdeleted file mode 100644 index a52e482..0000000 --- a/core.3316655 +++ /dev/null diff --git a/core.3321670 b/core.3321670 Binary files differdeleted file mode 100644 index f5cc42a..0000000 --- a/core.3321670 +++ /dev/null diff --git a/core.3326764 b/core.3326764 Binary files differdeleted file mode 100644 index c1a0e8c..0000000 --- a/core.3326764 +++ /dev/null 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; } @@ -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; @@ -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); @@ -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 Binary files differdeleted file mode 100644 index 3097503..0000000 --- a/tools/__pycache__/gbhud.cpython-314.pyc +++ /dev/null diff --git a/tools/__pycache__/gbmovie.cpython-314.pyc b/tools/__pycache__/gbmovie.cpython-314.pyc Binary files differnew file mode 100644 index 0000000..ebe547c --- /dev/null +++ b/tools/__pycache__/gbmovie.cpython-314.pyc |
