romdevtools 0.14.0 → 0.15.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 (96) hide show
  1. package/AGENTS.md +17 -10
  2. package/CHANGELOG.md +63 -0
  3. package/examples/atari2600/main.asm +1 -1
  4. package/examples/atari2600/templates/default.asm +1 -1
  5. package/examples/atari2600/templates/paddle.asm +59 -47
  6. package/examples/atari7800/main.c +1 -1
  7. package/examples/atari7800/templates/default.c +1 -1
  8. package/examples/atari7800/templates/music_demo.c +1 -1
  9. package/examples/c64/main.c +1 -1
  10. package/examples/c64/templates/platformer.c +2 -2
  11. package/examples/c64/templates/puzzle.c +1 -1
  12. package/examples/c64/templates/racing.c +3 -3
  13. package/examples/c64/templates/shmup.c +6 -5
  14. package/examples/c64/templates/sports.c +4 -4
  15. package/examples/gb/main.asm +1 -1
  16. package/examples/gb/main.c +1 -1
  17. package/examples/gb/templates/puzzle.c +1 -1
  18. package/examples/gb/templates/racing.c +1 -1
  19. package/examples/gb/templates/shmup.c +1 -1
  20. package/examples/gba/templates/gba_hello.c +1 -1
  21. package/examples/gba/templates/maxmod_demo.c +1 -1
  22. package/examples/gba/templates/puzzle.c +17 -3
  23. package/examples/gba/templates/racing.c +16 -2
  24. package/examples/gba/templates/shmup.c +23 -4
  25. package/examples/gba/templates/tonc_hello.c +6 -4
  26. package/examples/gbc/main.asm +1 -1
  27. package/examples/gbc/templates/puzzle.c +1 -1
  28. package/examples/gbc/templates/racing.c +1 -1
  29. package/examples/gbc/templates/shmup.c +1 -1
  30. package/examples/genesis/main.s +1 -1
  31. package/examples/genesis/templates/puzzle.c +1 -1
  32. package/examples/genesis/templates/racing.c +45 -1
  33. package/examples/genesis/templates/shmup.c +12 -3
  34. package/examples/genesis/templates/shmup_2p.c +2 -2
  35. package/examples/genesis/templates/sports.c +39 -0
  36. package/examples/gg/templates/hello_sprite.c +38 -23
  37. package/examples/gg/templates/music_demo.c +11 -8
  38. package/examples/gg/templates/platformer.c +37 -15
  39. package/examples/gg/templates/racing.c +25 -12
  40. package/examples/gg/templates/shmup.c +12 -6
  41. package/examples/gg/templates/sports.c +30 -16
  42. package/examples/gg/templates/tile_engine.c +24 -10
  43. package/examples/lynx/templates/platformer.c +7 -1
  44. package/examples/lynx/templates/puzzle.c +8 -2
  45. package/examples/lynx/templates/racing.c +7 -1
  46. package/examples/lynx/templates/sports.c +7 -1
  47. package/examples/nes/main.c +2 -2
  48. package/examples/nes/space-shooter/nes_runtime.h +1 -1
  49. package/examples/nes/templates/default.c +4 -1
  50. package/examples/nes/templates/racing.c +50 -1
  51. package/examples/pce/main.c +1 -1
  52. package/examples/sms/templates/hello_sprite.c +1 -1
  53. package/examples/sms/templates/music_demo.c +1 -1
  54. package/examples/sms/templates/puzzle.c +1 -1
  55. package/examples/sms/templates/racing.c +1 -1
  56. package/examples/sms/templates/shmup.c +1 -1
  57. package/examples/sms/templates/shmup_2p.c +2 -2
  58. package/examples/snes/main.asm +1 -1
  59. package/examples/snes/templates/c-hello-data.asm +309 -14
  60. package/examples/snes/templates/c-hello.c +13 -2
  61. package/examples/snes/templates/default.c +1 -1
  62. package/examples/snes/templates/hello_sprite-data.asm +300 -2
  63. package/examples/snes/templates/hello_sprite.c +10 -1
  64. package/examples/snes/templates/music_demo-data.asm +300 -2
  65. package/examples/snes/templates/music_demo.c +10 -1
  66. package/examples/snes/templates/platformer-data.asm +300 -2
  67. package/examples/snes/templates/platformer.c +10 -1
  68. package/examples/snes/templates/puzzle-data.asm +300 -2
  69. package/examples/snes/templates/puzzle.c +11 -1
  70. package/examples/snes/templates/racing-data.asm +300 -2
  71. package/examples/snes/templates/racing.c +40 -4
  72. package/examples/snes/templates/shmup-data.asm +299 -6
  73. package/examples/snes/templates/shmup.c +11 -7
  74. package/examples/snes/templates/sports-data.asm +300 -2
  75. package/examples/snes/templates/sports.c +40 -5
  76. package/package.json +1 -1
  77. package/src/mcp/tools/project.js +33 -22
  78. package/src/mcp/tools/toolchain.js +183 -19
  79. package/src/observer/livestream.html +34 -4
  80. package/src/platforms/gg/MENTAL_MODEL.md +14 -13
  81. package/src/platforms/gg/lib/c/vdp_init.c +10 -8
  82. package/src/platforms/msx/MENTAL_MODEL.md +1 -1
  83. package/src/platforms/nes/TROUBLESHOOTING.md +1 -1
  84. package/src/platforms/nes/lib/c/nes_runtime.c +28 -6
  85. package/src/platforms/pce/MENTAL_MODEL.md +1 -1
  86. package/src/platforms/pce/lib/c/pce_hw.h +1 -0
  87. package/src/platforms/pce/lib/c/pce_video.c +26 -0
  88. package/src/platforms/sms/MENTAL_MODEL.md +12 -12
  89. package/src/platforms/sms/lib/c/vdp_init.c +10 -8
  90. package/src/platforms/sms/lib/vdp_init.s +1 -1
  91. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +1 -1
  92. package/src/toolchains/cc65/presets/nes/chr-ram.cfg +1 -1
  93. package/src/toolchains/cc65/presets/nes/chr-ram.crt0.s +1 -1
  94. package/src/toolchains/genesis-c/README.md +1 -1
  95. package/src/toolchains/sdcc/preflight-lint.js +47 -7
  96. package/src/toolchains/snes-c/snes-c.js +3 -7
