diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 358 |
1 files changed, 166 insertions, 192 deletions
@@ -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 |
