romdevtools 0.26.0 → 0.28.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 (144) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +322 -3
  3. package/README.md +1 -1
  4. package/examples/README.md +1 -1
  5. package/examples/atari2600/templates/platformer.asm +18 -9
  6. package/examples/atari2600/templates/racing.asm +25 -4
  7. package/examples/atari2600/templates/shmup.asm +30 -5
  8. package/examples/atari2600/templates/sports.asm +41 -9
  9. package/examples/atari7800/templates/hello_sprite.c +8 -4
  10. package/examples/atari7800/templates/platformer.c +12 -8
  11. package/examples/atari7800/templates/puzzle.c +7 -4
  12. package/examples/atari7800/templates/racing.c +5 -2
  13. package/examples/atari7800/templates/shmup.c +8 -4
  14. package/examples/atari7800/templates/sports.c +6 -3
  15. package/examples/c64/templates/platformer.c +28 -24
  16. package/examples/c64/templates/puzzle.c +77 -16
  17. package/examples/c64/templates/racing.c +9 -0
  18. package/examples/c64/templates/shmup.c +13 -1
  19. package/examples/c64/templates/sports.c +9 -4
  20. package/examples/gb/templates/platformer.c +6 -2
  21. package/examples/gb/templates/puzzle.c +279 -101
  22. package/examples/gb/templates/racing.c +13 -1
  23. package/examples/gb/templates/shmup.c +13 -1
  24. package/examples/gb/templates/sports.c +9 -3
  25. package/examples/gba/templates/platformer.c +7 -13
  26. package/examples/gba/templates/puzzle.c +93 -15
  27. package/examples/gba/templates/racing.c +13 -1
  28. package/examples/gba/templates/shmup.c +13 -1
  29. package/examples/gba/templates/sports.c +17 -5
  30. package/examples/gbc/templates/platformer.c +6 -2
  31. package/examples/gbc/templates/puzzle.c +878 -178
  32. package/examples/gbc/templates/racing.c +13 -1
  33. package/examples/gbc/templates/shmup.c +13 -1
  34. package/examples/gbc/templates/sports.c +9 -3
  35. package/examples/genesis/templates/puzzle.c +76 -15
  36. package/examples/genesis/templates/racing.c +13 -1
  37. package/examples/genesis/templates/shmup_2p.c +13 -1
  38. package/examples/gg/templates/platformer.c +4 -0
  39. package/examples/gg/templates/puzzle.c +80 -14
  40. package/examples/gg/templates/racing.c +17 -1
  41. package/examples/gg/templates/shmup.c +17 -1
  42. package/examples/gg/templates/sports.c +4 -0
  43. package/examples/lynx/templates/platformer.c +25 -6
  44. package/examples/lynx/templates/puzzle.c +77 -14
  45. package/examples/lynx/templates/shmup.c +13 -1
  46. package/examples/lynx/templates/sports.c +5 -2
  47. package/examples/msx/platformer/main.c +2 -0
  48. package/examples/msx/puzzle/main.c +78 -15
  49. package/examples/msx/racing/main.c +1 -0
  50. package/examples/msx/shmup/main.c +1 -0
  51. package/examples/msx/sports/main.c +3 -2
  52. package/examples/nes/templates/platformer.c +11 -3
  53. package/examples/nes/templates/puzzle.c +81 -21
  54. package/examples/nes/templates/racing.c +15 -1
  55. package/examples/nes/templates/shmup.c +1 -0
  56. package/examples/nes/templates/sports.c +1 -0
  57. package/examples/pce/platformer/main.c +3 -1
  58. package/examples/pce/puzzle/main.c +78 -12
  59. package/examples/pce/racing/main.c +1 -0
  60. package/examples/pce/shmup/main.c +5 -4
  61. package/examples/pce/sports/main.c +4 -3
  62. package/examples/sms/templates/platformer.c +4 -0
  63. package/examples/sms/templates/puzzle.c +80 -14
  64. package/examples/sms/templates/racing.c +17 -1
  65. package/examples/sms/templates/shmup.c +17 -1
  66. package/examples/sms/templates/shmup_2p.c +17 -1
  67. package/examples/sms/templates/sports.c +4 -0
  68. package/examples/snes/templates/platformer.c +32 -15
  69. package/examples/snes/templates/puzzle.c +84 -16
  70. package/examples/snes/templates/racing.c +20 -1
  71. package/examples/snes/templates/shmup.c +20 -2
  72. package/examples/snes/templates/sports.c +7 -0
  73. package/package.json +12 -12
  74. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  75. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  76. package/src/cores/wasm/fceumm_libretro.js +1 -1
  77. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  78. package/src/cores/wasm/gambatte_libretro.js +1 -1
  79. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  80. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  81. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  82. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  83. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  84. package/src/cores/wasm/handy_libretro.js +1 -1
  85. package/src/cores/wasm/handy_libretro.wasm +0 -0
  86. package/src/cores/wasm/mgba_libretro.js +1 -1
  87. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  88. package/src/cores/wasm/prosystem_libretro.js +1 -1
  89. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  90. package/src/cores/wasm/snes9x_libretro.js +1 -1
  91. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  92. package/src/cores/wasm/stella2014_libretro.js +1 -1
  93. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  94. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  95. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  96. package/src/host/LibretroHost.js +245 -10
  97. package/src/mcp/server.js +6 -0
  98. package/src/mcp/tools/disasm-rebuild.js +315 -65
  99. package/src/mcp/tools/disasm.js +149 -28
  100. package/src/mcp/tools/find-references.js +216 -51
  101. package/src/mcp/tools/frame.js +11 -4
  102. package/src/mcp/tools/index.js +15 -1
  103. package/src/mcp/tools/input.js +26 -3
  104. package/src/mcp/tools/memory.js +208 -39
  105. package/src/mcp/tools/playtest.js +56 -4
  106. package/src/mcp/tools/project.js +35 -9
  107. package/src/mcp/tools/toolchain.js +43 -10
  108. package/src/mcp/tools/watch-memory.js +172 -25
  109. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  110. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  111. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  112. package/src/platforms/atari7800/MENTAL_MODEL.md +27 -6
  113. package/src/platforms/gb/MENTAL_MODEL.md +16 -1
  114. package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
  115. package/src/platforms/gb/lib/c/patch-header.js +7 -4
  116. package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
  117. package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
  118. package/src/platforms/gbc/lib/c/font.h +43 -0
  119. package/src/platforms/gbc/lib/c/patch-header.js +7 -4
  120. package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
  121. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  122. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  123. package/src/platforms/gg/TROUBLESHOOTING.md +13 -17
  124. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  125. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  126. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  127. package/src/platforms/msx/MENTAL_MODEL.md +6 -0
  128. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  129. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  130. package/src/platforms/msx/lib/c/msx_hw.h +2 -0
  131. package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
  132. package/src/platforms/nes/MENTAL_MODEL.md +10 -3
  133. package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
  134. package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
  135. package/src/platforms/pce/MENTAL_MODEL.md +9 -0
  136. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  137. package/src/platforms/pce/lib/c/pce_hw.h +2 -1
  138. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  139. package/src/platforms/sms/MENTAL_MODEL.md +5 -0
  140. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  141. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  142. package/src/platforms/snes/MENTAL_MODEL.md +5 -0
  143. package/src/playtest/playtest.js +73 -3
  144. package/src/toolchains/index.js +37 -8