@@ -3,15 +3,17 @@
3
3
  * Writes the 11 mode-4 registers to a sane baseline:
4
4
  * display OFF, vblank IRQ off, 192-line mode 4, name table at $3800,
5
5
  * BG tile data at $0000, sprite attr table at $3F00, sprite tile data
6
- * at $0000 (R6=0xFB → SA13 clear → tiles read from $0000-$1FFF). Call
6
+ * at $2000 (R6=0xFF → SA13 set → tiles read from $2000-$3FFF). Call
7
7
  * once after reset before uploading palette/tiles/map.
8
8
  *
9
- * Footgun: many SMS references say "R6=0xFB sprite tiles at $2000"
10
- * which is BACKWARDS. R6 bit 2 (the SA13 select) is CLEAR in 0xFB, so
11
- * sprite tiles read from $0000 (sharing the bank with BG tiles). To
12
- * separate sprite tiles to $2000, set R6 = 0xFF instead. The
13
- * sprites({op:'inspect'}) tool's spriteTileDataBase field will show you the
14
- * real address the VDP is reading from.
9
+ * Sprite-tile base: R6 bit 2 (SA13) selects $0000 (clear) vs $2000 (set).
10
+ * We default to R6=0xFF ($2000) because EVERY bundled scaffold uploads its
11
+ * sprite tiles to $2000 (sms_load_tiles(0x2000, ...)) so the baseline must
12
+ * match what consumers do, or sprites read from the empty/BG bank and render
13
+ * invisible. (Many SMS references say "R6=0xFB $2000", which is backwards:
14
+ * 0xFB has SA13 CLEAR = $0000.) If you instead keep sprite tiles in the BG
15
+ * bank at $0000, set R6=0xFB. sprites({op:'inspect'}) → spriteTileDataBase
16
+ * shows the address the VDP is actually reading from.
15
17
  *
16
18
  * After loading assets, enable display by re-writing R1 with bit 6 set:
17
19
  * sms_vdp_display_on();
