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
@@ -14,7 +14,7 @@ $E000-$FFFB Work RAM mirror
14
14
  $FFFC-$FFFF Mapper control registers
15
15
  ```
16
16
 
17
- Most 32 KB scaffolds fit in banks 0+1 and never touch the mapper.
17
+ Most 32 KB example games fit in banks 0+1 and never touch the mapper.
18
18
 
19
19
  ## VDP (display)
20
20
 
@@ -36,7 +36,7 @@ GG VDP = SMS VDP in Mode 4, smaller visible viewport.
36
36
  OR draw the text via the BG name table (no per-line limit).
37
37
 
38
38
  **Always render gameplay content inside (48, 24)..(207, 167)** so it's
39
- visible on real hardware. The bundled scaffolds work without this
39
+ visible on real hardware. The bundled example games work without this
40
40
  because gpgx shows the full framebuffer.
41
41
 
42
42
  ### Sprite coords are hardware-space, NOT visible-space
@@ -78,9 +78,9 @@ on its own anymore.
78
78
  `gg_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
79
79
  sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
80
80
  from `$2000-$3FFF`, in their **own bank** separate from BG tiles at
81
- $0000. This is the baseline because every bundled scaffold uploads
81
+ $0000. This is the baseline because every bundled example uploads
82
82
  its sprite tiles to `$2000` (`gg_load_tiles(0x2000, …)`) — the
83
- default and the scaffolds match, so sprites Just Show Up.
83
+ default and the examples match, so sprites Just Show Up.
84
84
 
85
85
  Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
86
86
  (sharing the BG bank). If you ever set R6=0xFB you MUST also upload
@@ -16,7 +16,7 @@ Anything you draw outside `(48, 24)..(207, 167)` is in the border —
16
16
  visible in headless emulator screenshots (gpgx shows the whole frame)
17
17
  but invisible on real hardware.
18
18
 
19
- The bundled scaffolds are direct ports of the SMS scaffolds and target
19
+ The bundled example games are direct ports of the SMS examples and target
20
20
  the full 256×192 area. Works fine for development under gpgx; for
21
21
  shipping to a real GG, reposition sprite + tilemap content to the
22
22
  visible center.
@@ -28,7 +28,7 @@ active low), separate from the D-pad/A/B which are on `$DC` like
28
28
  SMS. `gg_joypad_read()` already merges them — START shows up in bit 7
29
29
  of the returned byte (`JOY_START` mask).
30
30
 
31
- If you copied an SMS scaffold that uses PAUSE-as-START semantics
31
+ If you copied an SMS example that uses PAUSE-as-START semantics
32
32
  (SMS pause button is at port $DD bit 4 IIRC), it won't work on GG;
33
33
  swap to JOY_START.
34
34
 
@@ -85,27 +85,23 @@ two-byte little-endian CRAM entry.
85
85
 
86
86
  ## "Linking error: undefined reference to sms_joypad_read_p2"
87
87
 
88
- GG only has one controller. The SMS scaffolds use `sms_joypad_read_p2`
88
+ GG only has one controller. The SMS examples use `sms_joypad_read_p2`
89
89
  for the two-controller patterns (Pong, 2P shmup). When porting, drop
90
90
  the P2 read + force `p2 = 0` so the AI fallback always engages.
91
91
 
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"
95
+ ## "TMR SEGA" header / ROM boots in the wrong video mode
96
96
 
97
- Same magic as SMS gpgx accepts headerless ROMs fine for development.
98
- For real-hardware ROM-burning include a header at $7FF0:
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.
99
100
 
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.
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.
@@ -3,7 +3,7 @@
3
3
  GG shares its toolchain (SDCC z80) + emulator (genesis_plus_gx) +
4
4
  most of the runtime with SMS (`sms_crt0.s` ≡ `gg_crt0.s` byte-for-
5
5
  byte; PSG protocol identical). The GG tree at `src/platforms/gg/`