@@ -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'})`** and **`disasm({target:'references'})`**
347
- both default to the top 16 KB (`$C000-$FFFF`), where the reset vector
348
- lands.
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
 
@@ -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;
@@ -166,6 +166,48 @@ 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 scaffolds):
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 scaffolds):
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
+
169
211
  ## Debug recipes
170
212
 
171
213
  A few high-leverage tools you might not know exist:
@@ -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 scaffold 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
@@ -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
@@ -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 scaffolds):
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
@@ -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
@@ -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 scaffold 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
@@ -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
 
@@ -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
 
@@ -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);
@@ -92,20 +92,16 @@ the P2 read + force `p2 = 0` so the AI fallback always engages.
92
92
  The bundled GG `sports.c` already does this — copy that pattern when
93
93
  porting other SMS multiplayer code.
94
94
 
95
- ## "Build errors mention 'TMR SEGA' or ROM header"
96
-
97
- Same magic as SMS gpgx accepts headerless ROMs fine for development.
98
- For real-hardware ROM-burning include a header at $7FF0:
99
-
100
- ```
101
- db "TMR SEGA"
102
- dw 0 ; reserved
103
- dw 0 ; checksum (gpgx ignores)
104
- db 0x00, 0x00, 0x00 ; product code BCD
105
- db 0x00 ; product code high + version
106
- db 0x40 ; region (0x40 = GG)
107
- db 0x4C ; ROM size (0x4C = 32 KB)
108
- ```
109
-
110
- The bundled scaffolds build without a header — sufficient for the
111
- emulator-driven workflow. Add one before shipping to a cartridge.
95
+ ## "TMR SEGA" header / ROM boots in the wrong video mode
96
+
97
+ The build pipeline now stamps the 16-byte header at `$7FF0` automatically
98
+ ("TMR SEGA" + checksum + the region/size byte at `$7FFF`) and pads every
99
+ image to 32 KB — you never hand-write it for romdev builds.
100
+
101
+ The byte that matters is `$7FFF`: **high nibble = region, low nibble = ROM
102
+ size**. romdev writes `$7C` (GG international, 32 KB) on `.gg` builds.
103
+ If you patch a ROM by hand and leave an SMS region nibble there (`$4C` =
104
+ SMS export), gpgx boots the `.gg` file in **SMS compatibility mode** —
105
+ 256×192 timing, SMS palette depth and everything renders dark and
106
+ mis-cropped even though your code is fine. Check `$7FFF` first when a GG
107
+ ROM suddenly looks like an SMS ROM.
@@ -37,10 +37,13 @@
37
37
  ;; ─── Reset vector at $0000 ────────────────────────────────────────
