romdevtools 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -65,6 +65,14 @@ thousands of bytes and you'll drown).
|
|
|
65
65
|
|
|
66
66
|
This is the Cheat-Engine/RetroArch loop. It is THE bread-and-butter primitive.
|
|
67
67
|
|
|
68
|
+
**Don't know the value? (lives/timer/ammo not on the HUD)** Use the
|
|
69
|
+
unknown-initial-value hunt: `memory({op:'searchUnknown', region, size})` seeds
|
|
70
|
+
the WHOLE region with no value, then narrow across events with
|
|
71
|
+
`searchNext({compare:'dec'|'inc'|'unchanged'|'changed'|'gt'|'lt'})` — e.g.
|
|
72
|
+
`searchUnknown` → lose a life → `searchNext({compare:'dec'})` → repeat until 1–2
|
|
73
|
+
remain. `op:'search'` needs a value; `op:'searchUnknown'` is for when you can't
|
|
74
|
+
see the number.
|
|
75
|
+
|
|
68
76
|
**Stored ≠ displayed.** When a correct-looking seed returns 0, the byte usually isn't the
|
|
69
77
|
raw number: seed `as:'bcd'` for packed-BCD scores (2 decimal digits per byte — very common
|
|
70
78
|
on NES), or `as:'digits'` for one byte per ON-SCREEN digit at any constant tile base (HUD
|
|
@@ -78,7 +86,13 @@ not for value hunting. `memory({op:'diff'})` defaults to a **clustered summary**
|
|
|
78
86
|
stride) so it won't flood you — a reported stride (e.g. "islands at 0x80") is
|
|
79
87
|
usually a struct/entity array, each island one record. Small clusters (≤8 bytes) carry
|
|
80
88
|
`before`/`after` hex inline, and `minDelta:N` drops |after−before| < N so RNG/counter
|
|
81
|
-
wiggle disappears from the report.
|
|
89
|
+
wiggle disappears from the report. For the locate-value-via-diff case, predicate
|
|
90
|
+
filters cut a 500-byte death-window diff to the ~3 rows you want in one call:
|
|
91
|
+
`changeDir:'dec'|'inc'` (direction), `deltaEq:N` (signed exact delta — `deltaEq:-1`
|
|
92
|
+
= "lost one life"), and `beforeMin/Max` + `afterMin/Max` (value-range gates, e.g.
|
|
93
|
+
`beforeMax:9` = a small counter, not a coordinate). `outputPath` writes the full
|
|
94
|
+
diff JSON to your path regardless of size (`echo:false` returns just the
|
|
95
|
+
counts+path so a big diff never streams through context).
|
|
82
96
|
|
|
83
97
|
**"Which byte does this INPUT drive?" → `memory({op:'diffRuns'})`** — runs the same start
|
|
84
98
|
state twice (savestate restore in between) under two different held inputs (`portsA` vs
|
|
@@ -136,8 +150,11 @@ a string — find a terminator / font map before treating the bytes as values.
|
|
|
136
150
|
platforms (Genesis/Mega Drive, GB/GBC, SMS/GG, PCE, Lynx) the **file offset IS
|
|
137
151
|
the CPU ROM address** — `memory({op:'readCart', offset:0x21FF00})` answers "does the
|
|
138
152
|
running ROM have my bytes at 0x21FF00?" in one call. (NES/SNES: bytes are
|
|
139
|
-
correct but mapper-banked — `mapped:true` in the response
|
|
140
|
-
|
|
153
|
+
correct but mapper-banked — `mapped:true` in the response.) For a BANKED CPU
|
|
154
|
+
address, read it directly: `memory({op:'readCart', cpuAddress:0x8654, bank:6})`
|
|
155
|
+
maps the bank→PRG offset for you (NES/SNES) — the inverse of the breakpoint
|
|
156
|
+
result's bank/prgOffset, so you stop hand-computing `cpuAddr−0x8000+bank*0x4000`.
|
|
157
|
+
A NES `$C000+` address resolves to the fixed top bank automatically.
|
|
141
158
|
|
|
142
159
|
When a write "doesn't show up", check the ROM here before assuming the patch
|
|
143
160
|
failed — it's usually live and the bug is elsewhere (wrong source, see §2/§5).
|
|
@@ -170,6 +187,18 @@ can pass `{startAddress, bank}` to `disasm({target:'rom'})`. The lighter
|
|
|
170
187
|
and returns a frame-boundary PC — a lead, not a guarantee under interrupts; use it for the
|
|
171
188
|
value timeline or when you just want the change history, and cross-check the value trace.
|
|
172
189
|
|
|
190
|
+
**Stop on the MEANINGFUL write, not the churn.** `breakpoint({on:'write'})` runs
|
|
191
|
+
to END OF FRAME and reports the LAST matching write that frame (with `hits` =
|
|
192
|
+
the count of all matching writes) — so a frequent **restoring** write (a pointer-
|
|
193
|
+
arithmetic `inc`/`dec` that touches the byte every frame, a re-arm) can mask the
|
|
194
|
+
write you actually want. Filter to the real change with `condition` (all 14
|
|
195
|
+
platforms): `condition:'decrease'` / `'increase'` stop only when the stored byte
|
|
196
|
+
actually went down/up (a real lives−1, not a restore), and `condition:'equals',
|
|
197
|
+
conditionValue:N` stops on the byte becoming N (e.g. a $00→$01 respawn re-arm).
|
|
198
|
+
The hit then reports `oldValueByte`→`valueByte` so you see the exact transition.
|
|
199
|
+
This is the difference between pinning a genuine decrement instantly and chasing
|
|
200
|
+
net-zero restoring churn.
|
|
201
|
+
|
|
173
202
|
---
|
|
174
203
|
|
|
175
204
|
## 5b. To READ a register at an instruction — execution breakpoints (all 14)
|
|
@@ -58,7 +58,7 @@ and DLL; you do NOT poke pixels into a framebuffer.**
|
|
|
58
58
|
and (worse) burns enough cycles that the CPU stops getting time.
|
|
59
59
|
- **Y position = which zone the object lives in.** Each zone covers
|
|
60
60
|
N scanlines. To move an object up/down, you move it between
|
|
61
|
-
zones. (Or — in our
|
|
61
|
+
zones. (Or — in our example games — you stamp the same sprite at
|
|
62
62
|
different row offsets within ONE zone's data block, which fakes
|
|
63
63
|
Y movement.)
|
|
64
64
|
- **Each DL header can pick a palette per object** (one of 8
|
|
@@ -136,7 +136,7 @@ The "loop continues" mask is `0x5F` (bits 0-4 + bit 6). Bit 5
|
|
|
136
136
|
(indirect flag) and bit 7 (write-mode) do NOT keep the loop going
|
|
137
137
|
by themselves.
|
|
138
138
|
|
|
139
|
-
### 5-byte extended form (the bundled
|
|
139
|
+
### 5-byte extended form (the bundled example games use this)
|
|
140
140
|
|
|
141
141
|
```
|
|
142
142
|
+0 pixel-data LOW byte
|
|
@@ -195,7 +195,7 @@ scanlines for the ENTIRE display area (243 scanlines on NTSC,
|
|
|
195
195
|
including 10 lines of top overscan before the visible area).
|
|
196
196
|
|
|
197
197
|
If your DLL is shorter than 243 entries, MARIA reads past the end
|
|
198
|
-
into random memory and renders garbage zones. The bundled
|
|
198
|
+
into random memory and renders garbage zones. The bundled example
|
|
199
199
|
allocates 243 entries × 3 bytes = 729 bytes (fits easily in 4 KB
|
|
200
200
|
internal RAM) and points every zone with no objects at a shared
|
|
201
201
|
`dl_empty[2] = {0, 0}` terminator.
|
|
@@ -213,11 +213,11 @@ for an 8-row sprite) unless you pack many sprites per page.
|
|
|
213
213
|
**Easy work-around:** make every zone 1 scanline tall (offset=0)
|
|
214
214
|
and use one DL entry per sprite ROW. Then `offset` is always 0, the
|
|
215
215
|
address quirk goes away, and you can store sprite rows back-to-back.
|
|
216
|
-
The bundled
|
|
216
|
+
The bundled example uses this pattern.
|
|
217
217
|
|
|
218
218
|
The cost is more DLL entries (one per scanline), but at 3 bytes each
|
|
219
219
|
across 243 lines = 729 bytes total — trivial RAM cost. Worth it for
|
|
220
|
-
the simpler mental model on a starter
|
|
220
|
+
the simpler mental model on a starter example.
|
|
221
221
|
|
|
222
222
|
## Colour bytes (Atari NTSC palette)
|
|
223
223
|
|
|
@@ -56,14 +56,14 @@ you have to:
|
|
|
56
56
|
2. Place the sprite's DL entry into the zone covering its Y range.
|
|
57
57
|
3. Per-frame, move the entry's bytes from old-zone DL → new-zone DL.
|
|
58
58
|
|
|
59
|
-
Our
|
|
59
|
+
Our example games use a single-zone DLL for simplicity. Vertical
|
|
60
60
|
movement is faked by stamping the sprite at different row offsets
|
|
61
61
|
within the canvas data — only works if the canvas is tall enough.
|
|
62
62
|
|
|
63
63
|
## "Memory overflow during link (RAM1 by N bytes)"
|
|
64
64
|
|
|
65
65
|
The 7800 has **4 KB of RAM**. The `default.c` and `hello_sprite.c`
|
|
66
|
-
|
|
66
|
+
example games use very little; the `shmup.c` puzzle (and the older
|
|
67
67
|
canvas-buffer approach) easily blow past it.
|
|
68
68
|
|
|
69
69
|
Symptoms:
|
|
@@ -77,7 +77,7 @@ Fixes:
|
|
|
77
77
|
- Use ROM constants (`const uint8_t` at file scope) instead of
|
|
78
78
|
RAM globals.
|
|
79
79
|
- Replace canvas-buffer rendering with per-object DLs (see
|
|
80
|
-
`shmup.c`
|
|
80
|
+
`shmup.c` example for the canonical pattern).
|
|
81
81
|
- Avoid per-frame `memset(canvas, 0, ...)` — instead, only stamp
|
|
82
82
|
changed cells.
|
|
83
83
|
|
|
@@ -126,7 +126,7 @@ DL during active rendering; safe modification windows:
|
|
|
126
126
|
Build a "next-frame" DL during the game-state update phase and
|
|
127
127
|
swap pointers (DPPL/DPPH) at vblank — double-buffered.
|
|
128
128
|
|
|
129
|
-
Our
|
|
129
|
+
Our example games rebuild the DL during vblank, which works for small
|
|
130
130
|
DLs (< ~100 bytes). Large DLs that take ~1 ms to rebuild may
|
|
131
131
|
exceed vblank time and start corrupting the active frame.
|
|
132
132
|
|
|
@@ -197,5 +197,5 @@ Fix options (in order of how much they shrink BSS):
|
|
|
197
197
|
if you only need one sprite at a time — see `default.c` and
|
|
198
198
|
`hello_sprite.c`. No per-scanline pool needed.
|
|
199
199
|
|
|
200
|
-
The bundled
|
|
200
|
+
The bundled example games size their pools to fit; if you scale up
|
|
201
201
|
(more objects, taller play area), watch the build log.
|
|
@@ -115,9 +115,16 @@ which is what the KERNAL's IRQ uses to update key state every
|
|
|
115
115
|
|
|
116
116
|
**Joystick.** One fire button. Press it with `input({op:'set', b: true})` (or
|
|
117
117
|
spatial `south`) — both clear `$DC00` bit 4 (verified live). `a` is a **no-op**
|
|
118
|
-
(no second button). Drive fire with `b`/`south` + the d-pad.
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
(no second button). Drive fire with `b`/`south` + the d-pad.
|
|
119
|
+
|
|
120
|
+
**Two players.** BOTH C64 control ports are live at once, so 2P games just work:
|
|
121
|
+
**host port 0 = player 1** (control port 2, `$DC00`) and **host port 1 = player
|
|
122
|
+
2** (control port 1, `$DC01`) — the universal "port 0 = P1" convention. Pass two
|
|
123
|
+
port entries: `input({op:'set', ports:[{up:true}, {down:true}]})` moves P1 up and
|
|
124
|
+
P2 down independently. (Under the hood the host enables the VICE userport-adapter
|
|
125
|
+
mapping so both ports route, and swaps them so P1 lands on control port 2 where
|
|
126
|
+
the games read it.) The legacy `input({op:'joyport', joyport:1|2})` still selects
|
|
127
|
+
which single port a ONE-stick setup drives, but you rarely need it now.
|
|
121
128
|
|
|
122
129
|
**Keyboard (the C64-specific part — many games NEED it).** Unlike consoles, most
|
|
123
130
|
C64 games (and cracktros) gate gameplay behind a KEYBOARD setup screen — **F1**
|
|
@@ -311,7 +318,7 @@ loads games, wrap the `.prg` into a `.d64`: `cart({op:'packDisk', prgPath})`
|
|
|
311
318
|
|
|
312
319
|
## Horizontal scrolling (for side-scrollers)
|
|
313
320
|
|
|
314
|
-
The `platformer`
|
|
321
|
+
The `platformer` example is single-screen. C64 scrolling is the fiddliest of
|
|
315
322
|
the platforms because the VIC-II only does a 0-7 px *fine* scroll in hardware;
|
|
316
323
|
moving further is a software char-cell shift.
|
|
317
324
|
|
|
@@ -83,6 +83,19 @@ KERNAL last selected. Result: ghost input.
|
|
|
83
83
|
|
|
84
84
|
**Use port 2 (CIA1_PRA) by default.** All bundled C64 templates do.
|
|
85
85
|
|
|
86
|
+
## "Player 2 input does nothing"
|
|
87
|
+
|
|
88
|
+
Both C64 control ports ARE live over MCP, so 2P works — the mapping is just
|
|
89
|
+
non-obvious: **host port 0 → control port 2 ($DC00) = player 1**, **host port 1
|
|
90
|
+
→ control port 1 ($DC01) = player 2** (the universal "port 0 = P1" convention).
|
|
91
|
+
So a 2P game reads P1 from $DC00 and P2 from $DC01, and you drive them with two
|
|
92
|
+
port entries: `input({op:'set', ports:[{up:true},{down:true}]})` moves P1 up,
|
|
93
|
+
P2 down. If P2 seems dead, check you passed a SECOND `ports` entry (not just
|
|
94
|
+
port 0) and that the game actually entered 2P mode (its title pick, e.g. "PORT 1
|
|
95
|
+
FIRE = 2P"). The host enables the VICE userport-adapter mapping + swaps the two
|
|
96
|
+
RetroPad ports under the hood so this convention holds — you don't configure
|
|
97
|
+
anything.
|
|
98
|
+
|
|
86
99
|
## "Audio is silent / SID doesn't play"
|
|
87
100
|
|
|
88
101
|
Three things to check:
|
|
@@ -280,9 +280,9 @@ names also resolve (east→A, west→B). So `input({op:'set', a: true})` presses
|
|
|
280
280
|
expected — unlike the genesis_plus_gx platforms (Genesis/SMS/GG), there's no
|
|
281
281
|
surprise here. (Same for **GBC** — it shares the gambatte core.)
|
|
282
282
|
|
|
283
|
-
## What `
|
|
283
|
+
## What `examples({op:'fork'})` copies into your project
|
|
284
284
|
|
|
285
|
-
`
|
|
285
|
+
`examples({op:'fork', example:"gb/..."|"gbc/...", name, path})` writes these files
|
|
286
286
|
into your project directory. **They're yours** — every byte that compiles
|
|
287
287
|
is in the repo. Edit, fork, replace; nothing is auto-injected at build time.
|
|
288
288
|
|
|
@@ -340,7 +340,7 @@ Most game patterns DON'T need any of this. Try the C path first.
|
|
|
340
340
|
|
|
341
341
|
## Horizontal scrolling (for side-scrollers)
|
|
342
342
|
|
|
343
|
-
The `platformer`
|
|
343
|
+
The `platformer` example is single-screen. To make it a side-scroller:
|
|
344
344
|
|
|
345
345
|
- **Hardware scroll:** write `SCX` (`$FF43`) each frame = camera X mod 256.
|
|
346
346
|
The BG is a 32×32 tile map (256×256 px) that wraps, so `SCX` alone scrolls
|
|
@@ -174,7 +174,7 @@ pokes the BG map "whenever the state changes" will have SOME of those pokes
|
|
|
174
174
|
land mid-frame and vanish: stale cells, a piece that visually lags the
|
|
175
175
|
logical grid, glitches that move around as code timing shifts.
|
|
176
176
|
|
|
177
|
-
The robust pattern (used by the bundled puzzle
|
|
177
|
+
The robust pattern (used by the bundled puzzle example games):
|
|
178
178
|
|
|
179
179
|
1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
|
|
180
180
|
pairs to a small RAM queue whenever game state changes a cell.
|
|
@@ -195,7 +195,7 @@ pokes the BG map "whenever the state changes" will have SOME of those pokes
|
|
|
195
195
|
land mid-frame and vanish: stale cells, a piece that visually lags the
|
|
196
196
|
logical grid, glitches that move around as code timing shifts.
|
|
197
197
|
|
|
198
|
-
The robust pattern (used by the bundled puzzle
|
|
198
|
+
The robust pattern (used by the bundled puzzle example games):
|
|
199
199
|
|
|
200
200
|
1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
|
|
201
201
|
pairs to a small RAM queue whenever game state changes a cell.
|
|
@@ -208,6 +208,60 @@ The robust pattern (used by the bundled puzzle scaffolds):
|
|
|
208
208
|
If you must write outside that structure, turn the LCD off first (only
|
|
209
209
|
acceptable during init/load screens — mid-game it flashes white).
|
|
210
210
|
|
|
211
|
+
## "My HUD scrolls with the background" / "the window ate the bottom of my screen"
|
|
212
|
+
|
|
213
|
+
The window layer (WX/WY + LCDC bit 5) is the GB's fixed-HUD mechanism: it has
|
|
214
|
+
no scroll registers, always draws its own map from (0,0) pinned to the screen,
|
|
215
|
+
on top of the BG. Three rules, all demonstrated in the shmup example
|
|
216
|
+
(`examples/gb/templates/shmup.c`, "HARDWARE IDIOM: window-layer HUD"):
|
|
217
|
+
|
|
218
|
+
- **WX is screen X plus 7.** `WX=7` is the left edge. WX 0-6 produces real
|
|
219
|
+
DMG pixel-pipeline glitches; WX ≥ 167 pushes the window off-screen.
|
|
220
|
+
- **The window has no height register.** From the first line it covers (WY)
|
|
221
|
+
it owns EVERY line to the bottom of the frame, full width from WX. That's
|
|
222
|
+
why GB HUDs live at the BOTTOM of the screen (`WY = 128` → lines 128-143 =
|
|
223
|
+
HUD, lines 0-127 = scrolling playfield). A top HUD needs a mid-frame
|
|
224
|
+
STAT/LYC interrupt to turn LCDC bit 5 back off — a different, fragile
|
|
225
|
+
idiom; don't fall into it by accident by setting WY=0.
|
|
226
|
+
- **Sprites are NOT clipped by the window** — they draw on top of it. Despawn
|
|
227
|
+
(or Y-clamp) everything before the HUD line, or your enemies fly across
|
|
228
|
+
the score bar.
|
|
229
|
+
|
|
230
|
+
Use a separate map for the window (LCDC bit 6 → $9C00) so it doesn't fight
|
|
231
|
+
the BG's $9800 map.
|
|
232
|
+
|
|
233
|
+
## "Hi-score doesn't persist" / save_ram is empty or all $FF
|
|
234
|
+
|
|
235
|
+
Battery saves need BOTH halves:
|
|
236
|
+
|
|
237
|
+
1. **The header must declare a battery cart.** The bundled `gb_crt0.s` emits
|
|
238
|
+
`$0147 = $03` (MBC1+RAM+BATTERY) and `$0149 = $02` (8 KB) as real bytes,
|
|
239
|
+
and the build's post-link header fix passes them through. The emulator
|
|
240
|
+
sizes its SAVE_RAM region from those two bytes — type $00 (ROM-only)
|
|
241
|
+
means no save_ram region at all, and writes to $A000 go nowhere.
|
|
242
|
+
2. **Cart RAM is gated.** It boots DISABLED; writes are silently discarded
|
|
243
|
+
until you write `$0A` to $0000-$1FFF (any address there — it's a mapper
|
|
244
|
+
register, not memory). Write `$00` to the same range after saving
|
|
245
|
+
(battery hygiene: an enabled RAM bank can corrupt at power-off on real
|
|
246
|
+
hardware).
|
|
247
|
+
|
|
248
|
+
Working pattern with magic + checksum (a fresh cart is $FF garbage — never
|
|
249
|
+
trust raw bytes): shmup example, "HARDWARE IDIOM: battery SRAM". Verify
|
|
250
|
+
headlessly: play to a score, force game over, `memory({op:'read',
|
|
251
|
+
region:"save_ram"})` shows the record, and the hi-score still shows on the
|
|
252
|
+
title after a hard reset (power cycle).
|
|
253
|
+
|
|
254
|
+
## "Boot takes seconds" / a screen repaint visibly stalls the game
|
|
255
|
+
|
|
256
|
+
The sm83 has no divide instruction. SDCC's software `%` / `/` costs ~700
|
|
257
|
+
cycles per call — one `(r*7+c*5) % 11` in a 32×32 map fill is 2048 calls
|
|
258
|
+
≈ 1.5 MILLION cycles ≈ a 1.5-second frozen boot (measured, not theoretical;
|
|
259
|
+
the shmup example shipped exactly that for an hour). In any per-cell or
|
|
260
|
+
per-frame loop, replace modulo patterns with running counters +
|
|
261
|
+
subtract-on-overflow (see `paint_starfield` in the shmup example) and
|
|
262
|
+
decimal score display with power-of-ten subtraction (`u16_to_tiles` there).
|
|
263
|
+
A single `%` per event — e.g. per enemy spawn — is fine.
|
|
264
|
+
|
|
211
265
|
## Debug recipes
|
|
212
266
|
|
|
213
267
|
A few high-leverage tools you might not know exist:
|
|
@@ -247,14 +301,13 @@ Boot order that always works for GBC:
|
|
|
247
301
|
}
|
|
248
302
|
```
|
|
249
303
|
|
|
250
|
-
Cribbed from `examples/gbc/templates/tile_engine.c` —
|
|
251
|
-
|
|
304
|
+
Cribbed from `examples/gbc/templates/tile_engine.c` — fork that
|
|
305
|
+
example into a fresh game with:
|
|
252
306
|
|
|
253
307
|
```js
|
|
254
|
-
|
|
255
|
-
op: '
|
|
256
|
-
|
|
257
|
-
template: "tile_engine",
|
|
308
|
+
examples({
|
|
309
|
+
op: 'fork',
|
|
310
|
+
example: "gbc/tile_engine",
|
|
258
311
|
name: "mygame",
|
|
259
312
|
path: "/abs/path/to/dir",
|
|
260
313
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# GB / GBC C runtime + headers
|
|
2
2
|
|
|
3
3
|
These are the source files that back the GB/GBC C templates. They're
|
|
4
|
-
**not** auto-injected at build time — `
|
|
5
|
-
|
|
4
|
+
**not** auto-injected at build time — `examples({op:'fork', example:"gb/<name>" or
|
|
5
|
+
"gbc/<name>", name, path})` copies them into your project directory so the
|
|
6
6
|
project is self-describing. Build calls then point at your project's
|
|
7
7
|
copy of these files via `sourcesPaths` / `includePaths` / `crt0Path`.
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ didn't produce, or to override a field:
|
|
|
32
32
|
Fixes up / overrides the header of an existing ROM on disk (title, cart
|
|
33
33
|
type, ROM/RAM size, CGB flag, etc.).
|
|
34
34
|
- `node patch-header.js out.gb` — standalone Node script, copied into
|
|
35
|
-
every GB project by `
|
|
35
|
+
every GB project by `examples({op:'fork'})`. Same logic, no MCP needed.
|
|
36
36
|
- `rgbfix -v -p 0 out.gb` — what the build pipeline runs under the hood;
|
|
37
37
|
RGBDS asm projects can invoke it directly.
|
|
38
38
|
|
|
@@ -46,17 +46,16 @@ didn't produce, or to override a field:
|
|
|
46
46
|
hardware, OAM DMA timing, joypad layout. Read this before your first
|
|
47
47
|
GB/GBC project.
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Forking an example
|
|
50
50
|
|
|
51
|
-
Bootstrap a working game-loop skeleton with `
|
|
51
|
+
Bootstrap a working game-loop skeleton by forking an example with `examples({op:'fork'})`:
|
|
52
52
|
|
|
53
53
|
```js
|
|
54
|
-
|
|
55
|
-
op:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
path: "/abs/path",
|
|
54
|
+
examples({
|
|
55
|
+
op: 'fork',
|
|
56
|
+
example: "gbc/tile_engine", // or "gbc/hello_sprite", or "gbc/default"
|
|
57
|
+
name: "mygame",
|
|
58
|
+
path: "/abs/path",
|
|
60
59
|
})
|
|
61
60
|
```
|
|
62
61
|
|
|
@@ -86,11 +86,35 @@
|
|
|
86
86
|
nop
|
|
87
87
|
jp init
|
|
88
88
|
|
|
89
|
-
;; ─── Header bytes at $0104-$014F — host pipeline fills these
|
|
89
|
+
;; ─── Header bytes at $0104-$014F — host pipeline fills most of these ─
|
|
90
|
+
;; The logo / title / checksums are patched post-link (rgbfix in the
|
|
91
|
+
;; build pipeline, or patch-header.js when rebuilding outside romdev).
|
|
92
|
+
;; The CART TYPE and RAM SIZE bytes are DECLARED HERE as real bytes so
|
|
93
|
+
;; the build's header fixup can read and preserve them:
|
|
94
|
+
;;
|
|
95
|
+
;; $0147 = $03 MBC1 + RAM + BATTERY
|
|
96
|
+
;; $0149 = $02 8 KB external cart RAM at $A000-$BFFF
|
|
97
|
+
;;
|
|
98
|
+
;; This is what makes battery saves (persistent hi-scores) work: the
|
|
99
|
+
;; emulator sizes its SAVE_RAM from these two bytes. The RAM is gated —
|
|
100
|
+
;; games must write $0A to $0000-$1FFF before touching $A000 and write
|
|
101
|
+
;; $00 after (see the SRAM idiom in the shmup example). Games that never
|
|
102
|
+
;; touch $A000 are completely unaffected by the mapper declaration: a
|
|
103
|
+
;; 32 KB image fits in MBC1 banks 0-1 exactly as it does in a ROM-only
|
|
104
|
+
;; cart, and the MBC registers only react to ROM-area WRITES (which
|
|
105
|
+
;; normal code never performs).
|
|
106
|
+
;;
|
|
107
|
+
;; If you rebuild OUTSIDE romdev, keep these bytes: rgbfix flags are
|
|
108
|
+
;; `-m 0x03 -r 0x02` (patch-header.js defaults to ROM-only — pass
|
|
109
|
+
;; cartType/ramSize through patchGbHeader() if you script it).
|
|
90
110
|
.area _HEADERe (ABS)
|
|
91
111
|
.org 0x0104
|
|
92
|
-
|
|
93
|
-
.
|
|
112
|
+
.ds 0x43 ; $0104-$0146 logo/title/CGB flag/licensee/SGB (patched in)
|
|
113
|
+
.org 0x0147
|
|
114
|
+
.db 0x03 ; $0147 cart type: MBC1+RAM+BATTERY (see above)
|
|
115
|
+
.db 0x00 ; $0148 ROM size (rgbfix -p recomputes)
|
|
116
|
+
.db 0x02 ; $0149 RAM size: 8 KB
|
|
117
|
+
.ds 0x06 ; $014A-$014F dest/licensee/version/checksums (patched in)
|
|
94
118
|
|
|
95
119
|
;; ─── init: real boot code, lives in _CODE starting at $0150 ────────
|
|
96
120
|
.area _CODE
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// runs a bundled rgbfix after every gb/gbc link — see the
|
|
14
14
|
// "rgbfix (auto header fix)" line in build logs), so you only need this
|
|
15
15
|
// script when rebuilding the project OUTSIDE romdev with stock SDCC and
|
|
16
|
-
// no RGBDS installed. It's what keeps the
|
|
16
|
+
// no RGBDS installed. It's what keeps the forked project self-contained.
|
|
17
17
|
//
|
|
18
18
|
// The bundled gb_crt0.s reserves $0100-$014F for the header window,
|
|
19
19
|
// so the bytes patched in here land on actual cartridge-header
|
|
@@ -151,7 +151,17 @@ if (isCli) {
|
|
|
151
151
|
const rom = new Uint8Array(readFileSync(inPath));
|
|
152
152
|
// Auto-detect CGB based on file extension.
|
|
153
153
|
const cgb = /\.gbc$/i.test(inPath) || /\.gbc$/i.test(outPath);
|
|
154
|
-
|
|
154
|
+
// Battery-cart passthrough: the bundled gb_crt0.s emits the cart-type and
|
|
155
|
+
// RAM-size bytes EXPLICITLY ($0147=$03 MBC1+RAM+BATTERY, $0149=$02 8KB) so
|
|
156
|
+
// battery hi-score saves work. Preserve a known battery-MBC declaration
|
|
157
|
+
// instead of stomping it back to ROM-only; unknown/garbage bytes (a crt0
|
|
158
|
+
// that left the header window as a .ds gap) still get the safe defaults.
|
|
159
|
+
const BATTERY_TYPES = new Set([0x03, 0x06, 0x0F, 0x10, 0x13, 0x1B, 0x1E]);
|
|
160
|
+
const declType = rom.length > 0x149 ? rom[0x147] : 0x00;
|
|
161
|
+
const declRam = rom.length > 0x149 ? rom[0x149] : 0x00;
|
|
162
|
+
const cartType = BATTERY_TYPES.has(declType) ? declType : 0x00;
|
|
163
|
+
const ramSize = cartType !== 0x00 && declRam >= 0x01 && declRam <= 0x05 ? declRam : 0x00;
|
|
164
|
+
patchGbHeader(rom, { cgb, cartType, ramSize });
|
|
155
165
|
writeFileSync(outPath, rom);
|
|
156
|
-
console.log(`patched ${inPath}${outPath !== inPath ? ` → ${outPath}` : ""} (${rom.length} bytes${cgb ? ", CGB" : ""})`);
|
|
166
|
+
console.log(`patched ${inPath}${outPath !== inPath ? ` → ${outPath}` : ""} (${rom.length} bytes${cgb ? ", CGB" : ""}${cartType ? `, cart $${cartType.toString(16)}+RAM` : ""})`);
|
|
157
167
|
}
|
|
@@ -129,11 +129,11 @@ Sound FIFO states. See "MCP debug & inspection tooling" below for the rest of
|
|
|
129
129
|
the live-debug loop (sprites / palette / background / cpu / breakpoint + the
|
|
130
130
|
memory regions and disasm pipeline).
|
|
131
131
|
|
|
132
|
-
**For
|
|
132
|
+
**For starter-level sfx**, the libtonc runtime ships a minimal
|
|
133
133
|
`gba_sfx.h` / `gba_sfx.c` pair (3 functions: `sfx_init`, `sfx_tone`,
|
|
134
134
|
`sfx_noise`) that wraps the DMG-compatible APU directly. Same shape
|
|
135
|
-
as the NES/GB
|
|
136
|
-
the same. All 5 GBA genre
|
|
135
|
+
as the NES/GB example sound API, so cross-platform game ports feel
|
|
136
|
+
the same. All 5 GBA genre example games (shmup/platformer/puzzle/sports/
|
|
137
137
|
racing) use it.
|
|
138
138
|
|
|
139
139
|
## MCP debug & inspection tooling
|
|
@@ -210,7 +210,7 @@ the first call** (`irq_init(NULL)` + `irq_add(II_VBLANK, NULL)` with
|
|
|
210
210
|
libtonc — `irqInit(NULL)` + `irqEnable(IRQ_VBLANK)` with libgba).
|
|
211
211
|
Without this, the BIOS halts the CPU forever waiting for an IRQ that
|
|
212
212
|
never fires. ROM appears to compile + load but freezes on frame 1 —
|
|
213
|
-
single most common GBA gotcha. Every bundled
|
|
213
|
+
single most common GBA gotcha. Every bundled example does it; copy
|
|
214
214
|
the pattern.
|
|
215
215
|
|
|
216
216
|
## Cart header format
|
|
@@ -36,7 +36,7 @@ installs the master handler + `irq_add(II_VBLANK, NULL)` registers a
|
|
|
36
36
|
no-op for vblank (just so the IRQ fires + the BIOS counter increments).
|
|
37
37
|
With libgba, `irqInit()` does both steps.
|
|
38
38
|
|
|
39
|
-
Every bundled R28
|
|
39
|
+
Every bundled R28 example (`tonc_hello`, `tonc_hello_sprite`, `shmup`,
|
|
40
40
|
`platformer`, `puzzle`, `sports`, `racing`) sets this up — copy the
|
|
41
41
|
pattern.
|
|
42
42
|
|
|
@@ -132,10 +132,10 @@ install promise but everything else still works.
|
|
|
132
132
|
A deferred enhancement is to port libsysbase into our build so
|
|
133
133
|
iprintf "just works."
|
|
134
134
|
|
|
135
|
-
## Adding sound to
|
|
135
|
+
## Adding sound to your game
|
|
136
136
|
|
|
137
137
|
Both runtimes bundle `gba_sfx.h` + `gba_sfx.c` next to your `main.c`
|
|
138
|
-
(courtesy of `
|
|
138
|
+
(courtesy of `examples({op:'fork'})`). The shape:
|
|
139
139
|
|
|
140
140
|
```c
|
|
141
141
|
#include "gba_sfx.h"
|
|
@@ -76,6 +76,46 @@ void sfx_noise(u8 length_frames) {
|
|
|
76
76
|
REG_SND4FREQ = 0xC033;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/* ── background music: 16-step melody loop on channel 2 ─────────────
|
|
80
|
+
* Mirrors the Genesis/NES example pattern: games get continuous music
|
|
81
|
+
* for free by calling sfx_music_tick() once per frame. The melody is a
|
|
82
|
+
* gentle A-minor arpeggio at reduced envelope volume (0xA of 0xF) so
|
|
83
|
+
* one-shot SFX on channel 1 / noise on 4 read clearly over it.
|
|
84
|
+
* Note values are GBA 11-bit frequency codes: Hz = 131072/(2048-code),
|
|
85
|
+
* code = 2048 - 131072/Hz. 0 = rest. */
|
|
86
|
+
static const u16 music_code[16] = {
|
|
87
|
+
1452, 1548, 1651, 1750, /* A3 C4 E4 A4 */
|
|
88
|
+
1714, 1651, 1548, 0, /* G4 E4 C4 - */
|
|
89
|
+
1452, 1546, 1620, 1714, /* A3 C4 D4 G4 */
|
|
90
|
+
1651, 1548, 1452, 0, /* E4 C4 A3 - */
|
|
91
|
+
};
|
|
92
|
+
static u8 music_enabled = 1;
|
|
93
|
+
static u8 music_step, music_timer;
|
|
94
|
+
|
|
95
|
+
void sfx_music(u8 on) {
|
|
96
|
+
music_enabled = on;
|
|
97
|
+
music_step = 0;
|
|
98
|
+
music_timer = 0;
|
|
99
|
+
if (!on) REG_SND2CNT = 0x0000; /* zero envelope = silence now */
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
void sfx_music_tick(void) {
|
|
103
|
+
if (!music_enabled) return;
|
|
104
|
+
if (music_timer == 0) {
|
|
105
|
+
u16 code = music_code[music_step & 15];
|
|
106
|
+
if (code) {
|
|
107
|
+
/* vol 0xA, 50% duty, length 40/64 steps (~156ms) — the
|
|
108
|
+
* length-enable auto-silences before the next note so
|
|
109
|
+
* rests actually rest. */
|
|
110
|
+
REG_SND2CNT = (u16)(0xA080 | (64 - 40));
|
|
111
|
+
REG_SND2FREQ = (u16)(0xC000 | (code & 0x07FF));
|
|
112
|
+
}
|
|
113
|
+
music_step++;
|
|
114
|
+
}
|
|
115
|
+
music_timer++;
|
|
116
|
+
if (music_timer >= 10) music_timer = 0; /* 6 notes/sec at 60fps */
|
|
117
|
+
}
|
|
118
|
+
|
|
79
119
|
void sfx_off(void) {
|
|
80
120
|
REG_SNDSTAT = 0x0000;
|
|
81
121
|
}
|
|
@@ -42,6 +42,16 @@ void sfx_tone(u8 channel, u16 freq_period, u8 length_frames);
|
|
|
42
42
|
* length_frames: 1..63 (same scaling as sfx_tone). */
|
|
43
43
|
void sfx_noise(u8 length_frames);
|
|
44
44
|
|
|
45
|
+
/* ── background music ────────────────────────────────────────────────
|
|
46
|
+
* A 16-step square-wave melody loop on channel 2 (so keep one-shot SFX
|
|
47
|
+
* on channel 1 + noise on 4 and nothing fights for the channel).
|
|
48
|
+
* Call sfx_music_tick() once per frame from your main loop — it steps
|
|
49
|
+
* the melody. ON by default after sfx_init(); sfx_music(0) silences it.
|
|
50
|
+
* "No sound" feedback in playtests is nearly always a missing per-frame
|
|
51
|
+
* tick, not broken registers. */
|
|
52
|
+
void sfx_music(u8 on);
|
|
53
|
+
void sfx_music_tick(void);
|
|
54
|
+
|
|
45
55
|
/* Power down the APU. */
|
|
46
56
|
void sfx_off(void);
|
|
47
57
|
|
|
@@ -114,7 +114,7 @@ The CGB boot ROM checks header byte **`$0143`**:
|
|
|
114
114
|
- `$80` → CGB-enhanced mode (color works, DMG-compat fallback)
|
|
115
115
|
- `$C0` → CGB-only mode (refuses to boot on a DMG)
|
|
116
116
|
|
|
117
|
-
**Every bundled GBC
|
|
117
|
+
**Every bundled GBC example game is built with `$0143 = $80`** — `build({output:'rom'})`
|
|
118
118
|
/ `build({output:'run'})` set this automatically at build time when `platform:"gbc"`,
|
|
119
119
|
so a freshly built `.gbc` boots in color with no extra step. (Build it as
|
|
120
120
|
`platform:"gb"` instead and the flag stays `$00` → DMG green-shade mode,
|
|
@@ -197,12 +197,12 @@ inversion: `{a}`→A, `{b}`→B, `{start}`/`{select}`, plus the d-pad (spatial
|
|
|
197
197
|
east→A, west→B). So `input({op:'set', a: true})` presses GBC A as expected — unlike
|
|
198
198
|
the genesis_plus_gx platforms (Genesis/SMS/GG), there's no surprise here.
|
|
199
199
|
|
|
200
|
-
##
|
|
200
|
+
## Example games
|
|
201
201
|
|
|
202
|
-
All GB
|
|
202
|
+
All GB example games (`shmup`, `platformer`, `puzzle`, `sports`, `racing`,
|
|
203
203
|
`hello_sprite`, `tile_engine`) compile identically as GBC ROMs — the
|
|
204
204
|
bundled GB runtime is already CGB-aware (writes OCPD/OCPS for color).
|
|
205
|
-
The genre
|
|
205
|
+
The genre examples inherit from GB via `TEMPLATES.gbc = TEMPLATES.gb`;
|
|
206
206
|
the only differences at build time are:
|
|
207
207
|
|
|
208
208
|
- ROM extension: `.gbc` (vs `.gb`)
|
|
@@ -128,7 +128,7 @@ pokes the BG map "whenever the state changes" will have SOME of those pokes
|
|
|
128
128
|
land mid-frame and vanish: stale cells, a piece that visually lags the
|
|
129
129
|
logical grid, glitches that move around as code timing shifts.
|
|
130
130
|
|
|
131
|
-
The robust pattern (used by the bundled puzzle
|
|
131
|
+
The robust pattern (used by the bundled puzzle example games):
|
|
132
132
|
|
|
133
133
|
1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
|
|
134
134
|
pairs to a small RAM queue whenever game state changes a cell.
|
|
@@ -149,7 +149,7 @@ sound channels or extra waveforms.
|
|
|
149
149
|
|
|
150
150
|
## "ROM size > 32 KB needed"
|
|
151
151
|
|
|
152
|
-
The bundled GBC
|
|
152
|
+
The bundled GBC example games all fit in 32 KB (single bank, no MBC).
|
|
153
153
|
For larger projects use an MBC (memory bank controller). MBC1 / MBC3
|
|
154
154
|
work in gambatte; set the `$0147` cartridge type byte accordingly.
|
|
155
155
|
romPatch({op:'gbHeader'}) doesn't set this — you write it from your asm/C.
|
|
@@ -159,11 +159,11 @@ romPatch({op:'gbHeader'}) doesn't set this — you write it from your asm/C.
|
|
|
159
159
|
Default GBC speed is the same as DMG (~4 MHz Z80). Double-speed mode
|
|
160
160
|
via KEY1 ($FF4D) doubles CPU but halves audio sample rate + breaks
|
|
161
161
|
cycle-counted code. Most homebrew leaves it off; if you need the
|
|
162
|
-
extra clocks, change the GB
|
|
162
|
+
extra clocks, change the GB example pattern to:
|
|
163
163
|
|
|
164
164
|
```c
|
|
165
165
|
KEY1 = 1; /* request speed switch */
|
|
166
166
|
__asm__("stop"); /* arm the switch (compiler-specific syntax) */
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
Not bundled in any
|
|
169
|
+
Not bundled in any example — use only if you've measured a need.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
GBC shares its toolchain (SDCC sm83) + emulator (gambatte) + most
|
|
4
4
|
of the runtime (`gb_runtime.c`, `gb_crt0.s`, `patch-header.js`,
|
|
5
5
|
hUGEDriver) with DMG. The CGB tree (`src/platforms/gbc/`) holds
|
|
6
|
-
the color-aware
|
|
6
|
+
the color-aware example games; everything below is in lockstep with
|
|
7
7
|
the GB tree.
|
|
8
8
|
|
|
9
9
|
CGB-specific:
|