@@ -33,7 +35,7 @@ void sms_vdp_init(void) {
33
35
  0xFF, /* R3: color table (ignored in M4) */
34
36
  0xFF, /* R4: BG tile data at $0000 */
35
37
  0xFF, /* R5: sprite attr table at $3F00 */
36
- 0xFB, /* R6: sprite tile data at $0000 (set 0xFF for $2000) */
38
+ 0xFF, /* R6: sprite tile data at $2000 (SA13 set; scaffolds upload here) */
37
39
  0x00, /* R7: border = sprite palette entry 0 */
38
40
  0x00, /* R8: BG X scroll */
39
41
  0x00, /* R9: BG Y scroll */
@@ -40,7 +40,7 @@ _vdp_init_regs:
40
40
  .db $FF ; R3: color table — M4 ignores
41
41
  .db $FF ; R4: BG tile data — M4: bit 2 selects $0000 vs $2000
42
42
  .db $FF ; R5: sprite attr table base ($3F00 = $7E << 7)
43
- .db $FB ; R6: sprite tile data bit 2 selects bank ($2000 here)
43
+ .db $FF ; R6: sprite tile data at $2000 (SA13 set; scaffolds upload here)
44
44
  .db $00 ; R7: border color = sprite palette entry 0
45
45
  .db $00 ; R8: BG X scroll
46
46
  .db $00 ; R9: BG Y scroll
@@ -12,7 +12,7 @@
12
12
  # runtime. There is no CHARS segment.
13
13
  #
14
14
  # To use this preset:
15
- # 1. linkerConfig: "chr-ram" on buildSource
15
+ # 1. linkerConfig: "chr-ram" on build({output:'rom'})
16
16
  # 2. Supply your own crt0/header source that writes the 16-byte iNES
17
17
  # header with byte 5 = 0. Easiest: paste the snippet
18
18
  # getStarterSnippet({platform:"nes", name:"chr_ram_header", language:"asm"}).
@@ -12,7 +12,7 @@
12
12
  # runtime. There is no CHARS segment.
13
13
  #
14
14
  # To use this preset:
15
- # 1. linkerConfig: "chr-ram" on buildSource
15
+ # 1. linkerConfig: "chr-ram" on build({output:'rom'})
16
16
  # 2. Supply your own crt0/header source that writes the 16-byte iNES
17
17
  # header with byte 5 = 0. Easiest: paste the snippet
18
18
  # getStarterSnippet({platform:"nes", name:"chr_ram_header", language:"asm"}).
@@ -91,7 +91,7 @@ _exit: jsr donelib
91
91
  ; rti
92
92
  ;
93
93
  ; ; then DELETE the `nmi: rti` line below from this crt0 (or load a
94
- ; ; modified crt0 via your own buildSource sources entry).
94
+ ; ; modified crt0 via your own build({output:'rom'}) sources entry).
95
95
 
96
96
  .segment "STARTUP"
97
97
 
@@ -23,7 +23,7 @@ once the WASM artifacts ship. Pipeline shape:
23
23
  - **WASM port of cc1/as/ld (stage 2)**: pending.
24
24
  - **SGDK native build against the cross-toolchain (stage 3)**: pending.
25
25
  - **JS driver `buildGenesisC()`**: pending.
26
- - **buildSource wiring**: pending.
26
+ - **build({output:'rom'}) wiring**: pending.
27
27
 
28
28
  ## Why a 3-stage build
29
29
 
