romdevtools 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/AGENTS.md +53 -43
  2. package/CHANGELOG.md +91 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +12 -12
  88. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  89. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  90. package/src/cores/wasm/fceumm_libretro.js +1 -1
  91. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  92. package/src/cores/wasm/gambatte_libretro.js +1 -1
  93. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  94. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  95. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  96. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  97. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  98. package/src/cores/wasm/handy_libretro.js +1 -1
  99. package/src/cores/wasm/handy_libretro.wasm +0 -0
  100. package/src/cores/wasm/mgba_libretro.js +1 -1
  101. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  102. package/src/cores/wasm/prosystem_libretro.js +1 -1
  103. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  104. package/src/cores/wasm/snes9x_libretro.js +1 -1
  105. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  106. package/src/cores/wasm/stella2014_libretro.js +1 -1
  107. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  108. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  109. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  110. package/src/host/LibretroHost.js +84 -8
  111. package/src/http/tool-registry.js +11 -11
  112. package/src/mcp/tools/cheats.js +2 -1
  113. package/src/mcp/tools/frame.js +3 -2
  114. package/src/mcp/tools/index.js +3 -3
  115. package/src/mcp/tools/input.js +5 -4
  116. package/src/mcp/tools/lifecycle.js +6 -4
  117. package/src/mcp/tools/memory.js +131 -24
  118. package/src/mcp/tools/platform-docs.js +1 -1
  119. package/src/mcp/tools/preview-tile.js +6 -2
  120. package/src/mcp/tools/project.js +1098 -130
  121. package/src/mcp/tools/record.js +6 -7
  122. package/src/mcp/tools/rom-id.js +5 -1
  123. package/src/mcp/tools/run-until.js +12 -4
  124. package/src/mcp/tools/snippets.js +6 -6
  125. package/src/mcp/tools/sprite-pipeline.js +14 -2
  126. package/src/mcp/tools/state.js +2 -1
  127. package/src/mcp/tools/tile-inspect.js +8 -1
  128. package/src/mcp/tools/toolchain.js +12 -1
  129. package/src/mcp/tools/watch-memory.js +53 -10
  130. package/src/observer/bus.js +73 -0
  131. package/src/observer/livestream.html +4 -2
  132. package/src/observer/tool-wrap.js +17 -14
  133. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
  134. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  135. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  136. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  137. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  138. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  139. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  140. package/src/platforms/gb/lib/c/README.md +10 -11
  141. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  142. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  143. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  144. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  145. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  146. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  147. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  148. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  149. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  150. package/src/platforms/gbc/lib/c/README.md +10 -11
  151. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  152. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  153. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  154. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  155. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  156. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  157. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  158. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  159. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  160. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  161. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  162. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  163. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  164. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  165. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  166. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  167. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  168. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  169. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  170. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  171. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  172. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  173. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  174. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  175. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  176. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  177. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  178. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  179. package/src/toolchains/index.js +27 -11
