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.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -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
- ;; 76 bytes total: Nintendo logo (48) + title (16) + flags+checksums (12)
93
- .ds 0x4C
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
@@ -7,10 +7,13 @@
7
7
  //
8
8
  // WHY: most libretro GB cores (gambatte) refuse to load a ROM whose
9
9
  // Nintendo logo at $0104-$0133 doesn't match the canonical bytes or
10
- // whose header checksum at $014D doesn't validate. RGBDS's `rgbfix`
11
- // does this in the asm-build path; for SDCC-built C ROMs (which our
12
- // pipeline does NOT auto-patch every byte that compiles is yours),
13
- // this script does the same job.
10
+ // whose header checksum at $014D doesn't validate.
11
+ //
12
+ // NOTE: romdev's own build pipeline DOES auto-patch the header now (it
13
+ // runs a bundled rgbfix after every gb/gbc link — see the
14
+ // "rgbfix (auto header fix)" line in build logs), so you only need this
15
+ // script when rebuilding the project OUTSIDE romdev with stock SDCC and
16
+ // no RGBDS installed. It's what keeps the forked project self-contained.
14
17
  //
15
18
  // The bundled gb_crt0.s reserves $0100-$014F for the header window,
16
19
  // so the bytes patched in here land on actual cartridge-header
@@ -148,7 +151,17 @@ if (isCli) {
148
151
  const rom = new Uint8Array(readFileSync(inPath));
149
152
  // Auto-detect CGB based on file extension.
150
153
  const cgb = /\.gbc$/i.test(inPath) || /\.gbc$/i.test(outPath);
151
- patchGbHeader(rom, { cgb });
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 });
152
165
  writeFileSync(outPath, rom);