6
- holds the GG-specific scaffolds and runtime helpers; SMS docs apply
6
+ holds the GG-specific example games and runtime helpers; SMS docs apply
7
7
  for everything else.
8
8
 
9
9
  GG-specific:
@@ -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
@@ -20,3 +20,32 @@ uint8_t gg_joypad_read(void) {
20
20
  uint8_t start = ~PORT_GG_INPUT; /* GG-specific port bit 7 = START */
21
21
  return (uint8_t)((a & 0x3F) | (start & 0x80));
22
22
  }
23
+
24
+ /*
25
+ * Player 2 read — for ALTERNATING-TURNS or 2-controller play.
26
+ *
27
+ * HONEST NOTE: a real Game Gear has only ONE controller port on the unit; its
28
+ * 2P story is the Gear-to-Gear LINK CABLE (a second console). But the GG VDP
29
+ * and I/O chip are the SMS's, and gpgx wires the SMS's full split-across-
30
+ * $DC/$DD second-controller layout for GG too — so a SECOND PAD does drive
31
+ * port B in the emulator (and on an SMS-pad adapter), which is exactly what an
32
+ * alternating-turns 2P platformer needs (the two players never play at once).
33
+ *
34
+ * The hardware layout is the SMS's awkward split:
35
+ * PORT_JOY_A bits 6-7 = P2 UP, P2 DOWN
36
+ * PORT_JOY_B bits 0-3 = P2 LEFT, P2 RIGHT, P2 B1, P2 B2
37
+ * Reassembled into the same bit layout P1 uses:
38
+ * bit 0 = UP, 1 = DOWN, 2 = LEFT, 3 = RIGHT, 4 = B1, 5 = B2.
39
+ * Returns 0 when no P2 pad is present (all bits high = released after invert).
40
+ */
41
+ uint8_t gg_joypad_read_p2(void) {
42
+ uint8_t a = ~PORT_JOY_A; /* P2 UP in bit 6, DOWN in bit 7 */
43
+ uint8_t b = ~PORT_JOY_B; /* P2 LEFT bit 0, RIGHT 1, B1 2, B2 3 */
44
+ uint8_t up = (a >> 6) & 0x01; /* bit 6 -> bit 0 */
45
+ uint8_t down = (a >> 6) & 0x02; /* bit 7 -> bit 1 */
46
+ uint8_t left = (b << 2) & 0x04; /* bit 0 -> bit 2 */
47
+ uint8_t right = (b << 2) & 0x08; /* bit 1 -> bit 3 */
48
+ uint8_t b1 = (b << 2) & 0x10; /* bit 2 -> bit 4 */
49
+ uint8_t b2 = (b << 2) & 0x20; /* bit 3 -> bit 5 */
50
+ return (uint8_t)(up | down | left | right | b1 | b2);
51
+ }
@@ -96,7 +96,7 @@ void main(void) {
96
96
  `tgi_updatedisplay()` is the frame heartbeat — it ping-pongs the
97
97
  double-buffered display and waits for vblank.
98
98
 
99
- ## Drawing many rectangles in one frame (game scaffold pattern)
99
+ ## Drawing many rectangles in one frame (example-game pattern)
100
100
 
101
101
  The minimal example above draws "one rect per frame." For an actual
102
102
  game with HUD + background + sprites you'll do many tgi_bar / tgi_setcolor
@@ -44,7 +44,7 @@ Two things that trip agents up:
44
44
  Skipping it is the #1 "Lynx is blank" trap.
45
45
  - **Don't rely on `tgi_clear()`** to blank the screen in this
46
46
  toolchain/emulator path — use a full-screen `tgi_bar(0,0,maxx,maxy)`
47
- in the background colour instead. The bundled `shmup` scaffold uses
47
+ in the background colour instead. The bundled `shmup` example uses
48
48
  this exact loop; copy it.
49
49
 
50
50
  ## "tgi_outtextxy renders nothing"
@@ -54,7 +54,7 @@ cc65's default TGI on Lynx ships without a font. Either:
54
54
  live in `$cc65_share/target/lynx/fonts/`.
55
55
  2. Draw your own glyphs with `tgi_bar`/`tgi_line`.
56
56
 
57
- The bundled scaffolds work around this by using simple rectangles
57
+ The bundled example games work around this by using simple rectangles
58
58
  for game content + only short text strings. For game UI text, embed
59
59
  a bitmap font directly in your code.
60
60
 
@@ -93,7 +93,7 @@ cc65 is C89. No mixed declarations + code, no inline `for (uint8_t i
93
93
  = 0; ...)`, no compound literals, no // comments in some configs.
94
94
  Declare all variables at the top of each block.
95
95
 
96
- The bundled Lynx scaffolds are C89-clean — copy that pattern.
96
+ The bundled Lynx example games are C89-clean — copy that pattern.
97
97
 
98
98
  ## "Compile fails: no rule to make target lynx-bll.cfg"
99
99
 
@@ -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
@@ -12,13 +12,13 @@ romdev ships a **hardware helper library** (`src/platforms/msx/lib/c/`:
12
12
  `msx_psg_tone()` in plain C. It uses DIRECT Z80 I/O ports (the reliable path —
13
13
  NOT fragile inline-asm BIOS wrappers).
14
14
 
15
- The fastest way to a working game: **`scaffold({op:'game', platform: "msx", genre:
16
- "shmup"})`** — or any of `platformer` / `puzzle` / `sports` / `racing`, the full
17
- genre set. For a smaller starting point use **`scaffold({op:'project', platform:
18
- "msx", template: "sprite_move"})`** (also `music_sfx`, `catch_game`). Either drops
15
+ The fastest way to a working game: **fork the example game whose core loop is
16
+ nearest yours — `examples({op:'fork', example:"msx/shmup", name, path})`** — or any
17
+ of `platformer` / `puzzle` / `sports` / `racing`, the full genre set. For a smaller
18
+ starting point fork `msx/sprite_move` (also `music_sfx`, `catch_game`). Either drops
19
19
  a complete, *building* project — a verified playable example + the helper lib +
20
20
  the cart crt0 + docs. Read the example's `main.c`, then change it. Examples live in
21
- `examples/msx/`. The `platformer` scaffold column-streams the SCREEN 2 name table
21
+ `examples/msx/`. The `platformer` example column-streams the SCREEN 2 name table
22
22
  for a tile-by-tile side-scroll. **Gotcha:** read joystick **port 1**
23
23
  (`msx_read_joystick(1)`) — port 0 is the keyboard, which an emulator's gamepad
24
24
  doesn't drive.
@@ -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 forked before 2026-06-09
89
+ shows ghost zeros, refresh its `msx_crt0.s` from a freshly forked example.
@@ -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,9 @@ 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_psg_noise(uint8_t chan, uint8_t rate, uint8_t vol); /* vol 0 = off */
91
+ void msx_music(uint8_t on); /* background melody on channel C — ON by default; 0 = off */
92
+ void msx_music_tick(void); /* call once per frame (scaffolds do) */
90
93
  void msx_psg_off(uint8_t chan);
91
94
 
92
95
  #endif /* MSX_HW_H */
@@ -121,6 +121,13 @@ void msx_psg_tone(uint8_t chan, uint16_t period, uint8_t vol) {
121
121
  uint8_t fine = (uint8_t)(period & 0xFF);
122
122
  uint8_t coarse = (uint8_t)((period >> 8) & 0x0F);
123
123
 
124
+ /* DI around the whole register sequence: the BIOS KEYINT ISR reads
125
+ * PSG R14 (joystick row) every frame, and it CLOBBERS the PSGADDR
126
+ * latch — an IRQ between our PSGADDR and PSGWRITE sent the period/
127
+ * volume bytes into R14 instead. Symptom: mixer set, period 0,
128
+ * amplitude 0 → every MSX scaffold was silent. */
129
+ __asm__("di");
130
+
124
131
  /* tone period: regs 0/1 (A), 2/3 (B), 4/5 (C) */
125
132
  PSGADDR = (uint8_t)(chan << 1); PSGWRITE = fine;
126
133
  PSGADDR = (uint8_t)((chan << 1) + 1); PSGWRITE = coarse;
@@ -136,15 +143,78 @@ void msx_psg_tone(uint8_t chan, uint16_t period, uint8_t vol) {
136
143
  mixer &= (uint8_t)~(1 << chan); /* tone ON for this channel */
137
144
  PSGADDR = 7;
138
145
  PSGWRITE = mixer;
146
+ __asm__("ei");
147
+ }
148
+
149
+ /* Play NOISE on PSG channel 0/1/2: the AY's one shared noise generator
150
+ * (reg 6, 5-bit period — bigger = lower rumble) routed into this channel by
151
+ * clearing its noise-disable mixer bit (reg 7 bits 3-5) while setting its
152
+ * tone-disable bit. The classic explosion/impact voice.
153
+ * msx_psg_noise(chan, rate, 0) silences the channel and re-masks its noise
154
+ * bit (msx_psg_off only re-masks TONE — it doesn't know about noise). */
155
+ void msx_psg_noise(uint8_t chan, uint8_t rate, uint8_t vol) {
156
+ uint8_t mixer;
157
+ __asm__("di"); /* same KEYINT race as above */
158
+ if (vol) {
159
+ PSGADDR = 6; /* noise period (shared) */
160
+ PSGWRITE = (uint8_t)(rate & 0x1F);
161
+ }
162
+ PSGADDR = (uint8_t)(8 + chan);
163
+ PSGWRITE = (uint8_t)(vol & 0x0F);
164
+ PSGADDR = 7;
165
+ mixer = PSGREAD;
166
+ mixer |= (uint8_t)(1 << chan); /* tone OFF for this channel */
167
+ if (vol) mixer &= (uint8_t)~(1 << (3 + chan)); /* noise ON */
168
+ else mixer |= (uint8_t)(1 << (3 + chan)); /* noise OFF */
169
+ PSGADDR = 7;
170
+ PSGWRITE = mixer;
171
+ __asm__("ei");
139
172
  }
140
173
 
141
174
  /* Silence a PSG channel: zero its volume and re-disable its tone bit. */
142
175
  void msx_psg_off(uint8_t chan) {
143
176
  uint8_t mixer;
177
+ __asm__("di"); /* same KEYINT race as above */
144
178
  PSGADDR = (uint8_t)(8 + chan); PSGWRITE = 0; /* volume 0 */
145
179
  PSGADDR = 7;
146
180
  mixer = PSGREAD;
147
181
  mixer |= (uint8_t)(1 << chan); /* tone OFF for this channel */
148
182
  PSGADDR = 7;
149
183
  PSGWRITE = mixer;
184
+ __asm__("ei");
185
+ }
186
+
187
+ /* ── background music: 16-step melody loop on PSG channel C (2) ─────
188
+ * Call msx_music_tick() once per frame (the scaffolds wire it in after
189
+ * their vsync wait); msx_music(0) turns it off. SFX use channels A/B,
190
+ * so effects always cut through. AY period = 1789773 / (16 * freq). */
191
+ static const uint16_t _msx_music_per[16] = {
192
+ 427, 339, 285, 214, 285, 339, 427, 0, /* C4 E4 G4 C5 G4 E4 C4 - */
193
+ 508, 427, 339, 254, 339, 427, 508, 0, /* A3 C4 E4 A4 E4 C4 A3 - */
194
+ };
195
+ static uint8_t _msx_music_on = 1;
196
+ static uint8_t _msx_music_step;
197
+ static uint8_t _msx_music_timer;
198
+
199
+ void msx_music(uint8_t on) {
200
+ _msx_music_on = on;
201
+ _msx_music_step = 0;
202
+ _msx_music_timer = 0;
203
+ if (!on) msx_psg_off(2);
204
+ }
205
+
206
+ void msx_music_tick(void) {
207
+ uint16_t p;
208
+ if (!_msx_music_on) return;
209
+ if (_msx_music_timer == 0) {
210
+ p = _msx_music_per[_msx_music_step & 15];
211
+ if (p) {
212
+ msx_psg_tone(2, p, 12); /* AY volume is ~logarithmic — 9 was a whisper */
213
+ } else {
214
+ msx_psg_off(2); /* rest */
215
+ }
216
+ ++_msx_music_step;
217
+ }
218
+ ++_msx_music_timer;
219
+ if (_msx_music_timer >= 9) _msx_music_timer = 0;
150
220
  }
@@ -207,9 +207,9 @@ names also resolve (east→A, west→B). So `input({op:'set', a: true})` presses
207
207
  expected — unlike the genesis_plus_gx platforms (Genesis/SMS/GG), there's no
208
208
  surprise here.
209
209
 
210
- ## What `scaffold({op:'project'})` copies into your project
210
+ ## What `examples({op:'fork'})` copies into your project
211
211
 
212
- `scaffold({op:'project', platform:"nes", template:"hello_sprite"|"tile_engine"|"default"})`
212
+ `examples({op:'fork', example:"nes/hello_sprite"|"nes/tile_engine"|"nes/default", name, path})`
213
213
  writes these files into your project directory. **They're yours** — every
214
214
  byte that compiles is in the repo. Edit, fork, replace; nothing is auto-injected
215
215
  at build time.
@@ -393,7 +393,11 @@ build({ output:'rom', platform:'nes',
393
393
  inesHeader:{ prgBanks:2, chrBanks:1, mapper:0, mirroring:"vertical" } })
394
394
  ```
395
395
  Mutually exclusive with `linkerConfig`. Works for any NROM (mapper 0, ≤2 PRG
396
- banks); for a banked mapper supply a linker `.cfg` that places each bank.
396
+ banks). For a BANKED mapper you don't hand-write the glue anymore:
397
+ `disasm({target:'project'})` emits a HEADER segment (the original 16 iNES
398
+ bytes), a `.segment "PRGn"` wrapper per bank, and a multi-bank `nes_rebuild.cfg`
399
+ (switchable banks at $8000, fixed top bank at $C000), all wired into
400
+ `rebuild.json` via `linkerConfigPath` — a one-call byte-exact rebuild.
397
401
 
398
402
  **2. `linkerConfig:"chr-rom"` — for homebrew C that ships FIXED tile art.**
399
403
  A cc65-C preset (segment split + a CHARS segment in an 8 KB ROM2 bank). Put your
@@ -402,8 +406,11 @@ tiles in `.segment "CHARS"` (`.incbin "tiles.chr"`) + pass the blob via
402
406
  other bank configs, prefer `inesHeader`.
403
407
 
404
408
  **3. `disasm({target:'project'})` — disassemble → rebuild, in two calls.**
405
- For NES it now extracts the CHR-ROM to `chr.bin`, writes a `rebuild.json` (the
406
- exact `build({inesHeader})` call, with absolute paths) and a `BUILD.md`. Feed
409
+ For NES it extracts the CHR-ROM to `chr.bin`, writes a `rebuild.json` (the
410
+ exact `build({...})` call, with absolute paths) and a `BUILD.md`. NROM gets the
411
+ `inesHeader` one-call form; BANKED mappers (UxROM/MMC1/MMC3…) get per-bank
412
+ `PRGn` segment wrappers + the original-bytes HEADER segment + a generated
413
+ multi-bank `.cfg` referenced via `linkerConfigPath`. Either way: feed
407
414
  `rebuild.json` straight back to `build` and you get a byte-identical ROM. This
408
415
  is the RE workhorse loop: `disasm({target:'project'})` → edit the `.asm` →
409
416
  rebuild → `diffRoms` to confirm your patch landed.