38
38
  .area _HEADER (ABS)
39
39
  .org 0x0000
40
+ ;; ONLY 8 BYTES fit before the RST $08 vector. The old block here
41
+ ;; (di/im 1/ld sp/jp = 9 bytes) overflowed into .org 0x0008, whose
42
+ ;; `ret` stomped the jp's high target byte -> boot jumped into
43
+ ;; garbage. di+im 1+jp = 6 bytes; SP setup moved to _boot below.
40
44
  di ; interrupts off until we're ready
41
45
  im 1 ; mode 1 — IRQs jump to $0038
42
- ld sp, #0xDFF0 ; GG/SMS stack (top of WRAM minus 16)
43
- jp gsinit ; skip the interrupt vector table
46
+ jp _boot ; continue past the vector table
44
47
 
45
48
  ;; ─── RST handlers (default = return) ──────────────────────────────
46
49
  .org 0x0008
@@ -80,6 +83,15 @@
80
83
  .org 0x0066
81
84
  retn
82
85
 
86
+ ;; ─── Boot continuation (right after the NMI vector) ───────────────
87
+ ;; SP first, then the C runtime init. Lives in the ABS header area so
88
+ ;; it exists at a known address regardless of where _CODE is linked
89
+ ;; (_CODE must start at >= $0100 so it can't overwrite this table).
90
+ .org 0x0068
91
+ _boot:
92
+ ld sp, #0xDFF0 ; stack at top of WRAM minus 16
93
+ jp gsinit
94
+
83
95
  ;; ─── crt0 body ────────────────────────────────────────────────────
84
96
  ;; Standard SDCC pattern: jump to a code area, run initializers, then
85
97
  ;; call main. The initializer area is filled by sdcc when it sees
@@ -103,21 +103,57 @@ static void sfx_flush_pending(void) {
103
103
  POKE(VOICE_BASE(i) + 1, 0x80); /* feedback off */
104
104
  POKE(VOICE_BASE(i) + 4, sfx_pending_period[i]);
105
105
  POKE(VOICE_BASE(i) + 5, 0x18); /* RELOAD + COUNT + 16us clock */
106
- POKE(VOICE_BASE(i) + 0, 64); /* volume */
106
+ POKE(VOICE_BASE(i) + 0, 100); /* volume (was 64 — read as near-silent on hardware) */
107
107
  } else if (sfx_pending_kind[i] == 2) {
108
108
  /* Noise on voice 3. */
109
109
  POKE(VOICE_BASE(i) + 7, 0x01); /* 12-bit LFSR */
110
110
  POKE(VOICE_BASE(i) + 1, 0x95); /* classic noise feedback */
111
111
  POKE(VOICE_BASE(i) + 4, 40);
112
112
  POKE(VOICE_BASE(i) + 5, 0x18);
113
- POKE(VOICE_BASE(i) + 0, 64);
113
+ POKE(VOICE_BASE(i) + 0, 100);
114
114
  }
115
115
  sfx_pending_kind[i] = 0;
116
116
  }
