romdevtools 0.27.0 → 0.29.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 +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- 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 +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- 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 -177
- 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 -180
- 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 -156
- 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 +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- 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 +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- 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 +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- 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 +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- 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 +19 -6
- 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 +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- 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/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- 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 +64 -19
|
@@ -59,15 +59,34 @@ thousands of bytes and you'll drown).
|
|
|
59
59
|
on-screen value. `region` defaults to `system_ram`.
|
|
60
60
|
2. Change the value in-game (take damage, score a point), then
|
|
61
61
|
`memory({op:'searchNext', compare:'eq', value})` — or `compare:'gt'|'lt'|'changed'|'unchanged'|
|
|
62
|
-
'inc'|'dec'` when you don't know the new value.
|
|
62
|
+
'inc'|'dec'` when you don't know the new value. The relative compares work as the
|
|
63
|
+
FIRST narrow too (baselines are recorded at seed). Repeat until a handful remain.
|
|
63
64
|
3. Confirm: `memory({op:'write'})` the candidate and watch the screen react.
|
|
64
65
|
|
|
65
66
|
This is the Cheat-Engine/RetroArch loop. It is THE bread-and-butter primitive.
|
|
66
67
|
|
|
68
|
+
**Stored ≠ displayed.** When a correct-looking seed returns 0, the byte usually isn't the
|
|
69
|
+
raw number: seed `as:'bcd'` for packed-BCD scores (2 decimal digits per byte — very common
|
|
70
|
+
on NES), or `as:'digits'` for one byte per ON-SCREEN digit at any constant tile base (HUD
|
|
71
|
+
tile-index buffers; the matched base is reported per candidate, and `searchNext` keeps
|
|
72
|
+
comparing in the same representation). For displayed−1 lives or ÷10 scores, just seed the
|
|
73
|
+
transformed number. If an INPUT drives the value (position, velocity, charge), skip the
|
|
74
|
+
loop entirely: `memory({op:'diffRuns', portsA:[{right:true}]})` isolates it in one call.
|
|
75
|
+
|
|
67
76
|
`memory({op:'snapshot'})` + `memory({op:'diff'})` is for "which bytes did THIS one event touch?",
|
|
68
77
|
not for value hunting. `memory({op:'diff'})` defaults to a **clustered summary** (ranges +
|
|
69
78
|
stride) so it won't flood you — a reported stride (e.g. "islands at 0x80") is
|
|
70
|
-
usually a struct/entity array, each island one record.
|
|
79
|
+
usually a struct/entity array, each island one record. Small clusters (≤8 bytes) carry
|
|
80
|
+
`before`/`after` hex inline, and `minDelta:N` drops |after−before| < N so RNG/counter
|
|
81
|
+
wiggle disappears from the report.
|
|
82
|
+
|
|
83
|
+
**"Which byte does this INPUT drive?" → `memory({op:'diffRuns'})`** — runs the same start
|
|
84
|
+
state twice (savestate restore in between) under two different held inputs (`portsA` vs
|
|
85
|
+
`portsB`, default released) for `frames` each, and returns only the bytes that DIVERGE
|
|
86
|
+
between the runs, with run-A/run-B values on small clusters. One call replaces the whole
|
|
87
|
+
save → hold → step → dump → restore → hold-other → dump → diff loop; the frame counter and
|
|
88
|
+
all input-independent churn cancel out automatically. (The emulator is left at the end of
|
|
89
|
+
run B.)
|
|
71
90
|
|
|
72
91
|
---
|
|
73
92
|
|
|
@@ -140,7 +159,12 @@ the copy reads from, then `breakpoint({on:'write'})` on THAT.
|
|
|
140
159
|
**Precision — exact vs sampled.** The default `breakpoint({on:'write'})` is a core-level write
|
|
141
160
|
watchpoint: it returns the EXACT writing instruction's PC, captured inside the CPU write
|
|
142
161
|
path — correct even for NMI/IRQ-driven writes (the common case where a frame-sampled PC
|
|
143
|
-
is just the idle loop). On
|
|
162
|
+
is just the idle loop). On ALL 14 platforms, every hit (write/read/pc) also carries
|
|
163
|
+
**`registersAtHit`** — the full register file frozen AT the hit instant — and the CPU
|
|
164
|
+
stays FROZEN until the hit is cleared. Use registersAtHit instead of a follow-up
|
|
165
|
+
`cpu({op:'read'})`: pre-0.28.0 the live registers kept running after a hit (on gpgx they
|
|
166
|
+
drifted hundreds of instructions — address registers read that way were someone else's
|
|
167
|
+
values). On a banked mapper it reports the `bank` (NES/GB/SMS-GG) so you
|
|
144
168
|
can pass `{startAddress, bank}` to `disasm({target:'rom'})`. The lighter
|
|
145
169
|
`breakpoint({on:'write', precision:'sampled'})` (a.k.a. `watch({on:'mem'})`) steps until the byte changes
|
|
146
170
|
and returns a frame-boundary PC — a lead, not a guarantee under interrupts; use it for the
|
|
@@ -197,6 +221,16 @@ pushes a sentinel return, and runs until it returns. Most of these formats have
|
|
|
197
221
|
you can usually craft a replacement by hand. (sandbox:false leaves the dest buffer
|
|
198
222
|
live for `memory({op:'read'})`; sandbox:true restores the game untouched.)
|
|
199
223
|
|
|
224
|
+
**Pass `pure:true` — on every platform.** A non-pure call that spans frames runs the
|
|
225
|
+
game's OWN frame logic concurrently (VBlank handlers via RAM vectors, music
|
|
226
|
+
drivers) — which can overwrite the dest buffer mid-call and hand you poisoned
|
|
227
|
+
"ground truth" (a real session spent hours diffing a CORRECT reimplementation
|
|
228
|
+
against it). With `pure:true` the game's handlers CANNOT run: Genesis/SMS/GG step
|
|
229
|
+
only the CPU (`pureMode:'cpu-only'`); everywhere else interrupt DELIVERY is
|
|
230
|
+
suppressed for the duration (`'irq-blocked'` — pending lines stay pending, video
|
|
231
|
+
advances harmlessly); the 2600 has no interrupts (`'no-interrupts'`). Non-pure
|
|
232
|
+
results carry a ⚠ caveat whenever frame logic ran.
|
|
233
|
+
|
|
200
234
|
## 5e. Re-inject an edited asset — the round-trip (don't reimplement the compressor)
|
|
201
235
|
|
|
202
236
|
Once you can SEE the decompressed bytes (5c) and you've edited them, put them BACK
|
|
@@ -315,9 +349,14 @@ Once you know WHAT to change, the write loop is a handful of calls — no custom
|
|
|
315
349
|
confirm a patch landed where you meant.
|
|
316
350
|
- **`disasm({target:'references', path, platform, address})`** — find every instruction that
|
|
317
351
|
references a target address, classified `call/jump/branch/read/write/use/ref` (walks the
|
|
318
|
-
vector table too). The fast "who touches this?" for a STATIC image.
|
|
319
|
-
|
|
320
|
-
|
|
352
|
+
vector table too). The fast "who touches this?" for a STATIC image. EVERY banked format
|
|
353
|
+
is scanned PER BANK — NES mappers (refs carry `prgBank`), and SNES multi-bank LoROM,
|
|
354
|
+
GB/GBC MBC, SMS/GG Sega-mapper, MSX megaROM, Atari 2600 F8/F6/F4, Atari 7800 SuperGame,
|
|
355
|
+
and >32KB HuCards (refs carry `romBank`) — so a hit in bank 12 of a 128KB cart shows up,
|
|
356
|
+
not just the first bank. Zero-page direct + indexed operands match, and `#$nn` immediates
|
|
357
|
+
are excluded (values, not addresses). Limitation: direct addressing only —
|
|
358
|
+
indirect/computed jumps aren't detected (use the runtime `watch`/`breakpoint` tools in
|
|
359
|
+
§5/§5d for those).
|
|
321
360
|
- **`cart({op:'extract', path, outputDir})`** — split a ROM into standard parts (NES header/
|
|
322
361
|
prg/chr; SNES copier_header+rom+internal header; Genesis vectors/header/body; GB boot/
|
|
323
362
|
header/body) + a `manifest.json` (mapper, mirroring…). **`cart({op:'wrap'})`** is the inverse:
|
|
@@ -330,8 +369,9 @@ watch the screen react — cheaper than shipping a wrong ROM patch.
|
|
|
330
369
|
|
|
331
370
|
For a STRUCTURAL hack (new logic, not a byte poke), turn the whole ROM into a
|
|
332
371
|
re-buildable project in one call: `disasm({target:'project', path, outputDir})`. It splits
|
|
333
|
-
the ROM into regions (per-
|
|
334
|
-
for
|
|
372
|
+
the ROM into regions (per-bank on EVERY banked format: 16KB banks for NES/GB/SMS-GG/MSX/
|
|
373
|
+
7800-SuperGame, 32KB for SNES LoROM, 4KB for banked 2600, 8KB pages for >32KB HuCards;
|
|
374
|
+
one flat region for Genesis/C64/Lynx/GBA and small carts), disassembles each through the CPU's
|
|
335
375
|
native objdump, then **reassembles + verifies byte-exact** against the original; any line
|
|
336
376
|
that won't reproduce faithfully heals to a `.byte`/`db` of its real bytes, so the emitted
|
|
337
377
|
`.asm` ALWAYS rebuilds (`roundTrip.allByteExact`). `readablePercent` per region tells you
|
|
@@ -343,22 +383,27 @@ rebuild exists — a `rebuild.json` of the precise `build({...})` args. So the l
|
|
|
343
383
|
|
|
344
384
|
**Two rebuild tiers** (the disasm emits each CPU's native-reassembler syntax — ca65 for
|
|
345
385
|
6502/65816, GNU `as` for m68k/arm/z80/gbz80 — which only some `build()` toolchains consume):
|
|
346
|
-
- **One-call `build()` rebuild, byte-identical** — **NES
|
|
347
|
-
|
|
348
|
-
|
|
386
|
+
- **One-call `build()` rebuild, byte-identical** — **NES (NROM *and* banked mappers), C64,
|
|
387
|
+
Atari 7800 (flat *and* SuperGame banked), Lynx, PC Engine (flat *and* banked HuCards)**.
|
|
388
|
+
Feed `rebuild.json` straight to `build`. Banked projects ship a HEADER segment with the
|
|
389
|
+
original header bytes (16 iNES / 128 .a78 / 512 copier), per-bank segment wrappers, and a
|
|
390
|
+
generated multi-bank `.cfg` referenced via `linkerConfigPath` (so the cfg never streams
|
|
391
|
+
through context). (Lynx: `build()` yields the headerless image; prepend the shipped
|
|
392
|
+
`lnx_header.bin` for the full `.lnx`.)
|
|
349
393
|
- **Native-recipe rebuild (`buildCall:null`), byte-identical, steps in `BUILD.md`** — **SMS,
|
|
350
394
|
GG, MSX, GB, GBC, Genesis, GBA, Atari 2600**. Their `build()` toolchains (SDCC/RGBDS/asar/
|
|
351
395
|
dasm/vasm) can't reassemble ca65/GNU-as syntax, so `BUILD.md` gives the proven native
|
|
352
|
-
`as`/`ld`/`objcopy` chain
|
|
353
|
-
|
|
354
|
-
strip a copier header) — `BUILD.md` flags it.
|
|
396
|
+
`as`/`ld`/`objcopy` chain — per-bank on banked carts (Sega-mapper SMS/GG, MSX megaROMs,
|
|
397
|
+
banked 2600 get per-bank wrappers + cfg blobs and a bank-by-bank recipe).
|
|
355
398
|
|
|
356
399
|
**Rebuilding a commercial NES (NROM CHR-ROM) game — `build({inesHeader})`:** the most common
|
|
357
400
|
NES RE rebuild. `build({output:'rom', platform:'nes', inesHeader:{prgBanks, chrBanks, mapper,
|
|
358
401
|
mirroring}, sourcesPaths:{…the PRG…}, binaryIncludePaths:{"chr.bin":…}})` auto-emits the
|
|
359
402
|
16-byte iNES header + CHARS-segment wiring + flat NROM `.cfg` — no hand-derived header bytes.
|
|
360
|
-
`disasm({target:'project'})` puts exactly this call in `rebuild.json
|
|
361
|
-
|
|
403
|
+
`disasm({target:'project'})` puts exactly this call in `rebuild.json` for NROM; banked
|
|
404
|
+
mappers get the per-bank segment + multi-bank `.cfg` form instead (see the one-call tier
|
|
405
|
+
above). (For homebrew C that ships fixed tile art, `linkerConfig:"chr-rom"` is the
|
|
406
|
+
segment-split equivalent.)
|
|
362
407
|
|
|
363
408
|
**Readability caveats** (the bytes are ALWAYS correct; only instruction-vs-`.byte` coverage
|
|
364
409
|
varies): SNES and large Genesis ROMs come back byte-exact but DATA-ONLY (flat whole-ROM
|
|
@@ -398,6 +443,7 @@ For sprite/tile edits (not text), don't hand-roll the tile-format math:
|
|
|
398
443
|
|---|---|
|
|
399
444
|
| Find a value's address | `memory({op:'search'})` → `memory({op:'searchNext'})` (NOT full-RAM diff) |
|
|
400
445
|
| Which bytes did one event touch | `memory({op:'snapshot'})` → `memory({op:'diff'})` (summary) |
|
|
446
|
+
| Which byte does an INPUT drive | `memory({op:'diffRuns', portsA, portsB?})` (A/B divergence, one call) |
|
|
401
447
|
| Is on-screen text a string or a bitmap | `text({op:'learn'})` (reports pre-rendered graphic) |
|
|
402
448
|
| Is a "table" really ASCII/code | `memory({op:'classify'})` |
|
|
403
449
|
| Confirm a patch is in the running ROM | `memory({op:'readCart'})` |
|
|
@@ -406,7 +452,8 @@ For sprite/tile edits (not text), don't hand-roll the tile-format math:
|
|
|
406
452
|
| Which instruction READ a byte | `breakpoint({on:'read', address})` (read-side `breakpoint({on:'write'})`) |
|
|
407
453
|
| Single-step the CPU | `frame({op:'stepInstruction'})` (+ `cpu({op:'read'})` to watch regs) |
|
|
408
454
|
| Set a CPU register | `cpu({op:'setReg', regId, value})` |
|
|
409
|
-
| Decompress a compressed asset | `cpu({op:'decompress'})` / `cpu({op:'call'})` (run the ROM's own codec) |
|
|
455
|
+
| Decompress a compressed asset | `cpu({op:'decompress'})` / `cpu({op:'call', pure:true})` (run the ROM's own codec, interference-free) |
|
|
456
|
+
| Where does this on-screen graphic come from | `watch({on:'copy', start, end})` (all 14 — writer PC per VRAM write; Genesis DMA also via `watch({on:'dma'})`) |
|
|
410
457
|
| Re-inject edited bytes the game accepts | `romPatch({op:'makeStored'})` (verbatim-expand block) → `romPatch({op:'findFree'})` → `romPatch({op:'relocate'})` |
|
|
411
458
|
| Find the pointer that loads an asset | `romPatch({op:'findPointer', romOffset})` |
|
|
412
459
|
| FIND the unknown routine touching X | `watch({on:'range', start,end})` (all hits) / `watch({on:'pc'})` (coverage) |
|
|
@@ -215,7 +215,11 @@ What you can read:
|
|
|
215
215
|
registers.
|
|
216
216
|
- **`disasm({target:'rom'})`** and **`disasm({target:'references'})`** —
|
|
217
217
|
both anchor to the top of the bank (`$F000-$FFFF`) and label the vector
|
|
218
|
-
table (NMI / RESET / IRQ at `$FFFA`).
|
|
218
|
+
table (NMI / RESET / IRQ at `$FFFA`). On banked carts (F8 = 8 KB,
|
|
219
|
+
F6 = 16 KB, F4 = 32 KB) `references` scans EVERY 4 KB bank at `$F000`,
|
|
220
|
+
refs tagged `romBank`; `disasm({target:'project'})` likewise emits one
|
|
221
|
+
region per bank plus per-bank `BANKn` wrappers and a multi-area `.cfg`
|
|
222
|
+
blob for the native ca65/ld65 rebuild.
|
|
219
223
|
|
|
220
224
|
Memory regions for **`memory({op:'read'})`**:
|
|
221
225
|
|
|
@@ -159,3 +159,43 @@ snippet for one approach.
|
|
|
159
159
|
## "First build is slow but later ones are fast"
|
|
160
160
|
|
|
161
161
|
Expected. dasm cold-load is ~500ms. Steady-state builds < 100ms.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Pressing RIGHT also "presses" LEFT (or the player can't move at all)
|
|
165
|
+
|
|
166
|
+
The classic `LDA SWCHA / ASL / BCS … / ASL / BCS …` carry-chain only works if
|
|
167
|
+
NOTHING between the shifts touches A. The moment a branch body does
|
|
168
|
+
`LDA P_X` (a bounds check, a compare), the next `ASL` shifts your *position*
|
|
169
|
+
instead of SWCHA — and since positions are < $80, carry comes back clear and
|
|
170
|
+
the "other direction" fires too. Net effect: moves cancel, the sprite sticks
|
|
171
|
+
to one edge. **Re-load SWCHA and AND a single bit per direction instead:**
|
|
172
|
+
|
|
173
|
+
```asm
|
|
174
|
+
LDA SWCHA
|
|
175
|
+
AND #$80 ; bit7 = P0 Right (active LOW: 0 = pressed)
|
|
176
|
+
BNE .noRight
|
|
177
|
+
...move right (clobber A freely)...
|
|
178
|
+
.noRight:
|
|
179
|
+
LDA SWCHA
|
|
180
|
+
AND #$40 ; bit6 = P0 Left
|
|
181
|
+
BNE .noLeft
|
|
182
|
+
...
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Jump plays its sound but the player never leaves the ground
|
|
186
|
+
|
|
187
|
+
Signed-velocity clamps must check the SIGN first. An unsigned
|
|
188
|
+
`CMP #$F8 / BCS keep` "terminal velocity" clamp also catches every POSITIVE
|
|
189
|
+
(rising) velocity — +6 is less than $F8 unsigned — so the jump impulse is
|
|
190
|
+
instantly slammed to falling and the whole arc resolves inside one frame
|
|
191
|
+
(SFX plays, screen blips, no visible jump). Clamp only while falling:
|
|
192
|
+
|
|
193
|
+
```asm
|
|
194
|
+
LDA P_VY
|
|
195
|
+
BPL .vyok ; rising → terminal clamp doesn't apply
|
|
196
|
+
CMP #$F8
|
|
197
|
+
BCS .vyok ; -8..-1 → fine
|
|
198
|
+
LDA #$F8 ; clamp to -8
|
|
199
|
+
STA P_VY
|
|
200
|
+
.vyok:
|
|
201
|
+
```
|
|
@@ -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
|
|
|
@@ -237,10 +237,23 @@ read:
|
|
|
237
237
|
|
|
238
238
|
```c
|
|
239
239
|
uint8_t pad = ~SWCHA;
|
|
240
|
-
if (pad & JOY_UP) /* P1 up */
|
|
241
|
-
if (pad & JOY_DOWN) /* P1 down */
|
|
242
|
-
if (pad & JOY_LEFT) /* P1 left */
|
|
243
240
|
if (pad & JOY_RIGHT) /* P1 right */
|
|
241
|
+
if (pad & JOY_LEFT) /* P1 left */
|
|
242
|
+
if (pad & JOY_DOWN) /* P1 down */
|
|
243
|
+
if (pad & JOY_UP) /* P1 up */
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**The bit order is the #1 7800 input footgun.** From bit 7 down the P1 nibble
|
|
247
|
+
is **Right ($80), Left ($40), Down ($20), Up ($10)** — same as the 2600. Defining
|
|
248
|
+
`JOY_UP 0x80 … JOY_RIGHT 0x10` (the "reads naturally" order) is exactly
|
|
249
|
+
REVERSED, and the symptom is bizarre enough to misdiagnose: up/down steer
|
|
250
|
+
left/right and vice versa. Always:
|
|
251
|
+
|
|
252
|
+
```c
|
|
253
|
+
#define JOY_RIGHT 0x80
|
|
254
|
+
#define JOY_LEFT 0x40
|
|
255
|
+
#define JOY_DOWN 0x20
|
|
256
|
+
#define JOY_UP 0x10
|
|
244
257
|
```
|
|
245
258
|
|
|
246
259
|
Fire button on `INPT4` at `$0C`, also active low.
|
|
@@ -343,9 +356,17 @@ What you can read:
|
|
|
343
356
|
P / SP / PC) read from prosystem's `sally` globals.
|
|
344
357
|
- **`background({view:'renderState'})`** — the MARIA CTRL bits, DPP,
|
|
345
358
|
CHARBASE, and the current `dlistPtr`.
|
|
346
|
-
- **`disasm({target:'rom'})`**
|
|
347
|
-
|
|
348
|
-
|
|
359
|
+
- **`disasm({target:'rom'})`** — defaults to the top 16 KB
|
|
360
|
+
(`$C000-$FFFF`), where the reset vector lands.
|
|
361
|
+
- **`disasm({target:'references'})`** — scans the WHOLE cart: flat carts
|
|
362
|
+
(≤48 KB) in one pass at their top-of-space org, SuperGame banked carts
|
|
363
|
+
(>48 KB) per 16 KB bank (last bank fixed at `$C000`, others at `$8000`),
|
|
364
|
+
refs tagged `romBank`. A 128-byte `.a78` header is stripped automatically.
|
|
365
|
+
- **`disasm({target:'project'})`** — flat carts rebuild with one flat cc65
|
|
366
|
+
build; SuperGame carts get per-bank regions + NES-style glue (HEADER
|
|
367
|
+
segment with the original 128 header bytes, `BANKn` wrappers, multi-bank
|
|
368
|
+
`.cfg` via `linkerConfigPath`) — a one-call byte-identical
|
|
369
|
+
`build()` rebuild either way.
|
|
349
370
|
|
|
350
371
|
Memory regions for **`memory({op:'read'})`**:
|
|
351
372
|
|
|
@@ -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:
|
|
@@ -68,7 +68,10 @@ returns nothing.
|
|
|
68
68
|
`disasm({target:'project'})` route through the native binutils z80 `objdump` in
|
|
69
69
|
its `gbz80` machine (WASM, `-m gbz80`) — full CB-prefix coverage plus the
|
|
70
70
|
SM83-specific opcodes (`ld (hl+),a`, `ldh`, `reti`, `ld hl,sp+e8`). One z80-elf
|
|
71
|
-
binutils serves both plain Z80 (SMS/GG/MSX) and the GB CPU.
|
|
71
|
+
binutils serves both plain Z80 (SMS/GG/MSX) and the GB CPU. MBC-banked carts
|
|
72
|
+
(>32 KB) are scanned per 16 KB bank by `references` (bank 0 @ `$0000`, banks
|
|
73
|
+
1+ @ their `$4000` window; refs tagged `romBank`) and split per-bank by
|
|
74
|
+
`disasm({target:'project'})`.
|
|
72
75
|
|
|
73
76
|
## Five silent-failure footguns to know before you start (R26 + R27)
|
|
74
77
|
|
|
@@ -103,6 +106,18 @@ check these first. All five have shipped fixes in the bundled runtime
|
|
|
103
106
|
(volatile-safe by construction) or cast through `volatile uint8_t *`.
|
|
104
107
|
See `gb_runtime/lib/c/SDCC_GOTCHAS.md` § "Writes to VRAM" for detail.
|
|
105
108
|
|
|
109
|
+
3b. **OAM DMA goes FIRST after `wait_vblank()` — before any staging work.**
|
|
110
|
+
The vblank window is ~10 scanlines (~1140 cycles) and SDCC call overhead
|
|
111
|
+
is brutal: even a few dozen `oam_set()` CALLS before the flush push the
|
|
112
|
+
DMA out of vblank into active display, where it tears the sprites on one
|
|
113
|
+
FIXED scanline every frame (the "horizontal line a third of the way down"
|
|
114
|
+
glitch). The robust frame shape is: stage OAM/BG writes into RAM any time
|
|
115
|
+
during the frame, then `wait_vblank(); oam_dma_flush();` as the very first
|
|
116
|
+
thing, then a small bounded batch of BG map writes. One frame of sprite
|
|
117
|
+
latency is imperceptible. Also: statics belong at `dataLoc 0xC200` or
|
|
118
|
+
above (the project recipe sets this) so they can't collide with
|
|
119
|
+
`shadow_oam` at $C100.
|
|
120
|
+
|
|
106
121
|
4. **OAM DMA must run from HRAM.** During the ~160 µs OAM DMA window
|
|
107
122
|
the CPU can ONLY fetch from HRAM ($FF80-$FFFE). The bundled
|
|
108
123
|
`oam_dma_copy()` now installs a 9-byte stub at $FF80 and CALLs it;
|
|
@@ -265,9 +280,9 @@ names also resolve (east→A, west→B). So `input({op:'set', a: true})` presses
|
|
|
265
280
|
expected — unlike the genesis_plus_gx platforms (Genesis/SMS/GG), there's no
|
|
266
281
|
surprise here. (Same for **GBC** — it shares the gambatte core.)
|
|
267
282
|
|
|
268
|
-
## What `
|
|
283
|
+
## What `examples({op:'fork'})` copies into your project
|
|
269
284
|
|
|
270
|
-
`
|
|
285
|
+
`examples({op:'fork', example:"gb/..."|"gbc/...", name, path})` writes these files
|
|
271
286
|
into your project directory. **They're yours** — every byte that compiles
|
|
272
287
|
is in the repo. Edit, fork, replace; nothing is auto-injected at build time.
|
|
273
288
|
|
|
@@ -325,7 +340,7 @@ Most game patterns DON'T need any of this. Try the C path first.
|
|
|
325
340
|
|
|
326
341
|
## Horizontal scrolling (for side-scrollers)
|
|
327
342
|
|
|
328
|
-
The `platformer`
|
|
343
|
+
The `platformer` example is single-screen. To make it a side-scroller:
|
|
329
344
|
|
|
330
345
|
- **Hardware scroll:** write `SCX` (`$FF43`) each frame = camera X mod 256.
|
|
331
346
|
The BG is a 32×32 tile map (256×256 px) that wraps, so `SCX` alone scrolls
|
|
@@ -166,6 +166,102 @@ at $0150. If you see code in that window, either:
|
|
|
166
166
|
works; don't override it for GB/GBC unless you know what you're
|
|
167
167
|
doing.
|
|
168
168
|
|
|
169
|
+
## "BG map updates randomly don't stick" / a tile updates one frame late forever
|
|
170
|
+
|
|
171
|
+
The core (like real hardware mid-frame) DROPS writes to VRAM ($8000-$9FFF)
|
|
172
|
+
that land outside vblank while the LCD is on — silently. A game loop that
|
|
173
|
+
pokes the BG map "whenever the state changes" will have SOME of those pokes
|
|
174
|
+
land mid-frame and vanish: stale cells, a piece that visually lags the
|
|
175
|
+
logical grid, glitches that move around as code timing shifts.
|
|
176
|
+
|
|
177
|
+
The robust pattern (used by the bundled puzzle example games):
|
|
178
|
+
|
|
179
|
+
1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
|
|
180
|
+
pairs to a small RAM queue whenever game state changes a cell.
|
|
181
|
+
2. **FLUSH** — immediately after `wait_vblank()` (right after the OAM DMA),
|
|
182
|
+
drain the queue with pure writes. No scanning, no logic — vblank is only
|
|
183
|
+
~1140 cycles, so the flush must be writes only and bounded.
|
|
184
|
+
3. **Scrub** — repaint one or two rows per frame round-robin as insurance,
|
|
185
|
+
so any cell that ever got dropped self-heals within a second.
|
|
186
|
+
|
|
187
|
+
If you must write outside that structure, turn the LCD off first (only
|
|
188
|
+
acceptable during init/load screens — mid-game it flashes white).
|
|
189
|
+
|
|
190
|
+
## "BG map updates randomly don't stick" / a tile updates one frame late forever
|
|
191
|
+
|
|
192
|
+
The core (like real hardware mid-frame) DROPS writes to VRAM ($8000-$9FFF)
|
|
193
|
+
that land outside vblank while the LCD is on — silently. A game loop that
|
|
194
|
+
pokes the BG map "whenever the state changes" will have SOME of those pokes
|
|
195
|
+
land mid-frame and vanish: stale cells, a piece that visually lags the
|
|
196
|
+
logical grid, glitches that move around as code timing shifts.
|
|
197
|
+
|
|
198
|
+
The robust pattern (used by the bundled puzzle example games):
|
|
199
|
+
|
|
200
|
+
1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
|
|
201
|
+
pairs to a small RAM queue whenever game state changes a cell.
|
|
202
|
+
2. **FLUSH** — immediately after `wait_vblank()` (right after the OAM DMA),
|
|
203
|
+
drain the queue with pure writes. No scanning, no logic — vblank is only
|
|
204
|
+
~1140 cycles, so the flush must be writes only and bounded.
|
|
205
|
+
3. **Scrub** — repaint one or two rows per frame round-robin as insurance,
|
|
206
|
+
so any cell that ever got dropped self-heals within a second.
|
|
207
|
+
|
|
208
|
+
If you must write outside that structure, turn the LCD off first (only
|
|
209
|
+
acceptable during init/load screens — mid-game it flashes white).
|
|
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
|
+
|
|
169
265
|
## Debug recipes
|
|
170
266
|
|
|
171
267
|
A few high-leverage tools you might not know exist:
|
|
@@ -205,14 +301,13 @@ Boot order that always works for GBC:
|
|
|
205
301
|
}
|
|
206
302
|
```
|
|
207
303
|
|
|
208
|
-
Cribbed from `examples/gbc/templates/tile_engine.c` —
|
|
209
|
-
|
|
304
|
+
Cribbed from `examples/gbc/templates/tile_engine.c` — fork that
|
|
305
|
+
example into a fresh game with:
|
|
210
306
|
|
|
211
307
|
```js
|
|
212
|
-
|
|
213
|
-
op: '
|
|
214
|
-
|
|
215
|
-
template: "tile_engine",
|
|
308
|
+
examples({
|
|
309
|
+
op: 'fork',
|
|
310
|
+
example: "gbc/tile_engine",
|
|
216
311
|
name: "mygame",
|
|
217
312
|
path: "/abs/path/to/dir",
|
|
218
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
|
|