@@ -12,13 +12,13 @@ romdev ships a **hardware helper library** (`src/platforms/pce/lib/c/`:
12
12
  `psg_tone()` instead of poking VDC/VCE registers by hand. cc65 has **no** sprite
13
13
  library, so this lib is how you get pixels on screen.
14
14
 
15
- The fastest way to a working game: **`scaffold({op:'game', platform: "pce", 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
- "pce", 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:"pce/shmup", name, path})`** — or any
17
+ of `platformer` / `puzzle` / `sports` / `racing`, the full genre set. For a smaller
18
+ starting point fork `pce/sprite_move` (also `music_sfx`, `catch_game`). Either drops
19
19
  a complete, *building* project — a verified playable example + the helper lib +
20
20
  docs. Read the example's `main.c`, then change it. The examples live in
21
- `examples/pce/`. The genre scaffolds fill the BAT (32×32 virtual screen); the
21
+ `examples/pce/`. The genre examples fill the BAT (32×32 virtual screen); the
22
22
  `platformer` smooth-scrolls the background via the VDC BXR (R7) register.
23
23
  **Gotcha:** `#include <stdint.h>` for int8/16/32_t — `pce.h` only typedefs u8/u16.
24
24
 
@@ -66,4 +66,4 @@ The 5-bit channel volume (`PSG_CHAN_CTRL` low bits, 0-31) is roughly an
66
66
  ATTENUATOR: each step below 31 costs ~1.5 dB. A "middle" value like 13 is
67
67
  about -27 dB — effectively silence on real hardware and most cores. Use
68
68
  **29-31 for SFX/music** and treat anything under ~20 as a deliberate whisper.
69
- (The bundled `psg_tone` scaffold helper and the music ticker default loud.)
69
+ (The bundled `psg_tone` example helper and the music ticker default loud.)
@@ -103,8 +103,19 @@ void disp_enable(void); /* VDC R5: BG + SPR + VBlank IRQ on at once
103
103
  void vblank_irq_enable(void); /* just the VBlank IRQ bit (waitvsync needs it) */
104
104
  void load_tiles(u16 vram, const u16 *src, u16 n); /* alias of vram_write (tiles) */
105
105
  void set_sprite(u8 slot, u16 x, u16 y, u16 pattern, u8 palette); /* fill shadow SATB */
106
+ void set_sprite_ex(u8 slot, u16 x, u16 y, u16 pattern, u8 palette, u16 attr_ex);
106
107
  void satb_dma(void); /* DMA shadow SATB -> VDC (R19) */
107
108
 
109
+ /* attr_ex bits for set_sprite_ex() — the HuC6270 large-sprite size + flip
110
+ * bits in SATB word3. A 32-wide sprite needs a 2-aligned pattern code, 32x32
111
+ * needs 4-aligned, 32x64 needs 8-aligned; the data is consecutive 16x16 cells
112
+ * (left-to-right, then down). See the set_sprite_ex() comment in pce_video.c. */
113
+ #define SPR_CGX_32 0x0100 /* width 32px (two cells side by side) */
114
+ #define SPR_CGY_32 0x1000 /* height 32px (two cell rows) */
115
+ #define SPR_CGY_64 0x3000 /* height 64px (four cell rows) */
116
+ #define SPR_XFLIP 0x0800 /* mirror horizontally */
117
+ #define SPR_YFLIP 0x8000 /* mirror vertically */
118
+
108
119
  /* The shadow SATB lives in VRAM at this word address; satb_dma() points the VDC
109
120
  * SATB-DMA source (R19) here. Pattern base for tiles is your choice; sprites
110
121
  * default to using the same VRAM you load_tiles() into. */
@@ -131,6 +131,38 @@ void set_sprite(u8 slot, u16 x, u16 y, u16 pattern, u8 palette) {
131
131
  e[3] = (u16)(0x0080 | (palette & 0x0F)); /* word3: SPBG-front + pal */
132
132
  }
133
133
 
134
+ /* set_sprite() with the HuC6270's LARGE-SPRITE size bits — the PCE's signature
135
+ * trick (sprites up to 32x64 from ONE SATB entry, where the NES needs 8+).
136
+ *
137
+ * SATB word3 (the attribute word) layout:
138
+ * bit 15 Y-flip
139
+ * bits13:12 CGY — sprite HEIGHT: 00=16px, 01=32px, 11=64px (10 is invalid)
140
+ * bit 11 X-flip
141
+ * bit 8 CGX — sprite WIDTH: 0=16px, 1=32px
142
+ * bit 7 SPBG — 1 = sprite in front of background
143
+ * bits 3:0 sprite sub-palette (0-15)
144
+ *
145
+ * `attr_ex` is OR'd into word3 — pass the SPR_* constants from pce_hw.h
146
+ * (e.g. SPR_CGX_32 | SPR_CGY_32 for a 32x32 sprite). SPBG-front is still set
147
+ * for you, same as set_sprite().
148
+ *
149
+ * PATTERN LAYOUT for large sprites: the hardware ignores the low bit(s) of the
150
+ * pattern code and fetches consecutive 16x16 cells (64 words each) instead:
151
+ * 32 wide: cell N = left, N+1 = right (N multiple of 2)
152
+ * 32 tall: row r adds 2*r: N, N+1 / N+2, N+3 (N multiple of 4)
153
+ * 64 tall: rows 0-3 add 2*r: N .. N+7 (N multiple of 8)
154
+ * So a 32x32 sprite's VRAM data is FOUR cells in TL, TR, BL, BR order, and
155
+ * its pattern code must be 4-aligned (pattern = VRAM>>6 like set_sprite). */
156
+ void set_sprite_ex(u8 slot, u16 x, u16 y, u16 pattern, u8 palette, u16 attr_ex) {
157
+ u16 *e;
158
+ if (slot >= 64) return;
159
+ e = &_pce_satb[slot * 4];
160
+ e[0] = (u16)(y + 64); /* word0: Y (biased) */
161
+ e[1] = (u16)(x + 32); /* word1: X (biased) */
162
+ e[2] = (u16)((pattern & 0x3FF) << 1); /* word2: pattern (cell<<1) */
163
+ e[3] = (u16)(0x0080 | (palette & 0x0F) | attr_ex); /* word3: size/flip + SPBG + pal */
164
+ }
165
+
134
166
  /* Copy the shadow SATB into VRAM at PCE_SATB_VRAM, then tell the VDC to DMA it
135
167
  * into its internal sprite table (R19 = SATB source). */
136
168
  void satb_dma(void) {
@@ -78,7 +78,7 @@ R1 = 0x80 display OFF, vblank IRQ off, 192-line
78
78
  R2 = 0xFF name table at $3800
79
79
  R4 = 0xFF BG tile data at $0000
80
80
  R5 = 0xFF sprite attr table at $3F00
81
- R6 = 0xFF sprite tile data at $2000 (own bank; scaffolds upload here)
81
+ R6 = 0xFF sprite tile data at $2000 (own bank; the example games upload here)
82
82
  R7 = 0x00 border colour
83
83
  ```
84
84
 
@@ -114,7 +114,7 @@ So Y bytes and X/tile pairs are split into TWO regions of the SAT.
114
114
  `src/platforms/sms/lib/c/sprite_table.c` keeps a 256-byte shadow
115
115
  buffer in WRAM and uploads it to the SAT each vblank.
116
116
 
117
- ### Two footguns the bundled scaffolds keep hitting
117
+ ### Two footguns the bundled example games keep hitting
118
118
 
119
119
  1. **8 sprites per scanline limit.** The VDP draws up to 8 sprites per
120
120
  scanline; the 9th+ are silently dropped. If you draw a "CATCH THE
@@ -147,9 +147,9 @@ buffer in WRAM and uploads it to the SAT each vblank.
147
147
  `sms_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
148
148
  sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
149
149
  from `$2000-$3FFF`, their **own bank** separate from BG tiles at
150
- $0000. This matches every bundled scaffold, which uploads sprite
150
+ $0000. This matches every bundled example, which uploads sprite
151
151
  tiles to `$2000` (`sms_load_tiles(0x2000, …)`) — default and
152
- scaffolds agree, so sprites render.
152
+ examples agree, so sprites render.
153
153
 
154
154
  Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
155
155
  (shared with the BG bank). If you set R6=0xFB you MUST upload your
@@ -222,7 +222,7 @@ PSG (SN76489) on port $7F. 4 channels: 3 square waves + 1 noise.
222
222
  Writes are byte-wise; the high bit selects "latch register" vs
223
223
  "continue previous register".
224
224
 
225
- A full driver is beyond the scope of these scaffolds. For
225
+ A full driver is beyond the scope of these example games. For
226
226
  playable SFX, manually pulse $7F with the latch-register byte
227
227
  followed by data bytes. Real games ship a music driver in WRAM.
228
228
 
@@ -325,7 +325,7 @@ region per bank with a bank-by-bank native rebuild recipe in `BUILD.md`.
325
325
 
326
326
  ## Horizontal scrolling (for side-scrollers)
327
327
 
328
- The `platformer` scaffold is single-screen. To make it a side-scroller:
328
+ The `platformer` example is single-screen. To make it a side-scroller:
329
329
 
330
330
  - **Hardware scroll:** write VDP register 8 (horizontal scroll) each frame =
331
331
  `-camX & 0xFF` (the reg scrolls the screen; the name table is 32×28 and
@@ -253,7 +253,7 @@ PVSnesLib's `hdr.asm` fills these in.
253
253
 
254
254
  ## Where the SDK lives (and how to read it)
255
255
 
256
- `scaffold({op:'project', platform:"snes"})` ships the FULL PVSnesLib source +
256
+ `examples({op:'fork'})` (any SNES example) ships the FULL PVSnesLib source +
257
257
  header tree into the new project at `vendor/pvsneslib/`. So when
258
258
  your code does `#include <snes.h>`, those headers come from
259
259
  `vendor/pvsneslib/include/`:
@@ -296,7 +296,7 @@ Loadable via snes9x (`loadMedia`).
296
296
 
297
297
  ## Horizontal scrolling (for side-scrollers)
298
298
 
299
- The `platformer` scaffold is single-screen. SNES scrolling is the easiest of
299
+ The `platformer` example is single-screen. SNES scrolling is the easiest of
300
300
  the tile platforms because each BG layer has its own hardware scroll register
301
301
  and parallax is nearly free.
302
302
 
@@ -160,6 +160,45 @@ Three layers:
160
160
  PVSnesLib's API is the path of least resistance. Roll your own SPC
161
161
  driver only when you really need the control.
162
162
 
163
+ ## "Music never starts (sfx works, sfx_init returned 0)"
164
+
165
+ A command sent to the bundled snes_sfx driver IMMEDIATELY after
166
+ `sfx_init()` returns can be silently swallowed. `sfx_init` returns the
167
+ instant the SPC echoes the jump command, but the driver then spends
168
+ ~50 DSP port writes initialising before it seeds its command
169
+ edge-detector from $2140. A `sfx_music_play()` issued inside that
170
+ window becomes the SEED — no edge, no dispatch, music never starts.
171
+
172
+ Symptoms via the debug tools: `getAudioState({chip:'dsp'})` shows
173
+ voice 1 with pitch 0 / env 0; ARAM $00 (prev_cmd) already equals your
174
+ command byte while ARAM $01 (music_on) is 0.
175
+
176
+ Fix: put one `WaitForVBlank()` between `sfx_init()` and the first
177
+ `sfx_play`/`sfx_music_play` — a frame is thousands of SPC cycles, the
178
+ driver is guaranteed to be in its command loop. The racing example does
179
+ exactly this (see its `sfx_init` call site).
180
+
181
+ ## "My HDMA table stops landing / OAM gets corrupted" (HDMA channel fights the OAM DMA)
182
+
183
+ A DMA channel cannot serve general-purpose DMA and HDMA in the same
184
+ frame — and PVSnesLib's runtime OWNS two channels for GP-DMA:
185
+
186
+ - **channel 0** — `dmaCopyVram` and friends (console text upload,
187
+ `oamInitGfxSet`, `consoleVblank`)
188
+ - **channel 7** — the VBlank ISR's OAM upload (vblank.asm rewrites
189
+ $4370-$4375 EVERY NMI)
190
+
191
+ Park an HDMA effect on channel 7 and it works for exactly zero frames:
192
+ each NMI silently rewrites the channel's DMAP/BBAD/A1T with OAM-DMA
193
+ parameters, so your per-scanline writes stop landing — and worse, the
194
+ HDMA unit then feeds your table bytes into $2104 (OAM data). The
195
+ failure is maddeningly partial: channels 1-6 keep working, so a
196
+ multi-channel effect (e.g. a Mode 7 split) comes up ALMOST right with
197
+ one register mysteriously stuck at a stale value.
198
+
199
+ Fix: keep HDMA on channels 1-6. The Mode 7 racing example uses 2-6 and
200
+ documents the assignment at its `road_hdma_on()`.
201
+
163
202
  ## "consoleDrawText output is corrupt / shifted"
164
203
 
165
204
  `consoleInitText(palnum, palsize, tilfont, palfont)` configures the
@@ -220,7 +259,7 @@ synthesizes a fallback `issues[]` entry with a hint. The idioms to avoid:
220
259
  crossed a bank boundary and the layout is wrong. Native interrupt vectors live
221
260
  at `$FFE4-$FFEE`, emulation vectors at `$FFF4-$FFFF` — keep your header/vector
222
261
  block where the layout expects it. Use
223
- `scaffold({op:'snippets', platform:"snes", mode:"get", name:"lorom_header.asm"})`
262
+ `examples({op:'snippets', platform:"snes", mode:"get", snippetName:"lorom_header.asm"})`
224
263
  for the canonical layout (and `lorom_multibank.asm` for multi-bank).
225
264
 
226
265
  (This is the asar/asm path. The default PVSnesLib **C** path goes through
@@ -19,13 +19,15 @@
19
19
  # 3. Write CHR data from C at runtime: PPUADDR = 0x00; PPUDATA = byte; etc.
20
20
 
21
21
  SYMBOLS {
22
- # Stack is $0200 (512 B) so the top RAM page ($0700-$07FF) can be
23
- # reserved below for a music driver's scratch RAM (FamiTone2 et al.),
24
- # which needs a dedicated, page-aligned block that the C BSS/DATA
25
- # region must NOT overlap. Tiny NROM scaffolds use far less than 512 B
26
- # of stack, so this is safe; scaffolds with no music driver simply
27
- # leave the reserved page unused.
28
- __STACKSIZE__: type = weak, value = $0200;
22
+ # C parameter stack is ONE page ($0600-$06FF, grows down from $0700).
23
+ # NROM-sized C games use far less than 256 B of it (shallow call
24
+ # depth, mostly static data), and shrinking it frees $0500-$05FF as
25
+ # the USER SCRATCH PAGE: game code may place absolute-addressed
26
+ # arrays there (e.g. `#define BOARD ((unsigned char*)0x0500)`) when
27
+ # BSS ($0300-$04FF) is full — the puzzle example game does exactly
28
+ # this. The top page ($0700-$07FF) stays reserved for a music
29
+ # driver's scratch RAM (FamiTone2 et al.).
30
+ __STACKSIZE__: type = weak, value = $0100;
29
31
  }
30
32
  MEMORY {
31
33
  ZP: file = "", start = $0002, size = $001A, type = rw, define = yes;
@@ -41,7 +43,10 @@ MEMORY {
41
43
 
42
44
  # NO ROM2 / CHARS — this is the whole point of the CHR-RAM preset.
43
45
 
44
- SRAM: file = "", start = $0500, size = __STACKSIZE__, define = yes;
46
+ # $0500-$05FF: user scratch page (see SYMBOLS note) NOT a segment;
47
+ # game code addresses it absolutely so the linker never places
48
+ # anything here.
49
+ SRAM: file = "", start = $0600, size = __STACKSIZE__, define = yes;
45
50
 
46
51
  # Reserved page for a sound-driver's RAM scratch ($0700-$07FF). The
47
52
  # bundled FamiTone2 engine (music_demo scaffold) pins FT_BASE_ADR here
@@ -28,7 +28,12 @@
28
28
  .import _main, zerobss, copydata
29
29
  .import __RAM_START__, __RAM_SIZE__
30
30
  .import __SRAM_START__, __SRAM_SIZE__
31
- .import _vram_queue_flush
31
+ .import _vram_q_hi, _vram_q_lo, _vram_q_val
32
+ .import _vram_queue_head, _vram_queue_len, _vram_queue_lock
33
+
34
+ ; Must match nes_runtime.c (QUEUE_MAX 32 ring buffer).
35
+ QUEUE_MASK = 31
36
+ FLUSH_BUDGET = 16
32
37
  .import _scroll_x, _scroll_y, _ppuctrl_value, _nmi_counter
33
38
  .importzp c_sp
34
39
 
@@ -39,7 +44,12 @@
39
44
  .byte $4e, $45, $53, $1a ; "NES" + EOF
40
45
  .byte 2 ; PRG-ROM banks (16K each) → 32K
41
46
  .byte 0 ; CHR-ROM banks (8K each) → 0 = CHR-RAM
42
- .byte %00000001 ; flags6 — vertical mirroring
47
+ .byte %00000011 ; flags6 — vertical mirroring + BATTERY.
48
+ ; The battery bit maps persistent 8KB
49
+ ; PRG-RAM at $6000 (the save_ram region)
50
+ ; — hiscore_load/save in nes_runtime use
51
+ ; it. Benign when unused; without it,
52
+ ; $6000-$7FFF is OPEN BUS on NROM.
43
53
  .byte %00000000 ; flags7 — mapper hi nybble
44
54
  .byte 0, 0, 0, 0, 0, 0, 0, 0
45
55
 
@@ -129,9 +139,46 @@ nmi:
129
139
  lda #$02 ; high byte of $0200
130
140
  sta $4014 ; PPU OAMDMA — kicks off the copy
131
141
 
132
- ; Flush the VRAM queue (writes anything game code stashed via
133
- ; vram_set / tile_set / tile_set_palette). PPU is unlocked here.
134
- jsr _vram_queue_flush
142
+ ; ── Drain the VRAM queue IN ASSEMBLY, on purpose ──────────────
143
+ ; Vblank is ~2273 CPU cycles and the OAM DMA above just spent 513.
144
+ ; Compiled C costs 200+ cycles per queue entry, so a C flush blows
145
+ ; past the end of vblank — and PPUDATA writes during ACTIVE
146
+ ; RENDERING land at corrupted addresses (the PPU's internal v
147
+ ; register is busy fetching tiles; its coarse-X/fine-Y counters
148
+ ; shear every late write). This loop costs ~40 cycles per entry,
149
+ ; so FLUSH_BUDGET entries always finish safely inside vblank.
150
+ ; QUEUE_MASK/FLUSH_BUDGET must match nes_runtime.c's ring buffer.
151
+ lda _vram_queue_lock
152
+ bne @flush_done ; a push is mid-flight — skip this vblank
153
+ lda _vram_queue_len
154
+ beq @flush_done
155
+ cmp #FLUSH_BUDGET
156
+ bcc @flush_n_ok
157
+ lda #FLUSH_BUDGET
158
+ @flush_n_ok:
159
+ sta nmi_drain ; loop counter
160
+ sta nmi_drained ; remembered for the length update
161
+ bit $2002 ; reset the PPUADDR write latch
162
+ ldx _vram_queue_head
163
+ @flush_loop:
164
+ lda _vram_q_hi,x
165
+ sta $2006
166
+ lda _vram_q_lo,x
167
+ sta $2006
168
+ lda _vram_q_val,x
169
+ sta $2007
170
+ inx
171
+ txa
172
+ and #QUEUE_MASK ; ring wrap
173
+ tax
174
+ dec nmi_drain
175
+ bne @flush_loop
176
+ stx _vram_queue_head
177
+ lda _vram_queue_len
178
+ sec
179
+ sbc nmi_drained
180
+ sta _vram_queue_len
181
+ @flush_done:
135
182
 
136
183
  ; Reset PPUADDR to $2000 (otherwise the queue's last $2006 write
137
184
  ; leaves it dangling and the PPU samples random VRAM as the BG).
@@ -172,6 +219,12 @@ irq: rti
172
219
  _shadow_oam: .res 256
173
220
 
174
221
  ; ------------------------------------------------------------------------
222
+ ; NMI-private temporaries — deliberately NOT cc65's zp tmp1-4 (the NMI
223
+ ; would corrupt them under interrupted C code).
224
+ .segment "BSS"
225
+ nmi_drain: .res 1
226
+ nmi_drained: .res 1
227
+
175
228
  .segment "VECTORS"
176
229
  .word nmi ; $FFFA
177
230
  .word start ; $FFFC
@@ -22,7 +22,12 @@
22
22
  .import _main, zerobss, copydata
23
23
  .import __RAM_START__, __RAM_SIZE__
24
24
  .import __SRAM_START__, __SRAM_SIZE__
25
- .import _vram_queue_flush
25
+ .import _vram_q_hi, _vram_q_lo, _vram_q_val
26
+ .import _vram_queue_head, _vram_queue_len, _vram_queue_lock
27
+
28
+ ; Must match nes_runtime.c (QUEUE_MAX 32 ring buffer).
29
+ QUEUE_MASK = 31
30
+ FLUSH_BUDGET = 16
26
31
  .import _scroll_x, _scroll_y, _ppuctrl_value, _nmi_counter
27
32
  .importzp c_sp
28
33
 
@@ -109,8 +114,46 @@ nmi:
109
114
  lda #$02 ; high byte of $0200
110
115
  sta $4014 ; PPU OAMDMA — kicks off the copy
111
116
 
112
- ; Flush the VRAM queue (nametable/palette writes game code stashed).
113
- jsr _vram_queue_flush
117
+ ; ── Drain the VRAM queue IN ASSEMBLY, on purpose ──────────────
118
+ ; Vblank is ~2273 CPU cycles and the OAM DMA above just spent 513.
119
+ ; Compiled C costs 200+ cycles per queue entry, so a C flush blows
120
+ ; past the end of vblank — and PPUDATA writes during ACTIVE
121
+ ; RENDERING land at corrupted addresses (the PPU's internal v
122
+ ; register is busy fetching tiles; its coarse-X/fine-Y counters
123
+ ; shear every late write). This loop costs ~40 cycles per entry,
124
+ ; so FLUSH_BUDGET entries always finish safely inside vblank.
125
+ ; QUEUE_MASK/FLUSH_BUDGET must match nes_runtime.c's ring buffer.
126
+ lda _vram_queue_lock
127
+ bne @flush_done ; a push is mid-flight — skip this vblank
128
+ lda _vram_queue_len
129
+ beq @flush_done
130
+ cmp #FLUSH_BUDGET
131
+ bcc @flush_n_ok
132
+ lda #FLUSH_BUDGET
133
+ @flush_n_ok:
134
+ sta nmi_drain ; loop counter
135
+ sta nmi_drained ; remembered for the length update
136
+ bit $2002 ; reset the PPUADDR write latch
137
+ ldx _vram_queue_head
138
+ @flush_loop:
139
+ lda _vram_q_hi,x
140
+ sta $2006
141
+ lda _vram_q_lo,x
142
+ sta $2006
143
+ lda _vram_q_val,x
144
+ sta $2007
145
+ inx
146
+ txa
147
+ and #QUEUE_MASK ; ring wrap
148
+ tax
149
+ dec nmi_drain
150
+ bne @flush_loop
151
+ stx _vram_queue_head
152
+ lda _vram_queue_len
153
+ sec
154
+ sbc nmi_drained
155
+ sta _vram_queue_len
156
+ @flush_done:
114
157
 
115
158
  ; Reset PPUADDR to $2000 so the PPU doesn't sample random VRAM as BG.
116
159
  bit $2002
@@ -147,6 +190,12 @@ irq: rti
147
190
  _shadow_oam: .res 256
148
191
 
149
192
  ; ------------------------------------------------------------------------
193
+ ; NMI-private temporaries — deliberately NOT cc65's zp tmp1-4 (the NMI
194
+ ; would corrupt them under interrupted C code).
195
+ .segment "BSS"
196
+ nmi_drain: .res 1
197
+ nmi_drained: .res 1
198
+
150
199
  .segment "VECTORS"
151
200
  .word nmi ; $FFFA
152
201
  .word start ; $FFFC
@@ -0,0 +1,52 @@
1
+ # PC Engine 32KB HuCard ld65 config (romdev 'rom32k' preset).
2
+ #
3
+ # WHY THIS EXISTS: cc65's stock pce.cfg defaults to an 8KB image, and its
4
+ # documented 16K/32K option ($CARTSIZE) places STARTUP/VECTORS at the END of
5
+ # the file — but a HuCard maps file offset 0 as bank 0, and the HuC6280 reset
6
+ # maps MPR7 (=$E000-$FFFF, where the vectors live) to BANK 0. So a stock-cfg
7
+ # 32K image boots to a black screen (verified on geargrafx). This config puts
8
+ # bank 0 (STARTUP/VECTORS + hot code) FIRST in the file, at $E000, and the
9
+ # remaining 24KB of banks 1-3 at $8000-$DFFF — exactly where cc65's pce crt0
10
+ # TAMs them (MPR4=bank1, MPR5=bank2, MPR6=bank3) before calling main().
11
+ SYMBOLS {
12
+ __CARTSIZE__: type = weak, value = $8000; # crt0 compares >$8000 vs this
13
+ __STACKSIZE__: type = weak, value = $0300; # 3 pages stack
14
+ }
15
+ MEMORY {
16
+ ZP: file = "", start = $0000, define = yes, size = $0100;
17
+ # RAM bank ($F8 at MPR1)
18
+ MAIN: file = "", start = $2200, define = yes, size = $1E00 - __STACKSIZE__;
19
+ # HuCard bank 0 — hardware maps it at $E000 (MPR7) at reset. File offset 0.
20
+ ROM0: file = %O, start = $E000, size = $2000, fill = yes, fillval = $FF;
21
+ # HuCard banks 1-3 — crt0 maps them at $8000/$A000/$C000 (MPR4/5/6).
22
+ ROM: file = %O, start = $8000, size = $6000, fill = yes, fillval = $FF;
23
+ }
24
+ SEGMENTS {
25
+ ZEROPAGE: load = ZP, type = zp;
26
+ EXTZP: load = ZP, type = zp, optional = yes;
27
+ APPZP: load = ZP, type = zp, optional = yes;
28
+ DATA: load = ROM0, run = MAIN, type = rw, define = yes;
29
+ INIT: load = MAIN, type = bss, optional = yes;
30
+ BSS: load = MAIN, type = bss, define = yes;
31
+ LOWCODE: load = ROM0, type = ro, optional = yes;
32
+ ONCE: load = ROM0, type = ro, optional = yes;
33
+ CODE: load = ROM, type = ro;
34
+ RODATA: load = ROM, type = ro;
35
+ STARTUP: load = ROM0, type = ro, start = $FFF6 - $0066;
36
+ VECTORS: load = ROM0, type = ro, start = $FFF6;
37
+ }
38
+ FEATURES {
39
+ CONDES: type = constructor,
40
+ label = __CONSTRUCTOR_TABLE__,
41
+ count = __CONSTRUCTOR_COUNT__,
42
+ segment = ONCE;
43
+ CONDES: type = destructor,
44
+ label = __DESTRUCTOR_TABLE__,
45
+ count = __DESTRUCTOR_COUNT__,
46
+ segment = RODATA;
47
+ CONDES: type = interruptor,
48
+ label = __INTERRUPTOR_TABLE__,
49
+ count = __INTERRUPTOR_COUNT__,
50
+ segment = RODATA,
51
+ import = __CALLIRQ__;
52
+ }
@@ -37,7 +37,7 @@ const CC65_TARGET = {
37
37
  const LANGUAGE_TOOLCHAIN = {
38
38
  atari2600: {
39
39
  asm: { toolchain: "dasm", available: true },
40
- basic: { toolchain: "batariBasic", available: false, note: "BASIC for 2600 via batariBasic — not bundled. bB's transpiler is written in Perl, which we don't ship as WASM. A port to C or JS would be a multi-day project. For now, write 2600 games in 6507 asm via dasm — the bundled scaffolds (default, paddle, single_screen) show the canonical race-the-beam pattern, and an LLM agent writes 2600 asm fluently." },
40
+ basic: { toolchain: "batariBasic", available: false, note: "BASIC for 2600 via batariBasic — not bundled. bB's transpiler is written in Perl, which we don't ship as WASM. A port to C or JS would be a multi-day project. For now, write 2600 games in 6507 asm via dasm — the bundled example games (default, paddle, single_screen) show the canonical race-the-beam pattern, and an LLM agent writes 2600 asm fluently." },
41
41
  },
42
42
  nes: {
43
43
  asm: { toolchain: "cc65", available: true },
@@ -65,11 +65,11 @@ const LANGUAGE_TOOLCHAIN = {
65
65
  },
66
66
  snes: {
67
67
  asm: { toolchain: "asar", available: true },
68
- c: { toolchain: "tcc816+wladx", available: true, note: "C for SNES via tcc-65816 + wla-65816 + wlalink. The PVSnesLib runtime IS bundled (built from source) and auto-linked — #include <snes.h> gives you consoleDrawText, setMode, oamSet, WaitForVBlank, etc. out of the box. `createGame`/`createProject` scaffold a complete PVSnesLib C project. Pass options.pvsneslib:false for the bare-main minimum-viable path." },
68
+ c: { toolchain: "tcc816+wladx", available: true, note: "C for SNES via tcc-65816 + wla-65816 + wlalink. The PVSnesLib runtime IS bundled (built from source) and auto-linked — #include <snes.h> gives you consoleDrawText, setMode, oamSet, WaitForVBlank, etc. out of the box. `examples({op:'fork'})` gives you a complete working PVSnesLib C project. Pass options.pvsneslib:false for the bare-main minimum-viable path." },
69
69
  },
70
70
  genesis: {
71
71
  asm: { toolchain: "vasm68k", available: true },
72
- c: { toolchain: "m68k-elf-gcc", available: true, note: "C for Genesis via gcc 14.2.0 + binutils + newlib, all compiled to WASM. The SGDK runtime IS bundled (built from source) and auto-linked — sprite engine, VDP, controller, PSG/Z80 sound, resource helpers all work; #include <genesis.h>. `createGame`/`createProject` scaffold a complete SGDK C project (the recommended path). Pass options.sgdk:false for the bare-gcc minimum-viable path." },
72
+ c: { toolchain: "m68k-elf-gcc", available: true, note: "C for Genesis via gcc 14.2.0 + binutils + newlib, all compiled to WASM. The SGDK runtime IS bundled (built from source) and auto-linked — sprite engine, VDP, controller, PSG/Z80 sound, resource helpers all work; #include <genesis.h>. `examples({op:'fork'})` gives you a complete working SGDK C project (the recommended path). Pass options.sgdk:false for the bare-gcc minimum-viable path." },
73
73
  },
74
74
  gba: {
75
75
  c: { toolchain: "arm-none-eabi-gcc", available: true, note: "C for GBA via gcc 14.2.0 + binutils + newlib + libtonc 1.4.5 (default) OR libgba 0.5.4 (opt-in via runtime:\"libgba\"), all compiled to WASM (R24 + R28). #include <tonc.h> + tte_write/tte_printf works out of the box — that's the canonical Tonc-tutorial API every published GBA C resource uses. Caveat: tte_iohook (libtonc) and console.c (libgba) — the libsysbase-backed iprintf bridges — are NOT bundled. Use tte_printf directly, which is what the Tonc tutorial actually does." },
@@ -91,7 +91,7 @@ const LANGUAGE_TOOLCHAIN = {
91
91
  * Default language per platform. The choice reflects what's fastest /
92
92
  * smallest / best-matched to LLM fluency. Every platform that has a bundled
93
93
  * C compiler + runtime defaults to C — that's the canonical, productive path
94
- * and what `createGame`/`createProject` scaffold (cc65 for NES/C64/Atari7800/
94
+ * and what `examples({op:'fork'})` projects use (cc65 for NES/C64/Atari7800/
95
95
  * Lynx, SDCC for GB/GBC/SMS/GG, gcc+SGDK for Genesis, tcc+PVSnesLib for SNES,
96
96
  * gcc+libtonc for GBA). Platforms whose only bundled toolchain is an assembler
97
97
  * default to asm (Atari 2600 → dasm; SNES/Genesis keep an asm option too, but
@@ -743,10 +743,9 @@ export async function buildForPlatform(args) {
743
743
  // crt0 + headers + sources come straight from the caller. The build
744
744
  // pipeline does NOT auto-inject platform runtimes, custom crt0s,
745
745
  // or post-link header patches. Every byte that compiles is visible
746
- // to the caller's repo. Use `createProject({platform, template})`
747
- // to scaffold a self-contained project with the runtime files
748
- // copied in, or call `getStarterSnippet` / `getAllStarterSnippets`
749
- // to fetch individual pieces.
746
+ // to the caller's repo. Use `examples({op:'fork'})` to get a
747
+ // self-contained project with the runtime files copied in, or
748
+ // `examples({op:'snippets'/'copySnippets'})` to fetch individual pieces.
750
749
  const crt0 = args.crt0;
751
750
 
752
751
  // Pre-flight lint: scan the C sources for known SDCC C89 violations
@@ -787,10 +786,27 @@ export async function buildForPlatform(args) {
787
786
  // and RAM-size ($0149) bytes — without -m/-r, -v leaves them at the
788
787
  // linker's garbage pad (e.g. type $3C), and emulators/hardware reject
789
788
  // an unknown MBC type with "retro_load_game failed". -m 0x00 = ROM ONLY
790
- // (no mapper), -r 0x00 = no cart RAM — correct for these 32KB scaffolds.
789
+ // (no mapper), -r 0x00 = no cart RAM — correct for plain 32KB builds.
790
+ //
791
+ // Battery-cart passthrough (0.29.0 examples): a crt0 may DECLARE the
792
+ // cart in the header window (the GB equivalent of the NES crt0's iNES
793
+ // BATTERY bit — see the gbc lib gb_crt0.s, which emits $0147=$03 /
794
+ // $0149=$02 for MBC1+RAM+BATTERY so hi-scores persist in SAVE_RAM).
795
+ // If the linked image carries a KNOWN battery-MBC type byte with a
796
+ // sane RAM size, pass those through to rgbfix instead of stomping
797
+ // them to ROM-only; anything unrecognized (linker pad garbage) still
798
+ // falls back to the safe ROM-only default, so crt0s that don't
799
+ // declare a cart behave exactly as before.
800
+ const BATTERY_CART_TYPES = new Set([0x03, 0x06, 0x0F, 0x10, 0x13, 0x1B, 0x1E]); // MBC1/2/3/5 +BATTERY variants
801
+ const declType = binary.length > 0x149 ? binary[0x147] : 0x00;
802
+ const declRam = binary.length > 0x149 ? binary[0x149] : 0x00;
803
+ const cartByte = BATTERY_CART_TYPES.has(declType) ? declType : 0x00;
804
+ const ramByte = cartByte !== 0x00 && declRam >= 0x01 && declRam <= 0x05 ? declRam : 0x00;
805
+ const mArg = "0x" + cartByte.toString(16).padStart(2, "0").toUpperCase();
806
+ const rArg = "0x" + ramByte.toString(16).padStart(2, "0").toUpperCase();
791
807
  const fixOpts = args.platform === "gbc"
792
- ? ["-v", "-p", "0xFF", "-C", "-m", "0x00", "-r", "0x00"]
793
- : ["-v", "-p", "0xFF", "-m", "0x00", "-r", "0x00"];
808
+ ? ["-v", "-p", "0xFF", "-C", "-m", mArg, "-r", rArg]
809
+ : ["-v", "-p", "0xFF", "-m", mArg, "-r", rArg];
794
810
  const fix = await runRgbfix({ rom: binary, options: fixOpts });
795
811
  if (fix.exitCode === 0 && fix.binary) {
796
812
  binary = fix.binary;