153
- 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` : ""})`);
154
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 scaffold-level sfx**, the libtonc runtime ships a minimal
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 scaffold sound API, so cross-platform game ports feel
136
- the same. All 5 GBA genre scaffolds (shmup/platformer/puzzle/sports/
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 scaffold does it; copy
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 scaffold (`tonc_hello`, `tonc_hello_sprite`, `shmup`,
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 a scaffold
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 `scaffold({op:'project'})`). The shape:
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
 
@@ -31,6 +31,18 @@ the same wall.
31
31
  cast through `volatile uint8_t *`. See `lib/c/SDCC_GOTCHAS.md`
32
32
  § "Writes to VRAM".
33
33
 
34
+ 3b. **OAM DMA goes FIRST after `wait_vblank()` — before any staging work.**
35
+ The vblank window is ~10 scanlines (~1140 cycles) and SDCC call overhead
36
+ is brutal: even a few dozen `oam_set()` CALLS before the flush push the
37
+ DMA out of vblank into active display, where it tears the sprites on one
38
+ FIXED scanline every frame (the "horizontal line a third of the way down"
39
+ glitch). The robust frame shape is: stage OAM/BG writes into RAM any time
40
+ during the frame, then `wait_vblank(); oam_dma_flush();` as the very first
41
+ thing, then a small bounded batch of BG map writes. One frame of sprite
42
+ latency is imperceptible. Also: statics belong at `dataLoc 0xC200` or
43
+ above (the project recipe sets this) so they can't collide with
44
+ `shadow_oam` at $C100.
45
+
34
46
  4. **OAM DMA must run from HRAM.** During the ~160 µs OAM DMA window
35
47
  the CPU can ONLY fetch from HRAM ($FF80-$FFFE). The bundled
36
48
  `oam_dma_copy()` installs a 9-byte stub at $FF80 and CALLs it; the
@@ -102,7 +114,7 @@ The CGB boot ROM checks header byte **`$0143`**:
102
114
  - `$80` → CGB-enhanced mode (color works, DMG-compat fallback)
103
115
  - `$C0` → CGB-only mode (refuses to boot on a DMG)
104
116
 
105
- **Every bundled GBC scaffold is built with `$0143 = $80`** — `build({output:'rom'})`
117
+ **Every bundled GBC example game is built with `$0143 = $80`** — `build({output:'rom'})`
106
118
  / `build({output:'run'})` set this automatically at build time when `platform:"gbc"`,
107
119
  so a freshly built `.gbc` boots in color with no extra step. (Build it as
108
120
  `platform:"gb"` instead and the flag stays `$00` → DMG green-shade mode,
@@ -185,12 +197,12 @@ inversion: `{a}`→A, `{b}`→B, `{start}`/`{select}`, plus the d-pad (spatial
185
197
  east→A, west→B). So `input({op:'set', a: true})` presses GBC A as expected — unlike
186
198
  the genesis_plus_gx platforms (Genesis/SMS/GG), there's no surprise here.
187
199
 
188
- ## Scaffolds
200
+ ## Example games
189
201
 
190
- All GB scaffolds (`shmup`, `platformer`, `puzzle`, `sports`, `racing`,
202
+ All GB example games (`shmup`, `platformer`, `puzzle`, `sports`, `racing`,
191
203
  `hello_sprite`, `tile_engine`) compile identically as GBC ROMs — the
192
204
  bundled GB runtime is already CGB-aware (writes OCPD/OCPS for color).
193
- The genre scaffolds inherit from GB via `TEMPLATES.gbc = TEMPLATES.gb`;
205
+ The genre examples inherit from GB via `TEMPLATES.gbc = TEMPLATES.gb`;
194
206
  the only differences at build time are:
195
207
 
196
208
  - ROM extension: `.gbc` (vs `.gb`)
@@ -120,6 +120,27 @@ in DMG mode. To switch a DMG ROM to CGB:
120
120
  2. Run `romPatch({op:'gbHeader', path:"out.gbc"})` — also fixes the global
121
121
  checksum that the boot ROM checks
122
122
 
123
+ ## "BG map updates randomly don't stick" / a tile updates one frame late forever
124
+
125
+ The core (like real hardware mid-frame) DROPS writes to VRAM ($8000-$9FFF)
126
+ that land outside vblank while the LCD is on — silently. A game loop that
127
+ pokes the BG map "whenever the state changes" will have SOME of those pokes
128
+ land mid-frame and vanish: stale cells, a piece that visually lags the
129
+ logical grid, glitches that move around as code timing shifts.
130
+
131
+ The robust pattern (used by the bundled puzzle example games):
132
+
133
+ 1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
134
+ pairs to a small RAM queue whenever game state changes a cell.
135
+ 2. **FLUSH** — immediately after `wait_vblank()` (right after the OAM DMA),
136
+ drain the queue with pure writes. No scanning, no logic — vblank is only
137
+ ~1140 cycles, so the flush must be writes only and bounded.
138
+ 3. **Scrub** — repaint one or two rows per frame round-robin as insurance,
139
+ so any cell that ever got dropped self-heals within a second.
140
+
141
+ If you must write outside that structure, turn the LCD off first (only
142
+ acceptable during init/load screens — mid-game it flashes white).
143
+
123
144
  ## "Sound is the same as DMG"
124
145
 
125
146
  That's correct — CGB has the **identical** 4-channel APU as DMG. The
@@ -128,7 +149,7 @@ sound channels or extra waveforms.
128
149
 
129
150
  ## "ROM size > 32 KB needed"
130
151
 
131
- The bundled GBC scaffolds all fit in 32 KB (single bank, no MBC).
152
+ The bundled GBC example games all fit in 32 KB (single bank, no MBC).
132
153
  For larger projects use an MBC (memory bank controller). MBC1 / MBC3
133
154
  work in gambatte; set the `$0147` cartridge type byte accordingly.
134
155
  romPatch({op:'gbHeader'}) doesn't set this — you write it from your asm/C.
@@ -138,11 +159,11 @@ romPatch({op:'gbHeader'}) doesn't set this — you write it from your asm/C.
138
159
  Default GBC speed is the same as DMG (~4 MHz Z80). Double-speed mode
139
160
  via KEY1 ($FF4D) doubles CPU but halves audio sample rate + breaks
140
161
  cycle-counted code. Most homebrew leaves it off; if you need the
141
- extra clocks, change the GB scaffold pattern to:
162
+ extra clocks, change the GB example pattern to:
142
163
 
143
164
  ```c
144
165
  KEY1 = 1; /* request speed switch */
145
166
  __asm__("stop"); /* arm the switch (compiler-specific syntax) */
146
167
  ```
147
168
 
148
- Not bundled in any scaffold — use only if you've measured a need.
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 scaffolds; everything below is in lockstep with
6
+ the color-aware example games; everything below is in lockstep with
7
7
  the GB tree.
8
8
 
9
9
  CGB-specific:
@@ -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 — `scaffold({op:'project', platform:"gb"|"gbc",
5
- template:"..."})` copies them into your project directory so the
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 `scaffold({op:'project'})`. Same logic, no MCP needed.
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
- ## Project templates
49
+ ## Forking an example
50
50
 
51
- Bootstrap a working game-loop skeleton with `scaffold({op:'project'})`:
51
+ Bootstrap a working game-loop skeleton by forking an example with `examples({op:'fork'})`:
52
52
 
53
53
  ```js
54
- scaffold({
55
- op: 'project',
56
- platform: "gbc",
57
- template: "tile_engine", // or "hello_sprite", or "default"
58
- name: "mygame",
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
 
@@ -0,0 +1,43 @@
1
+ /* AUTO-GENERATED by gen_font.py — 5x7 font, GB 2bpp, ink=value 3. */
2
+ #ifndef FONT_H
3
+ #define FONT_H
4
+ #define FONT_GLYPHS 36
5
+ static const uint8_t font_data[576] = {
6
+ 0x38, 0x38, 0x44, 0x44, 0x4C, 0x4C, 0x54, 0x54, 0x64, 0x64, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 0 */
7
+ 0x10, 0x10, 0x30, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x38, 0x00, 0x00, /* 1 */
8
+ 0x38, 0x38, 0x44, 0x44, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x20, 0x20, 0x7C, 0x7C, 0x00, 0x00, /* 2 */
9
+ 0x7C, 0x7C, 0x08, 0x08, 0x10, 0x10, 0x08, 0x08, 0x04, 0x04, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 3 */
10
+ 0x08, 0x08, 0x18, 0x18, 0x28, 0x28, 0x48, 0x48, 0x7C, 0x7C, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, /* 4 */
11
+ 0x7C, 0x7C, 0x40, 0x40, 0x78, 0x78, 0x04, 0x04, 0x04, 0x04, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 5 */
12
+ 0x18, 0x18, 0x20, 0x20, 0x40, 0x40, 0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 6 */
13
+ 0x7C, 0x7C, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, /* 7 */
14
+ 0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 8 */
15
+ 0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x3C, 0x3C, 0x04, 0x04, 0x08, 0x08, 0x30, 0x30, 0x00, 0x00, /* 9 */
16
+ 0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x7C, 0x7C, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* A */
17
+ 0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x78, 0x00, 0x00, /* B */
18
+ 0x38, 0x38, 0x44, 0x44, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* C */
19
+ 0x70, 0x70, 0x48, 0x48, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x48, 0x48, 0x70, 0x70, 0x00, 0x00, /* D */
20
+ 0x7C, 0x7C, 0x40, 0x40, 0x40, 0x40, 0x78, 0x78, 0x40, 0x40, 0x40, 0x40, 0x7C, 0x7C, 0x00, 0x00, /* E */
21
+ 0x7C, 0x7C, 0x40, 0x40, 0x40, 0x40, 0x78, 0x78, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, /* F */
22
+ 0x38, 0x38, 0x44, 0x44, 0x40, 0x40, 0x5C, 0x5C, 0x44, 0x44, 0x44, 0x44, 0x3C, 0x3C, 0x00, 0x00, /* G */
23
+ 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x7C, 0x7C, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* H */
24
+ 0x38, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x38, 0x00, 0x00, /* I */
25
+ 0x1C, 0x1C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x48, 0x48, 0x30, 0x30, 0x00, 0x00, /* J */
26
+ 0x44, 0x44, 0x48, 0x48, 0x50, 0x50, 0x60, 0x60, 0x50, 0x50, 0x48, 0x48, 0x44, 0x44, 0x00, 0x00, /* K */
27
+ 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7C, 0x7C, 0x00, 0x00, /* L */
28
+ 0x44, 0x44, 0x6C, 0x6C, 0x54, 0x54, 0x54, 0x54, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* M */
29
+ 0x44, 0x44, 0x44, 0x44, 0x64, 0x64, 0x54, 0x54, 0x4C, 0x4C, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* N */
30
+ 0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* O */
31
+ 0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x78, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, /* P */
32
+ 0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x54, 0x54, 0x48, 0x48, 0x34, 0x34, 0x00, 0x00, /* Q */
33
+ 0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x78, 0x50, 0x50, 0x48, 0x48, 0x44, 0x44, 0x00, 0x00, /* R */
34
+ 0x3C, 0x3C, 0x40, 0x40, 0x40, 0x40, 0x38, 0x38, 0x04, 0x04, 0x04, 0x04, 0x78, 0x78, 0x00, 0x00, /* S */
35
+ 0x7C, 0x7C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, /* T */
36
+ 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* U */
37
+ 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x28, 0x28, 0x10, 0x10, 0x00, 0x00, /* V */
38
+ 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x54, 0x54, 0x54, 0x54, 0x6C, 0x6C, 0x44, 0x44, 0x00, 0x00, /* W */
39
+ 0x44, 0x44, 0x44, 0x44, 0x28, 0x28, 0x10, 0x10, 0x28, 0x28, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* X */
40
+ 0x44, 0x44, 0x44, 0x44, 0x28, 0x28, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, /* Y */
41
+ 0x7C, 0x7C, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x20, 0x20, 0x40, 0x40, 0x7C, 0x7C, 0x00, 0x00, /* Z */
42
+ };
43
+ #endif
@@ -86,11 +86,34 @@
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
90
  .area _HEADERe (ABS)
91
91
  .org 0x0104
92
- ;; 76 bytes total: Nintendo logo (48) + title (16) + flags+checksums (12)
93
- .ds 0x4C
92
+ ;; Nintendo logo ($0104-$0133, 48 bytes) + title/manufacturer/CGB
93
+ ;; flag ($0134-$0143) + licensee/SGB ($0144-$0146): left as a gap —
94
+ ;; the post-link header fix (bundled rgbfix, or patch-header.js when
95
+ ;; rebuilding outside romdev) writes the canonical logo, the CGB
96
+ ;; flag, and both checksums.
97
+ .ds 0x43
98
+ ;; Cartridge TYPE + RAM size ($0147-$0149) are emitted EXPLICITLY so
99
+ ;; the post-link fix can preserve them (it reads these bytes and
100
+ ;; passes them through; unknown/garbage values fall back to ROM-only).
101
+ ;; This is the GB equivalent of the NES crt0's iNES BATTERY bit:
102
+ ;; declaring the cart in the boot file makes battery saves part of
103
+ ;; the project source, not a build flag someone has to remember.
104
+ ;;
105
+ ;; $0147 = $03 MBC1 + RAM + BATTERY → the core exposes the 8KB
106
+ ;; at $A000-$BFFF as persistent SAVE_RAM (.srm).
107
+ ;; Writes only stick after the $0A enable sequence —
108
+ ;; see the SRAM HARDWARE IDIOM in the puzzle example.
109
+ ;; $0148 = $00 32 KB ROM (2 banks — no banking needed)
110
+ ;; $0149 = $02 8 KB cart RAM (one bank at $A000)
111
+ .org 0x0147
112
+ .db 0x03 ; cart type: MBC1+RAM+BATTERY
113
+ .db 0x00 ; ROM size: 32 KB
114
+ .db 0x02 ; RAM size: 8 KB
115
+ ;; $014A-$014F (destination/licensee/version/checksums): host fills.
116
+ .ds 0x06
94
117
 
95
118
  ;; ─── init: real boot code, lives in _CODE starting at $0150 ────────
96
119
  .area _CODE
@@ -7,10 +7,13 @@
7
7
  //
8
8
  // WHY: most libretro GB cores (gambatte) refuse to load a ROM whose
9
9
  // Nintendo logo at $0104-$0133 doesn't match the canonical bytes or
10
- // whose header checksum at $014D doesn't validate. RGBDS's `rgbfix`
11
- // does this in the asm-build path; for SDCC-built C ROMs (which our
12
- // pipeline does NOT auto-patch every byte that compiles is yours),
13
- // this script does the same job.
10
+ // whose header checksum at $014D doesn't validate.
11
+ //
12
+ // NOTE: romdev's own build pipeline DOES auto-patch the header now (it
13
+ // runs a bundled rgbfix after every gb/gbc link — see the
14
+ // "rgbfix (auto header fix)" line in build logs), so you only need this
15
+ // script when rebuilding the project OUTSIDE romdev with stock SDCC and
16
+ // no RGBDS installed. It's what keeps the forked project self-contained.
14
17
  //
15
18
  // The bundled gb_crt0.s reserves $0100-$014F for the header window,
16
19
  // so the bytes patched in here land on actual cartridge-header
@@ -148,7 +151,17 @@ if (isCli) {
148
151
  const rom = new Uint8Array(readFileSync(inPath));
149
152
  // Auto-detect CGB based on file extension.
150
153
  const cgb = /\.gbc$/i.test(inPath) || /\.gbc$/i.test(outPath);
151
- patchGbHeader(rom, { cgb });
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 });
152
165
  writeFileSync(outPath, rom);
153
- 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` : ""})`);
154
167
  }
@@ -50,9 +50,13 @@ memory({op:'read', region:'system_ram', offset: sym.ramOffset, length:2})
50
50
  - **`static` file-local globals resolve too** (SGDK emits per-symbol sections).
51
51
  A non-`static` global that's never *read* can be optimised away at -O2 — mark
52
52
  game-state vars you inspect `volatile` (you want that anyway).
53
- - **Genesis WRAM is host-LE word-byte-swapped** in gpgx, so a 16-bit value reads
54
- with its two bytes swapped at the offset (0x1234 bytes `34 12`). Read the
55
- word and account for it, or read single bytes.
53
+ - **WRAM (`system_ram`) is normalized to CPU byte order** offset X is the
54
+ byte the 68k sees at $FF0000+X, words read big-endian as expected, and
55
+ offsets line up with disassembly addresses and cheat-DB maps. (gpgx stores
56
+ work RAM host-LE word-swapped internally; the host un-swaps it. Before
57
+ 0.28.0 the raw swapped layout leaked through — value-search/diff loops were
58
+ self-consistent, but any offset cross-referenced against a `move.b $FFxxxx`
59
+ in a disassembly was off-by-XOR-1.)
56
60
  - **PC → which function?** `symbols({op:'addr', pc, symbolsText: b.mapText})` maps
57
61
  a live `cpu({op:'read'}).pc` to the enclosing C function.
58
62
 
@@ -174,7 +178,7 @@ software mistake, not a hardware limit:
174
178
  > burst or a per-frame DMA), you overrun vblank, drop frames, and the
175
179
  > scroll judders. **Paint the planes ONCE at setup; the loop only nudges
176
180
  > scroll registers and re-stages sprites.** Use the
177
- > `template:"two_plane_parallax"` scaffold as the known-good shape.
181
+ > `two_plane_parallax` example as the known-good shape.
178
182
 
179
183
  ### Hardware scroll, the whole loop
180
184
 
@@ -237,7 +241,7 @@ if (newTileCol != lastTileCol) {
237
241
  ```
238
242
 
239
243
  That's ~28 tile writes per 8 px of travel, not a 1792-cell plane redraw.
240
- The `template:"platformer"` scaffold scrolls within one plane (no
244
+ The `platformer` example scrolls within one plane (no
241
245
  streaming); add the column-stream above to go wider. (Real Sonic also
242
246
  splits the screen with H-blank raster effects for independent strips —
243
247
  that's an IRQ/raster topic, see the `asm` template.)
@@ -427,10 +431,40 @@ Video is deeply readable; the FM audio chip is only partially exposed:
427
431
  `getPsgState` decodes the SN76489 (3 tone + 1 noise channels).
428
432
  - **Memory regions:** `memory({op:'read'})` exposes CRAM, VSRAM, VDP_REGS,
429
433
  Z80_RAM (the sound CPU's RAM), M68K work RAM, YM2612, PSG, and VRAM.
430
- Remember the gpgx byte-swap quirk: VRAM and WRAM read host-LE
434
+ Remember the gpgx byte-swap quirk for VRAM: it reads host-LE
431
435
  word-byte-swapped (a 16-bit value's two bytes are swapped at the offset)
432
- account for it or read single bytes (see "Reading your C globals
433
- headlessly").
436
+ use tiles({op:'pixels'}) to decode in render order. M68K work RAM
437
+ (`system_ram`) is NOT affected: it's normalized to CPU byte order (see
438
+ "Reading your C globals headlessly").
439
+
440
+ ## Break-instant truth: registersAtHit + pure calls (0.28.0)
441
+
442
+ gpgx schedules its CPUs per scanline, so a `breakpoint` hit mid-frame used to
443
+ leave the LIVE register file hundreds of instructions past the hit by the time
444
+ you could read it — chasing pointer registers read that way burned a real
445
+ session for ~2h. Fixed two ways:
446
+
447
+ - **`registersAtHit`** — `breakpoint({on:'pc'|'write'|'read'})` hits now carry
448
+ the FULL register file (d0-d7/a0-a7/pc/sr/sp) frozen by the core at the hit
449
+ instant. Use it, never a follow-up `cpu({op:'read'})`. The reported `pc` for
450
+ write/read hits is the EXECUTING instruction's first byte (pre-0.28.0 it was
451
+ the post-prefetch PC — one instruction late). On a pc-break the 68k also
452
+ stays FROZEN for the rest of the frame, so even live reads agree.
453
+ - **`cpu({op:'call', pure:true})`** — steps ONLY the 68k: no VDP lines, no
454
+ Z80, no interrupts raised. Without it, a driven routine that spans frames
455
+ runs the game's own VBlank logic concurrently — which can stomp the output
456
+ buffer you're capturing (a real session diffed a CORRECT codec
457
+ reimplementation against that poisoned "ground truth" for hours). Prefer
458
+ `pure:true` for every decompressor/codec call; non-pure results carry a ⚠
459
+ caveat when frame logic ran. (SMS/GG get the same via the shared core; the
460
+ OTHER platforms get the same guarantee via interrupt blocking —
461
+ `pureMode:'irq-blocked'` — so the technique transfers everywhere.)
462
+ - **`watch({on:'copy'})`** — the CPU-port complement of `watch({on:'dma'})`:
463
+ logs every data-port write landing in a VRAM window with the executing
464
+ instruction's PC. Use `dma` when the upload is DMA'd (most Genesis
465
+ graphics), `copy` when the game pokes the data port directly (the
466
+ "video_ram writes don't reach the renderer" class of confusion — `copy`
467
+ shows you who's writing and where).
434
468
 
435
469
  ## ROM layout
436
470
 
@@ -447,7 +481,7 @@ build pipeline computes the checksum on link.
447
481
 
448
482
  ## Where the SDK lives (and how to read it)
449
483
 
450
- `scaffold({op:'project', platform:"genesis"})` ships the full SGDK include
484
+ `examples({op:'fork'})` (any Genesis example) ships the full SGDK include
451
485
  tree into the new project at `vendor/sgdk/`. So when your code does
452
486
  `#include <genesis.h>`, those headers come from
453
487
  `vendor/sgdk/include/`:
@@ -239,8 +239,8 @@ Genesis scrolls in HARDWARE — moving the world is two register writes
239
239
  plane every frame (a `VDP_fillTileMapRect` / `VDP_loadTileMap` / big
240
240
  `DMA_*` each frame), you overrun the vblank DMA budget and drop frames →
241
241
  judder. Fix: paint the planes ONCE at setup; the loop only nudges scroll
242
- registers + re-stages sprites. The `template:"two_plane_parallax"`
243
- scaffold is the known-good shape.
242
+ registers + re-stages sprites. The `two_plane_parallax`
243
+ example is the known-good shape.
244
244
 
245
245
  Diagnose it without guessing (no core rebuild):
246
246
 
@@ -32,6 +32,42 @@ void sfx_noise(u8 length_frames) {
32
32
  sfx_remaining[3] = length_frames;
33
33
  }
34
34
 
35
+ /* ── background music: a 16-step melody loop on PSG channel 2 ───────
36
+ * Ticked from sfx_update(), so every scaffold that already calls
37
+ * sfx_init() + sfx_update() gets continuous music for free ("no sound"
38
+ * was the #1 playtest complaint — a lone 6-frame blip on a rare event
39
+ * reads as silence). sfx_music(0) turns it off. SFX own channels 0-1 +
40
+ * noise, so effects always cut through. */
41
+ static const u16 music_hz[16] = {
42
+ 262, 330, 392, 523, 392, 330, 262, 0, /* C4 E4 G4 C5 G4 E4 C4 - */
43
+ 220, 262, 330, 440, 330, 262, 220, 0, /* A3 C4 E4 A4 E4 C4 A3 - */
44
+ };
45
+ static u8 music_enabled = 1;
46
+ static u8 music_step, music_timer;
47
+
48
+ void sfx_music(u8 on) {
49
+ music_enabled = on;
50
+ music_step = 0;
51
+ music_timer = 0;
52
+ if (!on) PSG_setEnvelope(2, PSG_ENVELOPE_MIN);
53
+ }
54
+
55
+ static void music_tick(void) {
56
+ if (!music_enabled) return;
57
+ if (music_timer == 0) {
58
+ u16 hz = music_hz[music_step & 15];
59
+ if (hz) {
60
+ PSG_setFrequency(2, hz);
61
+ PSG_setEnvelope(2, 5); /* moderate, under the SFX */
62
+ } else {
63
+ PSG_setEnvelope(2, PSG_ENVELOPE_MIN);
64
+ }
65
+ music_step++;
66
+ }
67
+ music_timer++;
68
+ if (music_timer >= 9) music_timer = 0; /* ~6.6 notes/sec */
69
+ }
70
+
35
71
  void sfx_update(void) {
36
72
  for (u8 i = 0; i < 4; i++) {
37
73
  if (sfx_remaining[i] > 0) {
@@ -41,6 +77,7 @@ void sfx_update(void) {
41
77
  }
42
78
  }
43
79
  }
80
+ music_tick();
44
81
  }
45
82
 
46
83
  void sfx_off(void) {
@@ -42,6 +42,7 @@ void sfx_noise(u8 length_frames);
42
42
  * decrement the auto-silence countdown. Without this, notes never
43
43
  * stop ringing. */
44
44
  void sfx_update(void);
45
+ void sfx_music(u8 on); /* background melody loop on PSG ch2 — ON by default; 0 = off */
45
46
 
46
47
  /* Power down all PSG channels immediately. */
47
48
  void sfx_off(void);