@@ -68,13 +68,53 @@ export function lintSdccSource(source, file = "main.c", opts = {}) {
68
68
  // CONSERVATIVE: only flag when we can SEE the counter declared u8 and
69
69
  // the bound is a constant we can evaluate — never guess.
70
70
  {
71
- // Collect names declared as 8-bit ints anywhere in the file.
72
- const u8re = /\b(?:unsigned\s+char|char|u8|uint8_t|uint8|int8_t|int8|signed\s+char)\s+([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)\s*;/g;
73
- const u8names = new Set();
74
- let dm;
75
- while ((dm = u8re.exec(source))) {
76
- for (const n of dm[1].split(",")) u8names.add(n.trim());
71
+ // Build a SCOPE-AWARE map of where each name is declared and at what
72
+ // width. A name like `i` is commonly re-declared in several functions
73
+ // some as uint8_t, some as uint16_t. A flat "is this name ever u8"
74
+ // set wrongly flags the uint16_t loop just because a DIFFERENT
75
+ // function declared its own `i` as uint8_t (the SMS/GG default
76
+ // scaffold false-positive). Instead we record EVERY declaration's
77
+ // line + width, then for each loop consult the nearest declaration of
78
+ // the counter that appears ABOVE the loop — i.e. the one actually in
79
+ // scope — and only flag it when that declaration is 8-bit.
80
+ //
81
+ // decls: Map<name, Array<{line:number, u8:boolean}>> (line is 1-based)
82
+ const decls = new Map();
83
+ const addDecl = (names, line, u8) => {
84
+ for (const raw of names.split(",")) {
85
+ const n = raw.trim();
86
+ if (!n) continue;
87
+ if (!decls.has(n)) decls.set(n, []);
88
+ decls.get(n).push({ line, u8 });
89
+ }
90
+ };
91
+ const u8re = /\b(?:unsigned\s+char|char|u8|uint8_t|uint8|int8_t|int8|signed\s+char)\s+([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)\s*;/;
92
+ // 16-bit-or-wider integer declarations (and float/double, which also
93
+ // can't overflow at 255). Anything not 8-bit is "wide" for our purpose.
94
+ const wideRe = /\b(?:unsigned\s+(?:short|int|long)|signed\s+(?:short|int|long)|short|int|long|u16|u32|u64|uint16_t|uint16|int16_t|int16|uint32_t|uint32|int32_t|int32|uint64_t|uint64|int64_t|int64|size_t|ptrdiff_t)\s+([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)\s*;/;
95
+ for (let i = 0; i < lines.length; i++) {
96
+ const code = lines[i].replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "");
97
+ const m8 = code.match(u8re);
98
+ if (m8) { addDecl(m8[1], i + 1, true); continue; }
99
+ const mw = code.match(wideRe);
100
+ if (mw) addDecl(mw[1], i + 1, false);
77
101
  }
102
+ // Is the counter declared 8-bit in the scope visible at `loopLine`?
103
+ // Use the NEAREST declaration above the loop. If the nearest visible
104
+ // declaration is wide (uint16_t etc.), the loop is fine.
105
+ const counterIsU8AtLine = (counter, loopLine) => {
106
+ const ds = decls.get(counter);
107
+ if (!ds) return false;
108
+ let best = null;
109
+ for (const d of ds) {
110
+ if (d.line <= loopLine && (best === null || d.line > best.line)) best = d;
111
+ }
112
+ // No declaration above the loop (e.g. param/global declared after, or
113
+ // out-of-order) — fall back to "flag only if EVERY decl is u8" so we
114
+ // never wolf-cry on a name that is also declared wide somewhere.
115
+ if (best === null) return ds.every((d) => d.u8);
116
+ return best.u8;
117
+ };
78
118
  const evalConst = (expr) => {
79
119
  // Only literals and pure `A * B [* C]` products of decimal/hex ints.
80
120
  const t = expr.trim();
@@ -91,7 +131,7 @@ export function lintSdccSource(source, file = "main.c", opts = {}) {
91
131
  const m = code.match(/\bfor\s*\([^;]*;\s*([A-Za-z_]\w*)\s*<\s*([^;]+?)\s*;/);
92
132
  if (!m) continue;
93
133
  const [, counter, boundExpr] = m;
94
- if (!u8names.has(counter)) continue;
134
+ if (!counterIsU8AtLine(counter, i + 1)) continue;
95
135
  const bound = evalConst(boundExpr);
96
136
  if (bound !== null && bound > 255) {
97
137
  issues.push({
@@ -148,13 +148,9 @@ function normalizeSnesSources(args) {
148
148
  if (cFiles.length === 0) {
149
149
  throw new Error("buildSnesC: `sources` must include at least one .c file.");
150
150
  }
151
- if (cFiles.length > 1) {
152
- throw new Error(
153
- `buildSnesC: multiple .c files in sources (${cFiles.join(", ")}). ` +
154
- `Today only one .c file is supported per build — combine via #include or wait for ` +
155
- `multi-TU support. .asm/.s siblings work fine.`,
156
- );
157
- }
151
+ // Multiple .c files ARE supported: buildWithPvSnesLib compiles each to its
152
+ // own .obj (tcc→wla) and links them all (Stage 1 + Stage 3). The genre
153
+ // scaffolds ship main.c + snes_sfx.c and rely on this.
158
154
  return args.sources;
159
155
  }
160
156
  if (typeof args.source === "string") {