# gbc — a Game Boy Color emulator in C, rendered to the 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. 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. ## Build & run ```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 --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) ``` Requires a truecolor-capable terminal (`COLORTERM=truecolor`) at least 160 columns wide. ### 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: ```sh ./build/gbc rom.gb --sixel # default 2x zoom (320×288 pixels) ./build/gbc rom.gb --sixel 4 # 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. ### 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: ```sh ./build/gbc rom.gb --frameskip 2 # emulate 60 fps, draw every 3rd frame (20 fps) ./build/gbc rom.gb --fps 120 # run the game at 2x speed ./build/gbc rom.gb --uncapped # run as fast as possible (turbo) ``` | 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 | 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. ## Controls | Key | Button | |----------------|---------| | arrows / WASD | D-pad | | `z` / `j` | A | | `x` / `k` | B | | enter | Start | | space | Select | | `f` | toggle fast-forward (turbo) | | `[` / `]` | decrease / increase frame skip | | `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). ### External control channel (FIFO) Start with `--fifo [path]` (default `/tmp/gbc.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. ```sh ./build/gbc rom.gb --fifo /tmp/gbc.fifo & echo 'a' > /tmp/gbc.fifo # momentary tap of A echo 'start' > /tmp/gbc.fifo # tap Start echo '+left' > /tmp/gbc.fifo # press and HOLD Left echo 'a b' > /tmp/gbc.fifo # tap A and B together (Left still held) echo '-left' > /tmp/gbc.fifo # release Left echo 'release' > /tmp/gbc.fifo # release everything held echo 'a:30' > /tmp/gbc.fifo # tap A, held for 30 frames ``` Commands (one line, space/comma separated tokens, case-insensitive): | Token | Effect | |----------------|-----------------------------------------| | `a b start select up down left right` | momentary tap | | `name:N` | tap held for N frames | | `+name` | press and hold until released | | `-name` | release a held button | | `release` | release all held buttons | Set `GBC_INPUT_DEBUG=1` to trace resulting joypad state to stderr. 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 Start with `--sock [path]` (default `/tmp/gbc.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: ```sh ./build/gbc rom.gb --sock /tmp/gbc.sock & socat - UNIX-CONNECT:/tmp/gbc.sock # interactive # or: nc -U /tmp/gbc.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`). | Command | Effect | |-----------------------------|----------------------------------------------------| | `ping` | `pong` liveness check | | `help` | one-line command summary | | `state` | `ok state running\|paused` | | `cpu` (`regs`) | dump CPU context (af/bc/de/hl/sp/pc/ime/halted/cycles) | | `reg ` | set a register/flag (a..l, af..hl, sp, pc, ime) | | `read [len]` | read `len` bytes (bus read), returned as hex | | `write ` | 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 | | `break ` | add a PC breakpoint (no arg lists them) | | `delete <0xaddr\|#idx\|all>` | remove a breakpoint | | `watch ` | value-change watchpoint on a byte (no arg lists) | | `unwatch <0xaddr\|#idx\|all>`| remove a watchpoint | | `quit` | shut the emulator down | | *(any button tokens)* | same vocabulary as the FIFO (tap/hold/release) | When free-running (`continue`) hits a breakpoint or watchpoint, every connected client receives 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: ``` > pause ok paused af=0x9040 … pc=0x021D … > break 0x0150 ok break #0 0x0150 > read 0xFF44 1 ok read 0xFF44 1 90 > write 0xC000 de ad be ef ok write 0xC000 4 > reg pc 0x0150 ok reg pc=0x0150 > continue ok running event stop breakpoint … pc=0x0150 … ``` #### `gbctl` — 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. ```sh ./gbctl spawn roms/game.gb --uncapped # tmux pane + emulator --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 press a b ; ./gbctl hold left ; ./gbctl release ./gbctl screen ; ./gbctl stop # view 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. ## Architecture | File | Responsibility | |-------------|-----------------------------------------------------------| | `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)| | `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| ## 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 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 - MBC1/2/3/5 with RAM banking and battery `.sav` files Not yet implemented: - APU (audio) — registers are stubbed - MBC3 RTC advancement - Sub-instruction PPU FIFO timing edge cases ## License For educational use. Test ROMs are downloaded separately and not included.