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
package/AGENTS.md CHANGED
@@ -242,14 +242,21 @@ your project dir, it lands at `./read_joystick.asm` (alongside
242
242
  `main.asm`), NOT under `./include/` or `./lib/`. Every platform
243
243
  follows the same flat layout.
244
244
 
245
- Because the layout is flat, **`build({output:'project', path, platform})` rebuilds the
246
- whole directory in one call no per-iteration file manifest.** It finds `main.c`
247
- (C / SGDK Genesis / GBA / cc65-C / SDCC-C) or `main.s` / `main.asm` (asm), links every
248
- `.c`/`.s` in the dir, treats `.h`/`.inc` as includes, and folds binary assets
249
- (`.bin/.chr/.pcm/.brr/.vgm/...`) in as `binaryIncludes`. So iterating an on-disk project is
250
- just `build({output:'project', path:'/my/proj', platform})` every time you don't re-send
251
- `sources`/`includes` each build. (Use `build({output:'rom'})` with explicit `sources` when
252
- the files aren't on disk, e.g. generated in-context.)
245
+ Because the layout is flat, **the simplest loop is `build({output:'run', path, platform})`
246
+ (build + load + run + screenshot in one call) or `build({output:'project', path, platform})`
247
+ (build the dir to a ROM) no per-iteration file manifest, on EVERY platform.** Point it at
248
+ a scaffolded directory and it does the right per-platform thing automatically: finds the
249
+ entry (`main.c` for C / SGDK Genesis / GBA / cc65-C / SDCC-C, or `main.s` / `main.asm` for
250
+ asm), routes the platform's crt0 correctly (e.g. GB/GBC `gb_crt0.s` via the cart-header path,
251
+ not as a plain source so no `gsinit` collision), applies the right linker preset
252
+ (e.g. NES `chr-ram-runtime`, which supplies the OAM/CHARS segments), skips SDK intermediates
253
+ (e.g. Genesis `sega.preprocessed.s`, the SNES SPC700 driver, any `*.upstream.*`), wires the
254
+ runtime (GBA libtonc/libgba/maxmod by what the source includes), routes `#include`d C/asm
255
+ siblings as includes, treats `.h`/`.inc` as includes, and folds binary assets
256
+ (`.bin/.chr/.pcm/.brr/.vgm/.xgc/...`) in as `binaryIncludes`. So iterating an on-disk project
257
+ is just `build({output:'run', path:'/my/proj', platform})` every time — you do **not** need to
258
+ enumerate `sources`/`includes`/`crt0Path`/`linkerConfig` by hand. (Use `build({output:'rom'})`
259
+ with explicit `sources` only when the files aren't on disk, e.g. generated in-context.)
253
260
 
254
261
  ## Supported platforms
255
262
 
@@ -881,7 +888,7 @@ OAM format: bytes per sprite are `[y, tileIndex, attributes, x]`.
881
888
 
882
889
  Three shapes, pick the one that matches what you're doing:
883
890
 
884
- - **`scaffold({op:'project', platform, name, path, template?})`** — writes a starter directory: `main.{c,asm,s}` (from `examples/<platform>/templates/`) + every runtime file the template depends on (headers, crt0, linker .cfg) + README + `.gitignore`. Self-contained: take it elsewhere and rebuild with stock cc65/sdcc, no romdev install needed. Defaults to `template:"default"` (smallest visible-and-runnable program); most tier-1 platforms also have `hello_sprite` + `tile_engine` + the 5 genre templates.
891
+ - **`scaffold({op:'project', platform, name, path, template?})`** — writes a starter directory: `main.{c,asm,s}` (from `examples/<platform>/templates/`) + every runtime file the template depends on (headers, crt0, linker .cfg) + README + `.gitignore`. Self-contained: take it elsewhere and rebuild with stock cc65/sdcc, no romdev install needed. Defaults to `template:"default"` (smallest visible-and-runnable program); most tier-1 platforms also have `hello_sprite` + `tile_engine` + the 5 genre templates. **Then build it in ONE call: `build({output:'run', path:<that dir>, platform})`** — the dir build applies the platform's recipe (crt0/linker-preset/runtime/intermediate-skip) automatically, so you never hand-wire `crt0Path`/`codeLoc`/`linkerConfig`. This scaffold→build path is verified to build + render on every platform/template.
885
892
 
886
893
  - **`scaffold({op:'project', ..., withSnippets: true})`** — same as above, **plus** drops every vetted starter snippet for the platform alongside main.c. Use when you want "main.c + every helper file ready to edit" in one shot, without picking a genre. Snippets that overlap with the template's runtime are skipped (no double-writes). Response includes `snippetsCopied: string[]`.
887
894
 
@@ -1044,7 +1051,7 @@ A few platform-tool quirks worth knowing up front:
1044
1051
  - **asar bank-border-crossed** can happen if your `org` + `dw` runs past $00FFFF. Native vectors are at $FFE4-$FFEE; emulation vectors at $FFF4-$FFFF. Use `scaffold({op:'snippets', platform: "snes", mode: "get", name: "lorom_header.asm"})` for the layout.
1045
1052
  - **cc65 (NES, C64, etc.) zero page** starts at $02. cc65 reserves $00-$01 for its runtime. Your first `.res 1` lands at $02, not $00. Use `symbols({op:'map'})` after `build({output:'romWithDebug'})` to confirm.
1046
1053
  - **NES pattern table cap = 256 tiles per nametable**. The tilemap index is 8-bit, so per-frame BG can use at most 256 unique tiles per pattern table. Auto-converting a busy illustration usually overflows. `encodeArt({stage:'tilemap'})` warns; the only workaround is mid-frame CHR bank switching (MMC3-class mapper).
1047
- - **NES + GB/GBC turnkey** (R9/R10 self-contained + sound, 2026-05-25): use `scaffold({op:'project', platform, template, name, path})` to scaffold a project. The pipeline copies every file the template depends on — `{nes,gb}_runtime.{h,c}`, `gb_hardware.h`, custom `crt0.s`, linker `.cfg`, `patch-header.js` (GB) — into the project directory alongside `main.c`. **No auto-injection at build time.** The build pipeline compiles exactly what you tell it via `sources` / `sourcesPaths` / `includes` / `includePaths` / `crt0` / `crt0Path` / `linkerConfig` / `codeLoc`. Take the project elsewhere with stock cc65/sdcc and it builds the same way. The runtime APIs include sprites, BG, input, AND **sound** — `sound_init` / `sound_play_tone(channel, period, vol, length)` / `sound_play_noise` / `sound_off`. NES drives pulse1+pulse2+triangle+noise via $4000-$400F + $4015; GB drives the 4-channel APU via NR10-NR52. SFX-grade, fire-and-forget — for full music tracks, drop in famitone2 (NES) or your own driver. Templates: `default` (palette cycle), `hello_sprite` (sprite + d-pad + **beep on A press**), `tile_engine` (multi-room tile map). Docs: [`src/platforms/nes/MENTAL_MODEL.md`](src/platforms/nes/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/nes/TROUBLESHOOTING.md); [`src/platforms/gb/MENTAL_MODEL.md`](src/platforms/gb/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/gb/TROUBLESHOOTING.md). **Game-loop order matters on NES:** stage `oam_clear`+`oam_spr` BEFORE `ppu_wait_nmi`, not after — the NMI handler DMA's whatever shadow_oam contains at vblank-start. **GB ROM header:** both asm and C builds now auto-run `rgbfix` inside `build({output:'rom'})`, so the Nintendo logo + checksums + CGB flag are correct out of the box — no manual header-patch step needed (use `romPatch({op:'gbHeader'})` only to fix up an external ROM).
1054
+ - **NES + GB/GBC turnkey** (R9/R10 self-contained + sound, 2026-05-25): use `scaffold({op:'project', platform, template, name, path})` to scaffold a project. The pipeline copies every file the template depends on — `{nes,gb}_runtime.{h,c}`, `gb_hardware.h`, custom `crt0.s`, linker `.cfg`, `patch-header.js` (GB) — into the project directory alongside `main.c`. **No auto-injection at build time.** Iterate the whole dir in one call with `build({output:'run', path, platform})` (or supply `sources` / `sourcesPaths` / `includes` / `includePaths` / `crt0` / `crt0Path` / `linkerConfig` / `codeLoc` explicitly when the files aren't on disk). Take the project elsewhere with stock cc65/sdcc and it builds the same way. The runtime APIs include sprites, BG, input, AND **sound** — `sound_init` / `sound_play_tone(channel, period, vol, length)` / `sound_play_noise` / `sound_off`. NES drives pulse1+pulse2+triangle+noise via $4000-$400F + $4015; GB drives the 4-channel APU via NR10-NR52. SFX-grade, fire-and-forget — for full music tracks, drop in famitone2 (NES) or your own driver. Templates: `default` (palette cycle), `hello_sprite` (sprite + d-pad + **beep on A press**), `tile_engine` (multi-room tile map). Docs: [`src/platforms/nes/MENTAL_MODEL.md`](src/platforms/nes/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/nes/TROUBLESHOOTING.md); [`src/platforms/gb/MENTAL_MODEL.md`](src/platforms/gb/MENTAL_MODEL.md) + [`TROUBLESHOOTING.md`](src/platforms/gb/TROUBLESHOOTING.md). **NES sprite staging:** the bundled `nes_runtime` now handles the OAM-DMA race for you (oam_clear just resets the index; ppu_wait_nmi hides unused slots after you stage), so the old "must stage before ppu_wait_nmi" flicker trap is gone — the standard `oam_clear oam_spr ppu_wait_nmi` loop renders cleanly. **GB ROM header:** both asm and C builds auto-run `rgbfix` inside `build({output:'rom'/'run'})`, so the Nintendo logo + checksums + CGB flag are correct out of the box — no manual header-patch step needed (use `romPatch({op:'gbHeader'})` only to fix up an external ROM).
1048
1055
  - **Game Boy / GBC silent-failure footguns** (R54 cleanup, full detail in `platform({op:'doc', platform:"gb"|"gbc", name:"mental_model"})`):
1049
1056
  - **The bundled `gb_crt0.s` is now actually linked.** Pre-r54 a fundamental bug in `buildZ80C` was shipping the raw .s text to sdld as if pre-assembled — sdld silently rejected it and fell back to SDCC's stock sm83 crt0 (no GB cart boot, no IRQ vectors). Map showed no `init` symbol, $0000 was $FF, $0100 was $FF. Every GB ROM ran on stock crt0 invisibly. Fixed by auto-detecting .s source vs .rel object and running it through sdasgb first. Post-fix: `init` at $0150, entry $0100 = `00 c3 50 01` (nop; jp $0150), reset vector $0000 = $C9. **This was the root cause for #14 audio AND part of why every previous "runtime should work OOTB" round still felt friction-heavy.**
1050
1057
  - **GB/GBC C builds now auto-fix the header at build time** (rgbfix runs inside `build({output:'rom'})`): Nintendo logo at $0104, header checksum at $014D, global checksum, and the CGB flag at $0143 ($00 for `.gb`, $C0 for `.gbc`). You no longer need to patch the header manually — the ROM `build({output:'rom'})` hands back boots on real hardware as-is. `romPatch({op:'gbHeader', path})` still exists if you want to override title / cart type / RAM size / etc. on an existing file.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,69 @@ All notable changes to `romdevtools`. Dates are release dates.
4
4
  (Published as `romdev-mcp` through 0.11.0; renamed to `romdevtools` in 0.13.0 —
5
5
  the `romdev-mcp` bin is kept as an alias.)
6
6
 
7
+ ## 0.15.0
8
+
9
+ **Scaffold audit: every scaffold on every platform now builds AND renders.** Two
10
+ independent multi-agent audits found the documented "scaffold then build the dir"
11
+ happy path was broken on most platforms, and that many scaffolds built but
12
+ rendered blank. Both are fixed — verified 115/115 templates build via the dir
13
+ path, and every genre game renders recognizable content. This matters most for
14
+ weaker agents: the first build now succeeds, so they build on a working example
15
+ instead of assuming the server is broken and installing their own tools.
16
+
17
+ ### Fixed — project-directory build (the linchpin)
18
+ - **`build({output:'run'|'project', path})` now works on EVERY platform.** It was
19
+ 0/8 on GB/GBC/NES/Genesis and 0/5 on Atari 2600 via the natural path. The
20
+ dir-builder used to glob every file as a source; it now applies a per-platform
21
+ recipe that matches a hand-written build: routes the crt0 correctly (GB/GBC
22
+ `gb_crt0.s` via the cart-header path — no more `Multiple definition of gsinit`),
23
+ applies the linker preset (NES `chr-ram-runtime` — no more `Missing memory area
24
+ 'OAM'`), skips SDK intermediates (Genesis `sega.preprocessed.s`, the SNES SPC700
25
+ driver, any `*.upstream.*`), wires the GBA runtime (libtonc/libgba/maxmod by
26
+ what the source includes), routes `#include`d C/asm siblings as includes, and
27
+ bundles every incbin asset (`.xgc`/`.vgz`/…). `output:'run'` accepts `path` too
28
+ (build+load+run+screenshot a dir in one call). One shared code path so the two
29
+ build routes can't drift. Locked in by `scaffold-build-happypath.test.js`.
30
+ - **SNES multi-`.c` builds** (genre scaffolds ship `main.c` + `snes_sfx.c`) — the
31
+ stale single-`.c` guard in `buildSnesC` is removed (the link path already
32
+ handled multiple TUs).
33
+
34
+ ### Fixed — scaffolds rendered blank/wrong (now show content)
35
+ - **SMS/GG**: `vdp_init` R6 default 0xFB→0xFF — sprite tiles now read from $2000
36
+ where scaffolds upload them (were reading the empty $0000 bank → invisible). GG
37
+ also: 64-byte GG palette format (scaffolds shipped 32) + visible-window coords.
38
+ - **Genesis**: genre scaffolds now call `VDP_linkSprites` (the SAT link bytes were
39
+ 0 = end-of-list → only slot 0 drew); shmup enemies spread across the field.
40
+ - **SNES**: `sports`/`racing` `oamSet` now uses byte offsets (`slot<<2`); a real
41
+ 4bpp console font is embedded (the stub made all text black) + the BG-base /
42
+ text-offset / `consoleVblank` wiring fixed → c_hello/puzzle/platformer show text.
43
+ - **Lynx**: `platformer`/`puzzle`/`sports`/`racing` use the `while(tgi_busy()){}` +
44
+ full-screen `tgi_bar` clear (were black via bare `tgi_clear()`).
45
+ - **C64**: genre sprite data moved $0800→$2000 (it collided with the $0801 `.prg`
46
+ load → `sports` crashed to BASIC, sprites corrupt).
47
+ - **NES**: the bundled runtime no longer races the OAM-DMA — `oam_clear` resets the
48
+ index and `ppu_wait_nmi` hides unused slots after staging, so sprite-light
49
+ scaffolds no longer flicker to black; `default` backdrop no longer cycles to black.
50
+ - **PCE**: `pce_video.c` now programs the VDC display timing (NTSC 256×224) — fixes
51
+ the vertically-doubled picture.
52
+ - **GBA**: per-frame `tte_printf("%05d")` (broken in the bundled libtonc — garbles +
53
+ wedges the loop) replaced with a hand-built score string + `tte_write`.
54
+ - **Atari 2600**: `paddle` kernel rebuilt to a 2-line kernel (the per-line work
55
+ overflowed a 76-cycle scanline → ~250 lines, no vsync lock); now stable at 210.
56
+
57
+ ### Changed
58
+ - **Doc-drift swept**: every agent-facing `runSource`/`buildSource` → the current
59
+ `build({output:'run'|'rom'})`, dead tool names (`loadCategory`, `inspectSprites`,
60
+ …) → the consolidated forms, across the scaffold README emitter, `nextStep`,
61
+ examples, platform docs, and `.cfg` comments. Emitted scaffold-README `frames`
62
+ 60→240 (60 caught the boot logo on some platforms → false "blank").
63
+ - **Thin genres** given a BG world (Genesis sports court / racing road, NES racing
64
+ road, SNES sports/racing) so they read as the genre, not objects on black.
65
+ - **Missing-genre error** for msx/pce/atari2600 now names the working project
66
+ templates instead of a bare "default".
67
+ - The `uint8-loop-bound` preflight lint is scope-aware (no longer false-flags a
68
+ `uint16_t` loop counter that shares a name with a `uint8_t` in another function).
69
+
7
70
  ## 0.14.0
8
71
 
9
72
  **Two platform-specific top-level tools folded into their domain verbs, a
@@ -3,7 +3,7 @@
3
3
  ; standard NTSC frame timing (3 vsync + 37 vblank + 192 visible + 30
4
4
  ; overscan). Read joystick to move the sprite.
5
5
  ;
6
- ; Build: buildSource({ platform: "atari2600", source: <this file> })
6
+ ; Build: build({ output: "rom", platform: "atari2600", source: <this file> })
7
7
 
8
8
  processor 6502
9
9
  org $F000
@@ -3,7 +3,7 @@
3
3
  ; standard NTSC frame timing (3 vsync + 37 vblank + 192 visible + 30
4
4
  ; overscan). Read joystick to move the sprite.
5
5
  ;
6
- ; Build: buildSource({ platform: "atari2600", source: <this file> })
6
+ ; Build: build({ output: "rom", platform: "atari2600", source: <this file> })
7
7
 
8
8
  processor 6502
9
9
  org $F000
@@ -111,9 +111,14 @@ MAIN:
111
111
  STA VSYNC
112
112
 
113
113
  ; ── VBLANK (37 lines) — game logic ────────────────────────────────
114
+ ; 34 here + the 3 STA WSYNC in the P0/P1 positioning block below = 37 VBLANK
115
+ ; lines total. (Bug fix: this loop used to be 37 AND the positioning added 3
116
+ ; more → 265 scanlines/frame → the TV/emulator can't lock vsync → rolling /
117
+ ; black picture. Exactly 262 lines = 3 VSYNC + 37 VBLANK + 192 visible + 30
118
+ ; overscan; the positioning WSYNCs MUST be counted against the 37.)
114
119
  LDA #2
115
120
  STA VBLANK
116
- LDX #37
121
+ LDX #34
117
122
  .vb:
118
123
  STA WSYNC
119
124
  DEX
@@ -212,93 +217,100 @@ MAIN:
212
217
  STA AUDV0
213
218
  .sfx_done:
214
219
 
215
- ; Position P0 horizontally at column 16 (race-the-beam delay)
216
- STA WSYNC
217
- LDA #1
220
+ ; ── Position P0 / P1 / HMOVE — exactly 3 WSYNC-bounded lines ───────
221
+ ; CRITICAL: every RESPx write AND the STA HMOVE must complete inside
222
+ ; the 76-cycle scanline that began with its STA WSYNC. A DEX/BNE delay
223
+ ; loop costs 5 cycles/iteration, so the loop count must be small enough
224
+ ; that RESPx still lands before the line ends. The old code used
225
+ ; LDX #38 (~189 cycles = 2.5 scanlines!) with no WSYNC before RESP1/
226
+ ; HMOVE, so it emitted ~2-3 UNCOUNTED scanlines past the 262 budget →
227
+ ; ~265 lines/frame → vsync never locks (rolling magenta band). HMOVE
228
+ ; was also issued mid-line; it must follow a fresh WSYNC.
229
+
230
+ ; Line 1 of 3: coarse-position P0 (left, ~column 16)
218
231
  STA WSYNC
219
- LDX #15
232
+ LDX #5
220
233
  .p0d:
221
234
  DEX
222
- BNE .p0d
235
+ BNE .p0d ; ~24 cycles in → P0 lands near the left edge
223
236
  STA RESP0
224
- ; Position P1 at column 144
237
+ ; Line 2 of 3: coarse-position P1 (right, ~column 132)
225
238
  STA WSYNC
226
- LDX #38
239
+ LDX #13
227
240
  .p1d:
228
241
  DEX
229
- BNE .p1d
242
+ BNE .p1d ; ~64 cycles in (< 76) → P1 lands near the right
230
243
  STA RESP1
244
+ ; Line 3 of 3: apply HMOVE on a FRESH line, right after WSYNC
245
+ STA WSYNC
231
246
  STA HMOVE
232
247
 
233
248
  LDA #0
234
249
  STA VBLANK
235
250
 
236
- ; ── Visible (192 lines) ───────────────────────────────────────────
251
+ ; ── Visible (192 lines) — TWO-LINE KERNEL ─────────────────────────
252
+ ; CRITICAL: a single scanline is only 76 CPU cycles. The full per-line
253
+ ; render here (playfield walls + P0 + P1 + ball, each a SEC/SBC/CMP +
254
+ ; conditional store) is ~88 cycles — it does NOT fit in one line. In a
255
+ ; 1-line kernel each WSYNC iteration then spills past the line boundary,
256
+ ; so 192 iterations stretch to ~232 emitted lines → ~250-line frame →
257
+ ; vsync never locks (rolling magenta band — THE bug).
258
+ ;
259
+ ; The fix is the standard 2600 "2-line kernel": each loop pass renders
260
+ ; TWO scanlines and splits the work across two WSYNCs, doubling the
261
+ ; budget to ~152 cycles. 96 passes × 2 lines = 192 visible lines.
262
+ ; Y counts 192→2 in steps of 2; paddles/ball move in 2px steps (fine
263
+ ; for Pong). The branchless "LDA #off / CMP / BCS skip / LDA #on" form
264
+ ; also drops the JMPs the old code paid every line.
237
265
  LDY #192
238
266
  .draw:
267
+ ; ---- first line of the pair: playfield walls + left paddle ----
239
268
  STA WSYNC
240
- TYA
241
- ; Top-bottom walls: lines 0..3 and 188..191 draw a full-width PF
242
- CMP #4
243
- BCS .nottop
244
- LDA #$FF
245
- STA PF0
246
- STA PF1
247
- STA PF2
248
- JMP .pf_done
249
- .nottop:
250
- CMP #188
251
- BCC .notbot
252
- LDA #$FF
253
- STA PF0
254
- STA PF1
255
- STA PF2
256
- JMP .pf_done
257
- .notbot:
269
+ ; Top + bottom walls: full-width PF on the outer rows (Y>=189 / Y<5)
258
270
  LDA #0
271
+ CPY #189
272
+ BCS .wall
273
+ CPY #5
274
+ BCS .nowall
275
+ .wall:
276
+ LDA #$FF
277
+ .nowall:
259
278
  STA PF0
260
279
  STA PF1
261
280
  STA PF2
262
- .pf_done:
263
281
  ; Left paddle: 8 lines starting at P0_Y
264
282
  TYA
265
283
  SEC
266
284
  SBC P0_Y
267
285
  CMP #8
268
- BCS .p0blank
269
- LDA #$FF
270
- STA GRP0
271
- JMP .p0done
272
- .p0blank:
273
286
  LDA #0
287
+ BCS .p0off
288
+ LDA #$FF
289
+ .p0off:
274
290
  STA GRP0
275
- .p0done:
291
+ ; ---- second line of the pair: right paddle + ball ----
292
+ STA WSYNC
276
293
  ; Right paddle: 8 lines starting at P1_Y
277
294
  TYA
278
295
  SEC
279
296
  SBC P1_Y
280
297
  CMP #8
281
- BCS .p1blank
282
- LDA #$FF
283
- STA GRP1
284
- JMP .p1done
285
- .p1blank:
286
298
  LDA #0
299
+ BCS .p1off
300
+ LDA #$FF
301
+ .p1off:
287
302
  STA GRP1
288
- .p1done:
289
303
  ; Ball: 2 lines starting at BALL_Y
290
304
  TYA
291
305
  SEC
292
306
  SBC BALL_Y
293
307
  CMP #2
294
- BCS .blblank
295
- LDA #2
296
- STA ENABL
297
- JMP .bldone
298
- .blblank:
299
308
  LDA #0
309
+ BCS .bloff
310
+ LDA #2
311
+ .bloff:
300
312
  STA ENABL
301
- .bldone:
313
+ DEY
302
314
  DEY
303
315
  BNE .draw
304
316
 
@@ -1,6 +1,6 @@
1
1
  // ── Hello, Atari 7800 — MARIA bring-up + single sprite ──────────────
2
2
  //
3
- // Build: buildSource({ platform: "atari7800", source: <this file>,
3
+ // Build: build({ output: "rom", platform: "atari7800", source: <this file>,
4
4
  // language: "c" })
5
5
  //
6
6
  // Sets up a minimal display list, paints the background blue and renders
@@ -34,7 +34,7 @@
34
34
  * the 4KB of internal RAM.
35
35
  *
36
36
  * ── Build ───────────────────────────────────────────────────────
37
- * buildSource({ platform: "atari7800", sources: { "main.c": ... } })
37
+ * build({ output: "rom", platform: "atari7800", sources: { "main.c": ... } })
38
38
  */
39
39
  #include <stdint.h>
40
40
  #include "atari7800_sfx.h"
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * The note tables live in atari7800_music.c — they ARE the song.
12
12
  *
13
- * Build: buildSource({ platform: "atari7800", template: "music_demo" })
13
+ * Build: build({ output: "rom", platform: "atari7800", template: "music_demo" })
14
14
  */
15
15
  #include <stdint.h>
16
16
  #include "atari7800_music.h"
@@ -4,7 +4,7 @@
4
4
  // directly: writes screen codes into screen RAM, sets per-cell colors
5
5
  // in color RAM, and cycles the border color in a main loop.
6
6
  //
7
- // Build: buildSource({ platform: "c64", source: <this file>, language: "c" })
7
+ // Build: build({ output: "rom", platform: "c64", source: <this file>, language: "c" })
8
8
 
9
9
  #include <stdint.h>
10
10
 
@@ -80,7 +80,7 @@ static void wait_vblank(void) {
80
80
 
81
81
  static void copy_sprite(uint8_t slot, const uint8_t *data) {
82
82
  uint8_t i;
83
- volatile uint8_t *dst = (volatile uint8_t*)(0x0800 + slot * 64);
83
+ volatile uint8_t *dst = (volatile uint8_t*)(0x2000 + slot * 64); /* $2000, not $0800 (collides w/ $0801 .prg) */
84
84
  for (i = 0; i < 64; i++) dst[i] = data[i];
85
85
  }
86
86
 
@@ -131,7 +131,7 @@ void main(void) {
131
131
 
132
132
  POKE(VIC_SPR_ENA, 0);
133
133
  copy_sprite(0, player_sprite);
134
- SPRITE_POINTERS[0] = 0x20;
134
+ SPRITE_POINTERS[0] = 0x80; /* $2000/64 */
135
135
  POKE(VIC_SPR_COL(0), 0x07); /* yellow player */
136
136
  POKE(VIC_BORDER, 0x06); /* dark blue */
137
137
  POKE(VIC_BG0, 0x06);
@@ -121,7 +121,7 @@ static void lock_piece(void) {
121
121
  grid[r][c] = 0;
122
122
  grid[r][c+1] = 0;
123
123
  grid[r][c+2] = 0;
124
- if (score < 65500) score += 30;
124
+ if (score < 65500u) score += 30;
125
125
  sfx_tone(0, 0x80, 0x10, 12);
126
126
  }
127
127
  }
@@ -49,7 +49,7 @@ static void wait_vblank(void) {
49
49
 
50
50
  static void copy_sprite(uint8_t slot, const uint8_t *data) {
51
51
  uint8_t i;
52
- volatile uint8_t *dst = (volatile uint8_t*)(0x0800 + slot * 64);
52
+ volatile uint8_t *dst = (volatile uint8_t*)(0x2000 + slot * 64); /* $2000, not $0800 (collides w/ $0801 .prg) */
53
53
  for (i = 0; i < 64; i++) dst[i] = data[i];
54
54
  }
55
55
 
@@ -75,8 +75,8 @@ void main(void) {
75
75
  uint8_t i, pad;
76
76
  POKE(VIC_SPR_ENA, 0);
77
77
  copy_sprite(0, car_sprite);
78
- SPRITE_POINTERS[0] = 0x20;
79
- for (i = 0; i < MAX_OBS; i++) SPRITE_POINTERS[1 + i] = 0x20;
78
+ SPRITE_POINTERS[0] = 0x80; /* $2000/64 */
79
+ for (i = 0; i < MAX_OBS; i++) SPRITE_POINTERS[1 + i] = 0x80;
80
80
  POKE(VIC_SPR_COL(0), 0x07); /* yellow player */
81
81
  for (i = 0; i < MAX_OBS; i++) POKE(VIC_SPR_COL(1 + i), 0x02); /* red obstacles */
82
82
  POKE(VIC_BORDER, 0x00);
@@ -20,7 +20,8 @@
20
20
  #define PEEK(addr) (*(volatile uint8_t*)(addr))
21
21
 
22
22
  #define SPRITE_POINTERS ((volatile uint8_t*)0x07F8)
23
- #define SPRITE_DATA_BASE 0x0800 /* sprite N data at $0800 + N*64 */
23
+ #define SPRITE_DATA_BASE 0x2000 /* sprite N data at $2000 + N*64 — NOT $0800,
24
+ * which collides with the $0801 .prg load (C64-1) */
24
25
 
25
26
  #define JOY_UP 0x01
26
27
  #define JOY_DOWN 0x02
@@ -121,9 +122,9 @@ void main(void) {
121
122
 
122
123
  /* Sprite pointers: slot N points at /64 index into the VIC bank.
123
124
  * Default bank = $0000-$3FFF. $0800 / 64 = 32 = $20. */
124
- SPRITE_POINTERS[SLOT_PLAYER] = 0x20;
125
- for (i = 0; i < MAX_BULLETS; i++) SPRITE_POINTERS[SLOT_BULLET0 + i] = 0x21;
126
- for (i = 0; i < MAX_ENEMIES; i++) SPRITE_POINTERS[SLOT_ENEMY0 + i] = 0x22;
125
+ SPRITE_POINTERS[SLOT_PLAYER] = 0x80; /* $2000/64 */
126
+ for (i = 0; i < MAX_BULLETS; i++) SPRITE_POINTERS[SLOT_BULLET0 + i] = 0x81;
127
+ for (i = 0; i < MAX_ENEMIES; i++) SPRITE_POINTERS[SLOT_ENEMY0 + i] = 0x82;
127
128
 
128
129
  POKE(VIC_SPR_COL(SLOT_PLAYER), 0x07); /* yellow */
129
130
  for (i = 0; i < MAX_BULLETS; i++) POKE(VIC_SPR_COL(SLOT_BULLET0 + i), 0x01); /* white */
@@ -175,7 +176,7 @@ void main(void) {
175
176
  if (aabb(&bullets[i], &enemies[j])) {
176
177
  bullets[i].alive = 0;
177
178
  enemies[j].alive = 0;
178
- if (score < 65500) score += 10;
179
+ if (score < 65500u) score += 10;
179
180
  sfx_noise(8);
180
181
  break;
181
182
  }
@@ -41,7 +41,7 @@ static void wait_vblank(void) {
41
41
 
42
42
  static void copy_sprite(uint8_t slot, const uint8_t *data) {
43
43
  uint8_t i;
44
- volatile uint8_t *dst = (volatile uint8_t*)(0x0800 + slot * 64);
44
+ volatile uint8_t *dst = (volatile uint8_t*)(0x2000 + slot * 64); /* $2000, not $0800 (collides w/ $0801 .prg) */
45
45
  for (i = 0; i < 64; i++) dst[i] = data[i];
46
46
  }
47
47
 
@@ -56,9 +56,9 @@ void main(void) {
56
56
  copy_sprite(0, paddle_sprite);
57
57
  copy_sprite(1, paddle_sprite);
58
58
  copy_sprite(2, ball_sprite);
59
- SPRITE_POINTERS[0] = 0x20;
60
- SPRITE_POINTERS[1] = 0x21;
61
- SPRITE_POINTERS[2] = 0x22;
59
+ SPRITE_POINTERS[0] = 0x80; /* $2000/64 */
60
+ SPRITE_POINTERS[1] = 0x81;
61
+ SPRITE_POINTERS[2] = 0x82;
62
62
  POKE(VIC_SPR_COL(0), 0x01); /* white */
63
63
  POKE(VIC_SPR_COL(1), 0x01);
64
64
  POKE(VIC_SPR_COL(2), 0x07); /* yellow ball */
@@ -4,7 +4,7 @@
4
4
  ; it to the BG map, enables the LCD, then loops reading joypad and
5
5
  ; scrolling the BG. Build with:
6
6
  ;
7
- ; buildSource({platform:"gb", source: <this>})
7
+ ; build({ output: "rom", platform:"gb", source: <this>})
8
8
  ;
9
9
  ; rgbfix gets applied automatically by buildForPlatform so the header
10
10
  ; checksum is valid.
@@ -1,7 +1,7 @@
1
1
  /* ── Hello, Game Boy in C — SDCC sm83 port ─────────────────────────
2
2
  * Minimal: cycle the BG palette on every vblank.
3
3
  *
4
- * Build: buildSource({ platform: "gb", source: <this file>, language: "c" })
4
+ * Build: build({ output: "rom", platform: "gb", source: <this file>, language: "c" })
5
5
  *
6
6
  * SDCC 4.4.0 codegen quirks to avoid in `__sfr __at` register-heavy
7
7
  * code:
@@ -118,7 +118,7 @@ static void lock_piece(void) {
118
118
  grid[r][c] = 0;
119
119
  grid[r][c + 1] = 0;
120
120
  grid[r][c + 2] = 0;
121
- if (score < 65500) score += 30;
121
+ if (score < 65500u) score += 30;
122
122
  }
123
123
  }
124
124
  }
@@ -153,6 +153,6 @@ void main(void) {
153
153
  break;
154
154
  }
155
155
  }
156
- if (score < 65500) score++;
156
+ if (score < 65500u) score++;
157
157
  }
158
158
  }
@@ -162,7 +162,7 @@ void main(void) {
162
162
  if (aabb(&bullets[i], &enemies[j])) {
163
163
  bullets[i].alive = 0;
164
164
  enemies[j].alive = 0;
165
- if (score < 65500) score += 10;
165
+ if (score < 65500u) score += 10;
166
166
  sound_play_noise(6);
167
167
  break;
168
168
  }
@@ -5,7 +5,7 @@
5
5
  * pixel that moves left/right via the d-pad.
6
6
  *
7
7
  * Build via romdev:
8
- * buildSource({platform:"gba", language:"c", runtime:"libgba",
8
+ * build({ output: "rom", platform:"gba", language:"c", runtime:"libgba",
9
9
  * source: <this file>})
10
10
  *
11
11
  * NOTE: the DEFAULT GBA runtime is libtonc (use `tonc_hello.c` instead
@@ -10,7 +10,7 @@
10
10
  * 4. mmStart(MOD_<NAME>_FROM_HEADER, mode) to begin playback.
11
11
  *
12
12
  * Build via romdev:
13
- * buildSource({
13
+ * build({ output: "rom",
14
14
  * platform: "gba",
15
15
  * language: "c",
16
16
  * source: <this file>,
@@ -22,6 +22,20 @@
22
22
  #include <tonc.h>
23
23
  #include "gba_sfx.h"
24
24
 
25
+ /* draw a 5-digit score WITHOUT tte_printf (broken in this libtonc — GBA-1). */
26
+ static void draw_score(int x, unsigned v) {
27
+ char buf[24];
28
+ int i, n = 0;
29
+ buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
30
+ if (x >= 100) buf[n++] = '0' + (x/100)%10;
31
+ if (x >= 10) buf[n++] = '0' + (x/10)%10;
32
+ buf[n++] = '0' + x%10;
33
+ buf[n++]=','; buf[n++]='8'; buf[n++]='}';
34
+ for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
35
+ n += 5; buf[n] = 0;
36
+ tte_write(buf);
37
+ }
38
+
25
39
  #define COLS 6
26
40
  #define ROWS 12
27
41
 
@@ -134,7 +148,7 @@ static void lock_piece(void) {
134
148
  grid[r][c] = 0;
135
149
  grid[r][c + 1] = 0;
136
150
  grid[r][c + 2] = 0;
137
- if (score < 65500) score += 30;
151
+ if (score < 65500u) score += 30;
138
152
  sfx_tone(1, 1700, 10); /* triple-clear chime */
139
153
  }
140
154
  }
@@ -211,7 +225,7 @@ int main(void) {
211
225
  new_piece();
212
226
  prev = now;
213
227
  tte_erase_rect(88 + 6*8, 8, 88 + 11*8, 16);
214
- tte_printf("#{P:%d,8}%05d", 88 + 6*8, score);
228
+ draw_score(88 + 6*8, score);
215
229
  continue;
216
230
  }
217
231
  prev = now;
@@ -231,7 +245,7 @@ int main(void) {
231
245
  draw_piece(piece_x, piece_y, 0);
232
246
 
233
247
  tte_erase_rect(88 + 6*8, 8, 88 + 11*8, 16);
234
- tte_printf("#{P:%d,8}%05d", 88 + 6*8, score);
248
+ draw_score(88 + 6*8, score);
235
249
  }
236
250
  return 0;
237
251
  }
@@ -15,6 +15,20 @@
15
15
  #include <tonc.h>
16
16
  #include "gba_sfx.h"
17
17
 
18
+ /* draw a 5-digit score WITHOUT tte_printf (broken in this libtonc — GBA-1). */
19
+ static void draw_score(int x, unsigned v) {
20
+ char buf[24];
21
+ int i, n = 0;
22
+ buf[n++]='#'; buf[n++]='{'; buf[n++]='P'; buf[n++]=':';
23
+ if (x >= 100) buf[n++] = '0' + (x/100)%10;
24
+ if (x >= 10) buf[n++] = '0' + (x/10)%10;
25
+ buf[n++] = '0' + x%10;
26
+ buf[n++]=','; buf[n++]='8'; buf[n++]='}';
27
+ for (i = 4; i >= 0; i--) { buf[n+i] = '0' + (v % 10); v /= 10; }
28
+ n += 5; buf[n] = 0;
29
+ tte_write(buf);
30
+ }
31
+
18
32
  #define LANE_LEFT_X 56
19
33
  #define LANE_MID_X 116
20
34
  #define LANE_RIGHT_X 176
@@ -149,7 +163,7 @@ int main(void) {
149
163
  }
150
164
  }
151
165
 
152
- if (score < 65500) score++;
166
+ if (score < 65500u) score++;
153
167
  }
154
168
 
155
169
  /* Sprite slots: 0 = player, 1..4 = obstacles. */
@@ -169,7 +183,7 @@ int main(void) {
169
183
  oam_copy(oam_mem, obj_buffer, 1 + MAX_OBSTACLES);
170
184
 
171
185
  tte_erase_rect(160 + 6*8, 8, 160 + 11*8, 16);
172
- tte_printf("#{P:%d,8}%05d", 160 + 6*8, score);
186
+ draw_score(160 + 6*8, score);
173
187
  }
174
188
  return 0;
175
189
  }