aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 8ce3002ac4967456955350156b033561ab01cd86 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# 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 <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       |
| `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)     |

### 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 `<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`:

```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 <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:

```
./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.