aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgbc dev <gbc@localhost>2026-07-17 01:11:07 +0200
committergbc dev <gbc@localhost>2026-07-17 01:11:07 +0200
commit4aa4df2687cb350fb4c7717bd9298b70e9b0dab6 (patch)
treea08b8cc8fbcd9426dbc524d4ef891b9194fd3c74
parentremove slop_drive.sh (moved to pokeyellow) (diff)
downloadsl0pboy-4aa4df2687cb350fb4c7717bd9298b70e9b0dab6.tar.gz
sl0pboy-4aa4df2687cb350fb4c7717bd9298b70e9b0dab6.tar.xz
sl0pboy-4aa4df2687cb350fb4c7717bd9298b70e9b0dab6.zip
readme: trim the fluff, loosen the tone
-rw-r--r--README.md358
1 files changed, 166 insertions, 192 deletions
diff --git a/README.md b/README.md
index 8ce3002..1bb1369 100644
--- a/README.md
+++ b/README.md
@@ -1,155 +1,141 @@
-# sl0pboy — a Game Boy Color emulator in C, rendered to the terminal
+# sl0pboy — a Game Boy Color emulator that lives in your terminal
-A from-scratch Game Boy / Game Boy Color emulator written in plain C11. It
-renders graphics directly to the terminal using 24-bit (truecolor) ANSI escape
-codes and the Unicode upper-half-block trick (`▀`): each character cell packs
-two vertical pixels (foreground = top pixel, background = bottom pixel),
-giving a 160×144 GB screen in 160×72 terminal cells.
+A from-scratch GB / GBC emulator in plain C11 that paints straight to the
+terminal — no window, no GPU, just truecolor ANSI and the upper-half-block
+trick (`▀`): every character cell = two stacked pixels (fg on top, bg on
+bottom). That squeezes the 160×144 screen into 160×72 cells.
-To keep terminal load low, output is minimized two ways: SGR color escapes
-are only emitted when the fg/bg changes, and **inter-frame diffing** redraws
-only the cells that changed since the previous frame (jumping over unchanged
-runs with `ESC[row;colH`). A fully static screen writes nothing after the
-first frame; on the acid2 screens this cut output from ~7.1 MB to ~65 KB
-over 2 s (~108×). Terminal resize (`SIGWINCH`) triggers a full repaint.
+To keep your terminal from crying, it only emits SGR escapes when a color
+changes, and it **diffs frames** — only redrawing cells that moved (skipping
+unchanged runs with `ESC[row;colH`). A static screen writes *nothing* after
+frame one; on acid2 that dropped output from ~7.1 MB to ~65 KB over 2 s (≈108×).
+Resize the terminal and `SIGWINCH` triggers a full repaint.
## Build & run
```sh
make
-./build/sl0pboy path/to/rom.gb # interactive (ROM is the final argument)
-./build/sl0pboy --test 30 rom.gb # run 30s, log serial to stderr (test roms)
-./build/sl0pboy --shot 60 out.ppm rom.gb # run 60 frames, dump a PPM screenshot
-./build/sl0pboy --sixel 3 rom.gb # sixel graphics output at 3x zoom
-./build/sl0pboy --sixel 3 --chrome rom.gb # ...wrapped in a Game Boy body/frame
-./build/sl0pboy --bios rom.gb # boot through the BIOS before the game
-./build/sl0pboy --sock rom.gb # open a control/debug socket (/tmp/sl0pboy.sock)
-./build/sl0pboy --headless rom.gb # no video; serial <-> stdio (OS console)
-./build/sl0pboy --help # full option list
+./build/sl0pboy path/to/rom.gb # just play it (ROM goes last)
+./build/sl0pboy --test 30 rom.gb # run 30s, serial -> stderr (test roms)
+./build/sl0pboy --shot 60 out.ppm rom.gb # 60 frames in, dump a PPM
+./build/sl0pboy --sixel 3 rom.gb # real pixels via sixel, 3x zoom
+./build/sl0pboy --sixel 3 --chrome rom.gb # ...in a lil' handheld body
+./build/sl0pboy --bios rom.gb # boot through the real BIOS first
+./build/sl0pboy --sock rom.gb # control/debug socket (/tmp/sl0pboy.sock)
+./build/sl0pboy --headless rom.gb # no video, serial <-> stdio
+./build/sl0pboy --help # the whole menu
```
-### Boot ROM / BIOS (`--bios`)
+Needs a truecolor terminal (`COLORTERM=truecolor`) at least 160 columns wide.
-By default the emulator skips the boot ROM and starts the cartridge directly
-with the documented post-boot register state. Pass `--bios [path]` to instead
-run a real boot ROM first, so you get the authentic power-on animation (the
-Nintendo logo drop, and on a CGB the color splash + chime) before control is
-handed to the game:
+## Boot ROM / BIOS (`--bios`)
+
+By default we skip the boot ROM and drop you into the cartridge with the
+documented post-boot register state. Want the real power-on show — the Nintendo
+logo drop, the CGB color splash + chime? Pass `--bios [path]`:
```sh
-./build/sl0pboy --bios roms/pky.gbc # defaults to bios/gbc_bios.bin
+./build/sl0pboy --bios roms/pky.gbc # defaults to bios/gbc_bios.bin
./build/sl0pboy --bios path/to/boot.bin roms/pky.gbc
-./build/sl0pboy --bios --sixel 3 --chrome roms/pky.gbc # boot inside the shell
+./build/sl0pboy --bios --sixel 3 --chrome roms/pky.gbc # boot inside the shell
```
-The boot ROM is mapped over low memory (with the CGB header hole at
-`0x0100-0x01FF` where the cartridge shows through) and unmaps itself when the
-ROM writes `0xFF50`, exactly like hardware. A 256-byte image boots as DMG; a
-2304-byte image (like the bundled `bios/gbc_bios.bin`) boots as CGB — which
-also colorizes plain DMG games via the boot ROM's compatibility palette.
+It maps the boot ROM over low memory (with the CGB header hole at
+`0x0100-0x01FF` where the cart peeks through) and unmaps itself when the ROM
+pokes `0xFF50`, just like hardware. 256 bytes → boots as DMG; 2304 bytes (like
+the bundled `bios/gbc_bios.bin`) → boots as CGB, which also colorizes plain DMG
+games via the boot ROM's compatibility palette.
-### Headless serial console (`--headless`)
+## 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**:
+Bringing up an OS/monitor on the emulated Game Boy? `--headless` kills all
+rendering and 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)
+- bytes the GB transmits (`SB`/`SC` w/ internal clock) → **stdout**
+- **stdin** feeds back as the received byte each transfer (non-blocking; `0xFF`
+ open-bus when nothing's 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:
+Paced to native speed by default; `--uncapped` runs flat out. It's just stdio,
+so it pipes clean:
```sh
./build/sl0pboy --headless os.gb # interactive byte console
-./build/sl0pboy --headless --uncapped os.gb # as fast as possible
-printf 'ls\n' | ./build/sl0pboy --headless os.gb # feed input, capture output
+./build/sl0pboy --headless --uncapped os.gb # gotta go fast
+printf 'ls\n' | ./build/sl0pboy --headless os.gb # feed input, grab output
./build/sl0pboy --headless os.gb | 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.
+The model is what a GB program expects: write byte → `SB` (`$FF01`), kick with
+`SC = $81` (`$FF02`, start + internal clock). Output shows up instantly and the
+transfer-complete interrupt still fires. (`--test` still logs serial to
+**stderr** like before.)
-### Sixel output
+## Sixel output
-On terminals that support **sixel** graphics (xterm `-ti vt340`, foot, WezTerm,
-mlterm, Contour, recent iTerm2, etc.) pass `--sixel [scale]` to render true
-pixels instead of half-blocks:
+On sixel-capable terminals (xterm `-ti vt340`, foot, WezTerm, mlterm, Contour,
+recent iTerm2…) `--sixel [scale]` draws real pixels instead of half-blocks:
```sh
-./build/sl0pboy --sixel rom.gb # default 2x zoom (320×288 pixels)
-./build/sl0pboy --sixel 4 rom.gb # 4x zoom
+./build/sl0pboy --sixel rom.gb # 2x zoom (320×288)
+./build/sl0pboy --sixel 4 rom.gb # 4x zoom
```
-Each frame builds a per-frame palette from the framebuffer (≤256 entries — 4
-shades on DMG, a small set on CGB; colors are quantized more coarsely only if a
-frame ever overflows), then emits standard sixel bands with per-column
-run-length encoding. `scale` is an integer pixel zoom (1–6). Sixel frames are
-sent in full (no inter-frame diffing), so combine with `--frameskip` on slower
-terminals.
+Each frame builds its own ≤256-color palette from the framebuffer (4 shades on
+DMG, a small set on CGB; quantized coarser only if a frame overflows) and emits
+standard sixel bands with per-column RLE. `scale` is 1–6. Sixel frames go out in
+full (no diffing) — pair with `--frameskip` on slow terminals.
-#### Game Boy chrome
+### Game Boy chrome
-Add `--chrome` (sixel only) to draw a DMG-style **sl0pboy** handheld around the
-LCD — screen bezel with accent stripe, power LED, silk-screened branding
-(`sl0pboy` logo, `DOT MATRIX WITH STEREO SOUND`, button labels), a D-pad, A/B
-buttons, Start/Select and a speaker grille. The controls light up while the
-matching button is held, so it doubles as an on-screen input display:
+`--chrome` (sixel only) wraps the LCD in a DMG-style **sl0pboy** handheld:
+bezel + accent stripe, power LED, silk-screened branding (`sl0pboy` logo,
+`DOT MATRIX WITH STEREO SOUND`, button labels), D-pad, A/B, Start/Select,
+speaker grille. Buttons light up while held, so it doubles as an input display:
```sh
./build/sl0pboy --sixel 3 --chrome rom.gb
```
-The shell is drawn with flat colors so it adds only a handful of palette
-entries. It composites into a larger canvas (240×342 px before scaling), so a
-given `scale` produces a proportionally bigger image than the bare LCD.
+Flat colors → only a handful of extra palette entries. It composites onto a
+bigger canvas (240×342 px pre-scale), so a given `scale` is proportionally
+larger than the bare LCD.
-#### LCD palette (`--palette`)
+### LCD palette (`--palette`)
-By default the LCD shows the game's true colors (the greenish DMG shades, or
-full CGB color). Pass `--palette NAME` to remap every LCD pixel to a 4-shade
-palette by luminance for the classic monochrome-LCD look — this even greens
-out full-color CGB games for that retro vibe:
+By default the LCD shows true colors (greenish DMG shades or full CGB). Want the
+retro monochrome-LCD look? `--palette NAME` remaps every LCD pixel to a 4-shade
+ramp by luminance — this even greens out full-color CGB games:
```sh
-./build/sl0pboy --sixel 3 --chrome --palette dmg rom.gb # classic DMG-01 pea green
+./build/sl0pboy --sixel 3 --chrome --palette dmg rom.gb # classic pea green
./build/sl0pboy --sixel 3 --green rom.gb # alias for --palette dmg
./build/sl0pboy --palette gray rom.gb # GB-Pocket grayscale
```
-Names: `dmg`/`green` (the iconic `#9bbc0f` pea-green ramp) and `pocket`/`gray`
-(grayscale). The recolor applies to every render path — half-block, sixel and
-chrome — and only touches the LCD, not the drawn shell.
+Names: `dmg`/`green` (the iconic `#9bbc0f` pea-green ramp) and `pocket`/`gray`.
+Applies to every render path — half-block, sixel, chrome — and only touches the
+LCD, never the shell.
-### Speed & frame skipping
+## Speed & frame skipping
-The emulator always advances every Game Boy frame at the correct rate, so
-games run at their intended speed. Because writing a frame to the terminal is
-by far the most expensive step, drawing can be *decoupled* from emulation via
-frame skipping — the machine keeps running in real time while the terminal is
-refreshed less often:
+Games always run at the correct rate. But writing a frame to the terminal is the
+expensive part, so drawing is *decoupled* from emulation — the machine keeps
+ticking in real time while the terminal refreshes less often:
```sh
-./build/sl0pboy --frameskip 2 rom.gb # emulate 60 fps, draw every 3rd frame (20 fps)
-./build/sl0pboy --fps 120 rom.gb # run the game at 2x speed
-./build/sl0pboy --uncapped rom.gb # run as fast as possible (turbo)
+./build/sl0pboy --frameskip 2 rom.gb # emulate 60fps, draw every 3rd (20fps)
+./build/sl0pboy --fps 120 rom.gb # 2x game speed
+./build/sl0pboy --uncapped rom.gb # turbo — as fast as it'll go
```
-| Flag | Meaning |
-|-------------------|-----------------------------------------------------|
-| `--frameskip N` | draw 1 of every `N+1` emulated frames (default 0) |
-| `--fps N` | emulation speed cap in fps (default 59.73 native) |
-| `--uncapped`/`--turbo` | run emulation as fast as possible |
+| Flag | Meaning |
+|------------------------|----------------------------------------------------|
+| `--frameskip N` | draw 1 of every `N+1` emulated frames (default 0) |
+| `--fps N` | emulation speed cap (default 59.73 native) |
+| `--uncapped`/`--turbo` | run emulation as fast as possible |
-Frame skip and turbo are also adjustable live with `[` / `]` and `f`; in turbo
-mode terminal redraws are additionally clamped to ~60 Hz. On exit the emulator
-prints how many frames it emulated vs. actually drew.
+Frame skip and turbo are live-adjustable with `[` / `]` and `f` (turbo also
+clamps redraws to ~60 Hz). On exit it prints frames emulated vs. actually drawn.
## Controls
@@ -161,31 +147,30 @@ prints how many frames it emulated vs. actually drew.
| enter | Start |
| space | Select |
| `f` | toggle fast-forward (turbo) |
-| `[` / `]` | decrease / increase frame skip |
+| `[` / `]` | frame skip down / up |
| `q` / Ctrl-C | Quit |
-Since terminals don't report key-release, buttons are held for a few frames
-after each keypress (relying on the terminal's key-repeat for sustained holds).
+Terminals don't report key-release, so buttons stay held for a few frames after
+each press (leaning on key-repeat for sustained holds).
-### External control channel (FIFO)
+## External control channel (FIFO)
-Start with `--fifo [path]` (default `/tmp/sl0pboy.fifo`) to open a named pipe that
-accepts button commands from any process. Unlike the terminal keyboard, this
-can send true key-**release** events, so it gives deterministic input for
-scripting and automated testing.
+`--fifo [path]` (default `/tmp/sl0pboy.fifo`) opens a named pipe any process can
+poke. Unlike the keyboard it can send true key-**release** events → deterministic
+input for scripting and tests.
```sh
./build/sl0pboy --fifo /tmp/sl0pboy.fifo rom.gb &
-echo 'a' > /tmp/sl0pboy.fifo # momentary tap of A
+echo 'a' > /tmp/sl0pboy.fifo # tap A
echo 'start' > /tmp/sl0pboy.fifo # tap Start
echo '+left' > /tmp/sl0pboy.fifo # press and HOLD Left
-echo 'a b' > /tmp/sl0pboy.fifo # tap A and B together (Left still held)
+echo 'a b' > /tmp/sl0pboy.fifo # tap A+B (Left still held)
echo '-left' > /tmp/sl0pboy.fifo # release Left
-echo 'release' > /tmp/sl0pboy.fifo # release everything held
-echo 'a:30' > /tmp/sl0pboy.fifo # tap A, held for 30 frames
+echo 'release' > /tmp/sl0pboy.fifo # release everything
+echo 'a:30' > /tmp/sl0pboy.fifo # tap A, held 30 frames
```
-Commands (one line, space/comma separated tokens, case-insensitive):
+One line, space/comma-separated, case-insensitive:
| Token | Effect |
|----------------|-----------------------------------------|
@@ -193,20 +178,16 @@ Commands (one line, space/comma separated tokens, case-insensitive):
| `name:N` | tap held for N frames |
| `+name` | press and hold until released |
| `-name` | release a held button |
-| `release` | release all held buttons |
+| `release` | release everything held |
-Set `GBC_INPUT_DEBUG=1` to trace resulting joypad state to stderr.
+`GBC_INPUT_DEBUG=1` traces joypad state to stderr. The FIFO is input-only and
+can't reply — for introspection use the socket below (it's a strict superset).
-The FIFO is one-way (input only) and can't reply. For introspection/debugging
-use the socket control channel below, which is a strict superset.
+## Socket control & debug channel
-### Socket control & debug channel
-
-Start with `--sock [path]` (default `/tmp/sl0pboy.sock`) to open a Unix-domain
-stream socket. It accepts everything the FIFO does **plus** emulator
-introspection commands, and — being a socket — it *replies* to each command and
-pushes asynchronous events when execution stops. Connect any line-oriented
-client:
+`--sock [path]` (default `/tmp/sl0pboy.sock`) opens a Unix stream socket. It does
+everything the FIFO does **plus** CPU/memory introspection, and — being a socket
+— it *replies* and pushes async events when execution stops:
```sh
./build/sl0pboy --sock /tmp/sl0pboy.sock rom.gb &
@@ -214,10 +195,9 @@ socat - UNIX-CONNECT:/tmp/sl0pboy.sock # interactive
# or: nc -U /tmp/sl0pboy.sock
```
-The protocol is line based (one command per line). Replies start with `ok` or
-`err`; asynchronous notifications start with `event`. Numbers accept `0x` hex
-or decimal; **memory bytes are hex** (so `read` output feeds straight back into
-`write`).
+Line-based: one command per line. Replies start `ok`/`err`; async notifications
+start `event`. Numbers take `0x` hex or decimal; **memory bytes are hex** so
+`read` output feeds straight back into `write`.
| Command | Effect |
|-----------------------------|----------------------------------------------------|
@@ -226,51 +206,50 @@ or decimal; **memory bytes are hex** (so `read` output feeds straight back into
| `state` | `ok state running\|paused` |
| `cpu` (`regs`) | dump CPU context (af/bc/de/hl/sp/pc/ime/halted/cycles) |
| `reg <name> <val>` | set a register/flag (a..l, af..hl, sp, pc, ime) |
-| `read <addr> [len]` | read `len` bytes (bus read), returned as hex |
-| `write <addr> <bytes…>` | write bytes: `de ad be ef` or a `deadbeef` string |
-| `step [n]` | single-step n instructions, replies with new context |
-| `continue` (`c`) | resume free-running emulation |
-| `pause` | halt CPU advancement at the next instruction |
+| `read <addr> [len]` | read `len` bytes (bus read), as hex |
+| `write <addr> <bytes…>` | write bytes: `de ad be ef` or `deadbeef` |
+| `step [n]` | single-step n instructions, replies w/ new context |
+| `continue` (`c`) | resume free-running emulation |
+| `pause` | halt at the next instruction |
| `break <addr>` | add a PC breakpoint (no arg lists them) |
| `delete <0xaddr\|#idx\|all>` | remove a breakpoint |
| `watch <addr>` | value-change watchpoint on a byte (no arg lists) |
| `unwatch <0xaddr\|#idx\|all>`| remove a watchpoint |
-| `record start <path> [everyN]` | capture frames to `<path>` (1 of every N); build a GIF with `tools/gbgif.py` |
-| `record stop` | finish the capture (replies with the frame count) |
-| `record` (`status`) | report whether a capture is active |
-| `input play <path> [reset]` | replay a synthesized input movie (frame-locked); `reset` = replay from a clean power-on |
-| `input stop` | stop movie playback (releases all buttons) |
-| `input` (`status`) | report movie playback state |
-| `quit` | shut the emulator down |
-| *(any button tokens)* | same vocabulary as the FIFO (tap/hold/release) |
+| `record start <path> [everyN]` | capture frames (1 of every N); GIF w/ `tools/gbgif.py` |
+| `record stop` | finish the capture (replies w/ frame count) |
+| `record` (`status`) | is a capture active? |
+| `input play <path> [reset]` | replay an input movie (frame-locked); `reset` = from clean power-on |
+| `input stop` | stop playback (releases all buttons) |
+| `input` (`status`) | movie playback state |
+| `quit` | shut it down |
+| *(any button tokens)* | same vocabulary as the FIFO |
-### Video capture & input replay (demo GIFs)
+## Video capture & input replay (demo GIFs)
-Capture the displayed frame to a flat file and turn it into a GIF:
+Dump the on-screen frame to a flat file, then bake a GIF:
-```
-./gbctl record start /abs/out.gbv 2 # capture 1 of every 2 frames (~30fps)
+```sh
+./gbctl record start /abs/out.gbv 2 # 1 of every 2 frames (~30fps)
...
./gbctl record stop
python3 tools/gbgif.py out.gbv out.gif --scale 3
```
-Recording captures exactly what's on screen: if the emulator was started with
-`--palette` and/or `--chrome`, those are baked into the capture (the `.gbv`
-header records the resulting dimensions, so e.g. chrome frames are 240×362 and
-`gbgif.py` picks that up automatically). `--shot` screenshots honor the same
-options. Start without them for the bare 160×144 true-color LCD.
+Capture grabs exactly what's on screen — start with `--palette`/`--chrome` and
+they're baked in (the `.gbv` header stores the real dimensions, so chrome frames
+come out 240×362 and `gbgif.py` figures it out automatically). `--shot`
+screenshots honor the same options. Skip them for the bare 160×144 LCD.
-Drive the ROM from a **synthesized input movie** instead of live input, for
-reproducible demos. A movie is a text file of `<frames> [buttons...]` lines
+Want reproducible demos? Drive the ROM from a **synthesized input movie** instead
+of live input. A movie is a text file of `<frames> [buttons...]` lines
(`a b start select up down left right`; none/`-` = released); the emulator
-applies one line's worth of joypad state per emulated frame, so replay is
-deterministic. Build one by hand or with `tools/gbmovie.py`:
+applies one line's joypad state per frame, so replay is deterministic. Build by
+hand or with `tools/gbmovie.py`:
```python
from gbmovie import Movie
m = Movie()
-m.wait(340) # boot splash + fastboot -> overworld
+m.wait(340) # boot + fastboot -> overworld
m.hold("select", 8) # open the SL0P menu
m.tap("down", repeat=13) # cursor to the last item
m.hold("a", 6); m.wait(600)
@@ -278,35 +257,31 @@ m.save("demo.gbmv")
```
`input play <path> reset` gives a clean, repeatable power-on (keeps cart
-ROM+SRAM, zeroes all other state), so a movie replays byte-identically every
-time. For a frame-exact demo GIF, arm both while paused so they start on the
-same frame:
+ROM+SRAM, zeroes everything else), so a movie replays byte-identically. For a
+frame-exact GIF, arm both while paused so they start on the same frame:
-```
+```sh
./gbctl pause
./gbctl input play /abs/demo.gbmv reset
./gbctl record start /abs/demo.gbv 2
./gbctl continue
-... # let the movie play
+... # let it play
./gbctl record stop
python3 tools/gbgif.py demo.gbv demo.gif --scale 3 --start 172
```
-When free-running (`continue`) hits a breakpoint or watchpoint, every connected
-client receives an async line and the machine pauses:
+When free-running hits a breakpoint/watchpoint, every connected client gets an
+async line and the machine pauses:
```
event stop breakpoint af=0x0840 bc=0x0800 … pc=0x0048 … cycles=72654560
event stop watch 0xFF44 af=0x90C0 … pc=0x4812 …
```
-There is also a `stopinfo` command (alias `why`/`laststop`) that reports the
-last reason execution halted, so a client that wasn't connected when an async
-`event` fired can still recover it (`ok stop breakpoint 0x0150 …`).
-
-`step` runs synchronously and replies immediately with the stop reason and the
-resulting context (`ok stop step …` / `ok stop breakpoint …` / `ok stop watch …`).
-Example session:
+Missed the event because you weren't connected yet? `stopinfo` (aka
+`why`/`laststop`) reports the last stop reason (`ok stop breakpoint 0x0150 …`).
+`step` runs synchronously and replies immediately with the stop reason + new
+context. A quick session:
```
> pause
@@ -324,56 +299,55 @@ ok running
event stop breakpoint … pc=0x0150 …
```
-#### `gbctl` — CLI driver (agent/tmux friendly)
+### `gbctl` — the CLI driver (agent/tmux friendly)
-`./gbctl` wraps the socket in a terse CLI that also spawns/stops a tmux pane
-running the emulator, so an LLM agent (or you) can drive it by tool-calling one
-command at a time. Socket auto-resolves to the single live instance.
+`./gbctl` wraps the socket in a terse CLI that spawns/stops a tmux pane running
+the emulator — so an LLM agent (or you) can drive it one tool-call at a time.
+Socket auto-resolves to the single live instance.
```sh
-./gbctl spawn roms/game.gb --uncapped # tmux pane + emulator --sock; waits ready
+./gbctl spawn roms/game.gb --uncapped # tmux pane + --sock; waits ready
./gbctl cpu ; ./gbctl read 0xC000 16 ; ./gbctl write 0xC000 deadbeef
-./gbctl break 0x0150 ; ./gbctl continue 5 # blocks up to 5s for the stop event
+./gbctl break 0x0150 ; ./gbctl continue 5 # blocks up to 5s for the stop
./gbctl press a b ; ./gbctl hold left ; ./gbctl release
-./gbctl screen ; ./gbctl stop # view the pane / tear down
+./gbctl screen ; ./gbctl stop # peek at the pane / tear down
```
-Run `./gbctl` with no args for the full command list. This is driven end-to-end
-by the `gbc-driver` skill.
+Run `./gbctl` bare for the full command list. Driven end-to-end by the
+`gbc-driver` skill.
## Architecture
-| File | Responsibility |
+| File | Does what |
|-------------|-----------------------------------------------------------|
| `types.h` | fixed-width integer typedefs |
| `cart.c` | ROM loading, header parsing, MBC1/2/3/5, battery saves |
| `gb.c` | system bus / memory map, joypad, OAM-DMA, HDMA/GDMA, reset|
-| `cpu.c` | Sharp SM83 core (full opcode set, cycle-accurate accesses)|
+| `cpu.c` | Sharp SM83 core (full opcodes, cycle-accurate accesses) |
| `timer.c` | DIV/TIMA/TMA/TAC with falling-edge accuracy |
| `ppu.c` | LCD controller, scanline renderer (BG/window/sprites), CGB|
| `render.c` | terminal truecolor output + raw-mode keyboard/FIFO input |
-| `control.c` | socket control/debug channel: memory, CPU, step, breakpoints|
-| `main.c` | argument parsing, frame loop, timing, screenshot/test modes|
+| `control.c` | socket control/debug: memory, CPU, step, breakpoints |
+| `main.c` | arg parsing, frame loop, timing, screenshot/test modes |
## Emulation status
-Passing:
-- **blargg `cpu_instrs`** — all 11 sub-tests
-- **blargg `instr_timing`**
-- **blargg `mem_timing`** / **`mem_timing-2`** — all 3
-- **dmg-acid2** — PPU test renders correctly
-- **cgb-acid2** — CGB PPU test renders correctly
+**Passing:**
+- blargg `cpu_instrs` — all 11 sub-tests
+- blargg `instr_timing`
+- blargg `mem_timing` / `mem_timing-2` — all 3
+- dmg-acid2 + cgb-acid2 — both render correctly
-Implemented:
+**Implemented:**
- Full SM83 CPU incl. HALT bug, EI delay, interrupt dispatch
- Accurate timer edge behavior
- PPU modes 0–3, STAT/LYC/VBlank interrupts, window line counter
-- CGB: double VRAM banks, BG/OBJ color palettes, tile attributes,
- master priority, WRAM banking, double-speed (KEY1), HDMA/GDMA
+- CGB: double VRAM banks, BG/OBJ color palettes, tile attributes, master
+ priority, WRAM banking, double-speed (KEY1), HDMA/GDMA
- MBC1/2/3/5 with RAM banking and battery `.sav` files
-Not yet implemented:
-- APU (audio) — registers are stubbed
+**Not yet:**
+- APU (audio) — registers stubbed
- MBC3 RTC advancement
- Sub-instruction PPU FIFO timing edge cases