117
117
  }
118
118
 
119
+ /* ── background music: 16-step melody loop on voice 1 ───────────────
120
+ * Ticked from sfx_update() through the SAME staged-write path (R57),
121
+ * so every scaffold that already calls sfx_init() + sfx_update() gets
122
+ * continuous music for free — "no sound at all" was the Lynx playtest
123
+ * verdict. sfx_music(0) turns it off. SFX use voice 0 (+ noise on 3).
124
+ * MIKEY period at the 16us clock: freq ~= 31250 / period. */
125
+ static const uint8_t music_period[16] = {
126
+ 119, 95, 80, 60, 80, 95, 119, 0, /* C4 E4 G4 C5 G4 E4 C4 - */
127
+ 142, 119, 95, 71, 95, 119, 142, 0, /* A3 C4 E4 A4 E4 C4 A3 - */
128
+ };
129
+ static uint8_t music_enabled = 1;
130
+ static uint8_t music_step, music_timer;
131
+
132
+ void sfx_music(uint8_t on) {
133
+ music_enabled = on;
134
+ music_step = 0;
135
+ music_timer = 0;
136
+ }
137
+
138
+ static void music_tick(void) {
139
+ uint8_t p;
140
+ if (!music_enabled) return;
141
+ if (music_timer == 0) {
142
+ p = music_period[music_step & 15];
143
+ if (p) {
144
+ sfx_pending_kind[1] = 1; /* staged like any tone (R57-safe) */
145
+ sfx_pending_period[1] = p;
146
+ sfx_remaining[1] = 8; /* hold 8 of 9 frames — articulated */
147
+ }
148
+ music_step++;
149
+ }
150
+ music_timer++;
151
+ if (music_timer >= 9) music_timer = 0;
152
+ }
153
+
119
154
  void sfx_update(void) {
120
155
  uint8_t i;
156
+ music_tick();
121
157
  /* R57: flush any sfx_tone/sfx_noise requests from THIS frame. The
122
158
  * caller is expected to have just returned from tgi_updatedisplay
123
159
  * (or wait_vblank), so the synchronous timer-event sweep that handy
@@ -54,6 +54,7 @@ void sfx_init(void);
54
54
  void sfx_tone(uint8_t channel, uint8_t period, uint8_t length_frames);
55
55
  void sfx_noise(uint8_t length_frames);
56
56
  void sfx_update(void);
57
+ void sfx_music(uint8_t on); /* background melody on voice 1 — ON by default; 0 = off */
57
58
  void sfx_off(void);
58
59
 
59
60
  #endif
@@ -117,6 +117,12 @@ exactly this.
117
117
  generator + the envelope (period + shape bits).
118
118
  - `memory({op:'read'})` regions: `msx_vram`, `msx_vdp_regs`, `msx_vdp_status`,
119
119
  `msx_palette`, `msx_cpu_regs`, `msx_psg_regs`, plus `system_ram` (work RAM).
120
+ - `disasm({target:'rom'|'references'|'project'})` — native binutils z80
121
+ `objdump`. MegaROMs (>32 KB) are handled per 16 KB bank: `references` scans
122
+ bank 0 at `$4000` (after the "AB" header) and banks 1+ at `$8000` (an
123
+ assumed ASCII16-style window), refs tagged `romBank`;
124
+ `disasm({target:'project'})` splits the header into its own data region and
125
+ emits a bank-by-bank native rebuild recipe in `BUILD.md`.
120
126
 
121
127
  ## MCP debug & inspection tooling
122
128
 
@@ -66,3 +66,24 @@ fixed hardware colors — you choose indices, not RGB.
66
66
  The build worker pool can transiently fail. Re-run the build. If it fails
67
67
  consistently, read the `log` — SDCC's C89 parser errors are terse; common causes
68
68
  are `//` comments, mid-block declarations, or file-scope inline asm (see above).
69
+
70
+
71
+ ## PSG writes get eaten — sound code "runs" but the chip stays silent
72
+
73
+ The BIOS KEYINT interrupt fires every frame and reads PSG register 14 (the
74
+ joystick row) — and it CLOBBERS the PSGADDR latch. If an interrupt lands
75
+ between your `PSGADDR = n` and the matching `PSGWRITE`, your byte goes into
76
+ R14 instead of the register you selected. Symptom: the mixer looks right but
77
+ periods/volumes stay 0 — total silence even though your code clearly ran.
78
+
79
+ **Rule: wrap every PSGADDR/PSGWRITE sequence in `__asm__("di")` /
80
+ `__asm__("ei")`.** The bundled `msx_psg_tone`/`msx_psg_off` (and the music
81
+ ticker) already do this; copy the pattern for any direct PSG access you write.
82
+
83
+ ## A `static x = 5;` boots as 0 (historical — fixed in the bundled crt0)
84
+
85
+ The old `msx_crt0.s` placed the SDCC `_INITIALIZER` area in RAM, so the boot
86
+ copy duplicated uninitialised RAM onto itself: every value-initialised static
87
+ read 0 and BSS was never zeroed. The bundled crt0 has been fixed (ROM-placed
88
+ `_INITIALIZER` + a BSS-zero loop). If a project scaffolded before 2026-06-09
89
+ shows ghost zeros, refresh its `msx_crt0.s` from a new scaffold.
@@ -27,6 +27,8 @@
27
27
  .globl _main
28
28
  .globl l__INITIALIZER
29
29
  .globl s__INITIALIZER
30
+ .globl s__DATA
31
+ .globl l__DATA
30
32
  .globl s__INITIALIZED
31
33
 
32
34
  ;; ─── Cartridge ROM header at $4000 ────────────────────────────────
@@ -44,7 +46,15 @@
44
46
  ;; ─── crt0 body ────────────────────────────────────────────────────
45
47
  ;; Standard SDCC area order so the linker fills _GSINIT with the global
46
48
  ;; initializer fragments sdcc emits, then _GSFINAL.
49
+ ;; AREA ORDERING IS LOAD-BEARING (same bug class fixed in the SMS/GG
50
+ ;; crt0s 2026-06-08): `_INITIALIZER` (the ROM image of every value-
51
+ ;; initialised static) MUST be declared in the ROM group — otherwise
52
+ ;; sdld places it in RAM after `_INITIALIZED` and the init copy below
53
+ ;; copies uninitialised RAM onto itself, so every `static x = N;`
54
+ ;; boots as 0. On MSX that silenced ALL scaffold audio (the PSG
55
+ ;; music/sfx state booted zeroed) among other ghosts.
47
56
  .area _HOME
57
+ .area _INITIALIZER
48
58
  .area _CODE
49
59
  .area _GSINIT
50
60
  .area _GSFINAL
@@ -59,6 +69,23 @@
59
69
 
60
70
  ;; INIT entry — the BIOS CALLs here with interrupts on and a valid stack.
61
71
  init:
72
+ ;; ── Zero the BSS segment (`_DATA`) ── every uninitialised static
73
+ ;; must read back 0 at boot (power-on RAM is garbage).
74
+ ld bc, #l__DATA
75
+ ld a, b
76
+ or a, c
77
+ jr Z, bss_done
78
+ ld hl, #s__DATA
79
+ ld (hl), #0x00
80
+ ld d, h
81
+ ld e, l
82
+ inc de
83
+ dec bc
84
+ ld a, b
85
+ or a, c
86
+ jr Z, bss_done
87
+ ldir
88
+ bss_done:
62
89
  ;; Copy initialized-data image from ROM to RAM (SDCC global inits).
63
90
  ld bc, #l__INITIALIZER
64
91
  ld a, b
@@ -87,6 +87,8 @@ void msx_clear_sprites(void);
87
87
  void msx_vblank_wait(void);
88
88
  uint8_t msx_read_joystick(uint8_t stick);
89
89
  void msx_psg_tone(uint8_t chan, uint16_t period, uint8_t vol);
90
+ void msx_music(uint8_t on); /* background melody on channel C — ON by default; 0 = off */
91
+ void msx_music_tick(void); /* call once per frame (scaffolds do) */
90
92
  void msx_psg_off(uint8_t chan);
91
93
 
92
94
  #endif /* MSX_HW_H */