aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 80151c42aa37878c83d29af92e4d2c9f57e8d374 (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
# 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 <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                               |
| `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.