# sl0pboy — 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/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 ``` ### Boot ROM / BIOS (`--bios`) 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: ```sh ./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 ``` 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. ### 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/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 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. ### 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/sl0pboy --sixel rom.gb # default 2x zoom (320×288 pixels) ./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. #### 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: ```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. #### 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: ```sh ./build/sl0pboy --sixel 3 --chrome --palette dmg rom.gb # classic DMG-01 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. ### 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/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) ``` | 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/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. ```sh ./build/sl0pboy --fifo /tmp/sl0pboy.fifo rom.gb & echo 'a' > /tmp/sl0pboy.fifo # momentary tap of 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 '-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 ``` 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/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: ```sh ./build/sl0pboy --sock /tmp/sl0pboy.sock rom.gb & 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`). | 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 | | `record start [everyN]` | capture frames to `` (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 [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) | ### Video capture & input replay (demo GIFs) Capture the displayed frame to a flat file and turn it into a GIF: ``` ./gbctl record start /abs/out.gbv 2 # capture 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. Drive the ROM from a **synthesized input movie** instead of live input, for reproducible demos. A movie is a text file of ` [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`: ```python from gbmovie import Movie m = Movie() m.wait(340) # boot splash + 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) m.save("demo.gbmv") ``` `input play 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: ``` ./gbctl pause ./gbctl input play /abs/demo.gbmv reset ./gbctl record start /abs/demo.gbv 2 ./gbctl continue ... # let the movie 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: ``` 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.