romdevtools 0.21.0 → 0.22.1

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 (127) hide show
  1. package/AGENTS.md +15 -4
  2. package/CHANGELOG.md +66 -0
  3. package/examples/atari7800/templates/hello_sprite.c +48 -4
  4. package/examples/atari7800/templates/music_demo.c +47 -2
  5. package/examples/c64/templates/tile_engine.c +77 -27
  6. package/examples/gb/templates/hello_sprite.c +15 -6
  7. package/examples/gb/templates/music_demo.c +36 -0
  8. package/examples/gb/templates/platformer.c +3 -2
  9. package/examples/gb/templates/puzzle.c +3 -2
  10. package/examples/gb/templates/racing.c +3 -2
  11. package/examples/gb/templates/shmup.c +3 -2
  12. package/examples/gb/templates/sports.c +3 -2
  13. package/examples/gb/templates/tile_engine.c +3 -2
  14. package/examples/gba/templates/maxmod_demo.c +36 -2
  15. package/examples/gba/templates/platformer.c +3 -1
  16. package/examples/gba/templates/tonc_hello_sprite.c +35 -1
  17. package/examples/gbc/templates/hello_sprite.c +12 -3
  18. package/examples/gbc/templates/music_demo.c +56 -12
  19. package/examples/gbc/templates/platformer.c +3 -2
  20. package/examples/gbc/templates/puzzle.c +3 -2
  21. package/examples/gbc/templates/racing.c +3 -2
  22. package/examples/gbc/templates/shmup.c +3 -2
  23. package/examples/gbc/templates/sports.c +3 -2
  24. package/examples/gbc/templates/tile_engine.c +3 -2
  25. package/examples/genesis/main.s +53 -1
  26. package/examples/genesis/templates/hello_sprite.c +25 -3
  27. package/examples/genesis/templates/shmup_2p.c +31 -0
  28. package/examples/genesis/templates/xgm2_demo.c +20 -0
  29. package/examples/gg/templates/hello_sprite.c +25 -2
  30. package/examples/gg/templates/music_demo.c +24 -2
  31. package/examples/gg/templates/racing.c +7 -4
  32. package/examples/gg/templates/sports.c +11 -13
  33. package/examples/gg/templates/tile_engine.c +12 -6
  34. package/examples/lynx/templates/hello_sprite.c +15 -1
  35. package/examples/lynx/templates/music_demo.c +13 -1
  36. package/examples/nes/templates/hello_sprite.c +35 -0
  37. package/examples/nes/templates/music_demo.c +40 -0
  38. package/examples/pce/catch_game/main.c +22 -3
  39. package/examples/pce/music_sfx/main.c +28 -1
  40. package/examples/pce/sprite_move/main.c +7 -2
  41. package/examples/sms/templates/hello_sprite.c +29 -3
  42. package/examples/sms/templates/music_demo.c +18 -4
  43. package/examples/sms/templates/shmup_2p.c +24 -1
  44. package/examples/sms/templates/sports.c +4 -2
  45. package/examples/snes/main.asm +108 -17
  46. package/examples/snes/templates/c-hello-data.asm +23 -0
  47. package/examples/snes/templates/c-hello.c +18 -1
  48. package/examples/snes/templates/hello_sprite-data.asm +23 -0
  49. package/examples/snes/templates/hello_sprite.c +17 -1
  50. package/examples/snes/templates/music_demo-data.asm +23 -0
  51. package/examples/snes/templates/music_demo.c +22 -4
  52. package/examples/snes/templates/platformer.c +4 -1
  53. package/examples/snes/templates/puzzle.c +4 -1
  54. package/package.json +1 -1
  55. package/src/cheats/gamegenie.js +0 -1
  56. package/src/cli/smoke.js +1 -3
  57. package/src/host/LibretroHost.js +69 -15
  58. package/src/host/chafa-render.js +2 -0
  59. package/src/host/dsp-state.js +2 -2
  60. package/src/host/gpgx-state.js +4 -0
  61. package/src/http/routes.js +1 -1
  62. package/src/mcp/server.js +1 -1
  63. package/src/mcp/state.js +36 -0
  64. package/src/mcp/tools/address-to-symbol.js +0 -1
  65. package/src/mcp/tools/art-loaders.js +1 -1
  66. package/src/mcp/tools/cart-parts.js +0 -1
  67. package/src/mcp/tools/classify-region.js +1 -1
  68. package/src/mcp/tools/diff-roms.js +1 -1
  69. package/src/mcp/tools/disasm-rebuild.js +1 -1
  70. package/src/mcp/tools/disasm.js +2 -3
  71. package/src/mcp/tools/find-references.js +1 -2
  72. package/src/mcp/tools/font-map.js +1 -1
  73. package/src/mcp/tools/index.js +0 -49
  74. package/src/mcp/tools/input-layout.js +0 -1
  75. package/src/mcp/tools/input.js +33 -3
  76. package/src/mcp/tools/lifecycle.js +14 -2
  77. package/src/mcp/tools/lospec.js +0 -19
  78. package/src/mcp/tools/platform-docs.js +1 -1
  79. package/src/mcp/tools/platform-tools.js +4 -4
  80. package/src/mcp/tools/project.js +0 -2
  81. package/src/mcp/tools/reinject.js +0 -1
  82. package/src/mcp/tools/rom-id.js +2 -2
  83. package/src/mcp/tools/snippets.js +2 -2
  84. package/src/mcp/tools/sprite-pipeline.js +1 -2
  85. package/src/mcp/tools/tile-inspect.js +1 -1
  86. package/src/mcp/tools/toolchain.js +29 -9
  87. package/src/mcp/tools/watch-memory.js +13 -3
  88. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
  89. package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
  90. package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
  91. package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
  92. package/src/platforms/c64/d64.js +0 -1
  93. package/src/platforms/c64/sid.js +0 -2
  94. package/src/platforms/common/metasprite-adapters.js +1 -1
  95. package/src/platforms/common/metasprite-codegen.js +3 -3
  96. package/src/platforms/common/registers.js +5 -3
  97. package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
  98. package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
  99. package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
  100. package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
  101. package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
  102. package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
  103. package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
  104. package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
  105. package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
  106. package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
  107. package/src/platforms/nes/image-to-tilemap.js +3 -0
  108. package/src/platforms/nes/lib/asm/famitone2.s +5 -1
  109. package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
  110. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  111. package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
  112. package/src/platforms/snes/brr.js +0 -2
  113. package/src/playtest/playtest.js +0 -7
  114. package/src/toolchains/asar/asar.js +0 -9
  115. package/src/toolchains/assemble-snippet.js +30 -12
  116. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
  117. package/src/toolchains/common/reassemble.js +0 -1
  118. package/src/toolchains/common/sdk-cache.js +1 -1
  119. package/src/toolchains/genesis-c/genesis-c.js +5 -3
  120. package/src/toolchains/index.js +27 -3
  121. package/src/toolchains/parse-errors.js +78 -1
  122. package/src/toolchains/sdcc/preflight-lint.js +5 -1
  123. package/src/toolchains/sdcc/sdcc.js +1 -1
  124. package/src/toolchains/sjasm/sjasm.js +1 -1
  125. package/src/toolchains/snes-c/snes-c.js +2 -2
  126. package/src/toolchains/vasm68k/vasm68k.js +2 -4
  127. package/src/toolchains/wladx/wladx.js +1 -1
package/AGENTS.md CHANGED
@@ -153,8 +153,10 @@ worry about ground truth:
153
153
  when you're scaffolding into a project dir.
154
154
 
155
155
  For most workflows, path A is all you need. Read MENTAL_MODEL.md +
156
- TROUBLESHOOTING.md when stuck. File a feedback round if the bundled
157
- examples are wrong.
156
+ TROUBLESHOOTING.md when stuck. **When a tool call FAILS, read the error
157
+ message and `issues[]` first — see "When a call fails" below; the error
158
+ usually names the fix.** File a feedback round if the bundled examples
159
+ are wrong.
158
160
 
159
161
  ### Path B — Debug when the bundled code disagrees with behavior
160
162
 
@@ -397,9 +399,12 @@ When `build({output:'run'})` is too coarse, the long-form workflow:
397
399
  5. `input({op:'set'})` / `input({op:'press'})` / `input({op:'sequence'})` to drive the game
398
400
  6. `state({op:'save'}, "checkpoint")` / `state({op:'load'}, "checkpoint")` for try/undo
399
401
 
400
- ## Build errors
402
+ ## When a call fails: READ THE ERROR FIRST
401
403
 
402
- Every build tool returns `issues: [{file, line, col, severity, message, stage}, ...]`. Use that array, not the raw `log`. If `issues` is empty but `ok: false`, fall back to `log`.
404
+ romdev errors are written FOR you they name what went wrong AND how to recover. Read the message (and `issues[]`) before guessing, screenshotting, or retrying blindly. Two shapes:
405
+
406
+ - **Build/compile failures** return `issues: [{file, line, col, severity, message, stage}, ...]` — the structured error list. Use that array, NOT the raw `log`; it almost always names the exact line. Fall back to `log` only if `issues` is empty but `ok: false`. `issues[]` is RANKED most-dangerous first (**critical → error → warning → info**), so read it top-down: an entry flagged `critical: true` (e.g. a `WILL HANG:` `uint8`-loop-bound trap) is a latent crash even on a build that otherwise succeeded — fix those FIRST, never skip them as "just a warning". Link errors carry no `line` but include a `hint` naming the missing symbol + how to resolve it.
407
+ - **Tool/runtime errors** (thrown) carry the recovery step in the message itself. Examples: a "No ROM loaded" error after a session reconnect echoes the EXACT `loadMedia({...})` call to restore your state; a rejected `loadMedia` names the likely cause (wrong platform / truncated / unsupported mapper) and points you at `cart({op:'identify'})`; an `input({op:'set'})` with a typo'd button returns `ignoredButtons[]` so you see it pressed nothing. Don't discard these — they're the fix.
403
408
 
404
409
  **Crash isolation (R12).** Every WASM toolchain call runs in a child worker process. If a tool aborts (`_abort()`, SIGSEGV, OOM), only the worker dies — the MCP server keeps running, all other agent sessions are unaffected, tool registration + save states + playtest windows survive. The build response surfaces as `{ ok: false, stage: "crash", log: "[crash] worker exited unexpectedly — signal=… code=…", crash: { exitCode, signal } }`. Treat `stage: "crash"` as "the toolchain blew up — log the args + source somewhere durable so it can be triaged; you can keep iterating in this session without reconnecting".
405
410
 
@@ -430,6 +435,12 @@ romPatch({op:'diff', platform, a: original, b: patched }) // 6. verify the pa
430
435
  loadMedia({ platform, path: patched }) → frame({op:'screenshot'}) // 7. run it
431
436
  ```
432
437
 
438
+ **Driving input through a watched run.** A `watch`/`breakpoint` with NO
439
+ `pressDuring` INHERITS whatever `input({op:'set'})` last held — same as
440
+ `frame({op:'step'})`. But if you pass `pressDuring`, that schedule OWNS the pad
441
+ for the whole run and a prior `input({op:'set'})` is ignored. So to hold a button
442
+ *through* a watched window, put it in `pressDuring` — not a preceding `set`.
443
+
433
444
  **Finding which CODE wrote a byte.** Static disasm reading is the slow part —
434
445
  multiple `cmp #$XX` instructions look identical. Don't guess. Two tools, in order
435
446
  of precision:
package/CHANGELOG.md CHANGED
@@ -4,6 +4,72 @@ 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.22.1
8
+
9
+ Doc-only follow-up to 0.22.0's movement-analysis feedback: the `pressDuring`
10
+ schema on `watch` and `breakpoint` now states that entries with OVERLAPPING
11
+ windows on the same port are OR'd into a chord (e.g. `b`+`right` held while `a`
12
+ fires mid-window), not overwritten. The driver already behaved this way; this
13
+ documents the guarantee so it doesn't have to be confirmed empirically.
14
+
15
+ ## 0.22.0
16
+
17
+ **Transparency + correctness pass: every tool failure is actionable, dangerous
18
+ warnings are ranked first, and all 14 platforms' scaffolds build clean AND
19
+ render visible content.** The theme: a coding agent should never be left guessing
20
+ by an opaque error, never skip a crash-class warning buried in noise, and never
21
+ copy a scaffold that ships with warnings or a blank screen.
22
+
23
+ ### Changed — actionable error messages across all 14 platforms
24
+ Failures now name the fix, not just the symptom:
25
+ - **`build`/assemble:** compile errors carry `{file, line, message, stage}`; LINK
26
+ errors (which have no source line) now reach `issues[]` too, each with a `hint`
27
+ naming the missing symbol + how to resolve it — on ALL FOUR linkers (GNU ld for
28
+ Genesis/GBA, ld65 for NES/C64/Lynx/A2600/A7800/PCE, sdld for GB/GBC/SMS/GG/MSX,
29
+ wlalink for SNES). The crt0 (startup-stub) assembly path and `assembleSnippet`
30
+ now surface the first `file:line: message` instead of dumping a raw log.
31
+ - **`loadMedia`:** a refused ROM names the likely cause (wrong platform / truncated
32
+ / unsupported mapper) and points at `cart({op:'identify'})`.
33
+ - **Runtime/host:** `getHost`'s "No ROM loaded" echoes the EXACT `loadMedia` call
34
+ to recover with after a session eviction; unknown memory region lists the valid
35
+ names; "no save state named X" lists the existing slots; `host({op:'unload'})`
36
+ no longer claims success when nothing was loaded.
37
+
38
+ ### Changed — build `issues[]` ranks the dangerous warnings FIRST
39
+ A weak agent skips a lethal warning when it's buried among unused-variable noise.
40
+ `issues[]` is now ordered **critical → error → warning → info** (stable within a
41
+ rank) on every platform. The SDCC pre-flight lint marks the unconditional
42
+ `uint8`-loop-bound trap as `critical: true` (it always hangs) with a `WILL HANG:`
43
+ message; the conditional VRAM byte-copy stays a plain warning (it can't be proven
44
+ unsafe statically, so it must not cry wolf).
45
+
46
+ ### Fixed — `watch`/`breakpoint` inherit held input (the movement-analysis bug)
47
+ A `watch`/`breakpoint` run with NO `pressDuring` now inherits whatever
48
+ `input({op:'set'})` last held — exactly like `frame({op:'step'})`. Previously the
49
+ first frame reset the pad to neutral, silently dropping a held button. A
50
+ `pressDuring` schedule still OWNS the pad for the run (deterministic capture).
51
+ Documented on the `input`/`watch`/`breakpoint` schemas.
52
+
53
+ ### Fixed — all 130 scaffolds: zero warnings AND render visible content
54
+ Swept every `scaffold({op:'project'})` template on all 14 platforms:
55
+ - **Warnings 65 → 0** (was concentrated in GB/GBC/Genesis/GG/GBA/SMS), fixed at
56
+ the SOURCE so scaffolds model the right pattern: GB/GBC VRAM tile copies use the
57
+ runtime's pointer-walk `memcpy_vram` (the indexed `dst[i]=src[i]` form SDCC sm83
58
+ miscompiles into VRAM); Genesis builds pass `-Wno-main` (SGDK mandates
59
+ `int main(bool)`); GBA/GG/SMS narrowing + dead-branch fixes.
60
+ - **Blank/broken renders 31 → 0** (verified via `frame({op:'verify'})`): added a
61
+ patterned background to lone-sprite/text scaffolds, and fixed real bugs found
62
+ along the way — **NES FamiTone2's `$0300` RAM collided with the C runtime BSS**
63
+ (zeroed PPUCTRL, killed rendering; the driver's RAM was relocated to `$0700`);
64
+ the **SNES `sfx_init()`-before-`setScreenOn()`** forced-blank trap; a **C64 cc65
65
+ screen-fill-loop hang** (rewritten via `memset`) + sprite-data/`$0801` overlap;
66
+ the **Lynx double-buffer** stale-page trap.
67
+
68
+ ### Added — ESLint over romdev's own JavaScript
69
+ Flat config (`npm run lint`) catching real bugs (undefined refs, unused
70
+ imports/vars, dupe keys, self-assignment) over the monorepo's plain-JS ESM
71
+ sources; vendored SDK/wasm/build trees ignored. Cleaned 114 pre-existing findings.
72
+
7
73
  ## 0.21.0
8
74
 
9
75
  **NES CHR-ROM / iNES rebuild ergonomics + turnkey `disasm({target:'project'})`
@@ -16,6 +16,8 @@
16
16
  #define P0C1 (*(volatile uint8_t*)0x21)
17
17
  #define P0C2 (*(volatile uint8_t*)0x22)
18
18
  #define P0C3 (*(volatile uint8_t*)0x23)
19
+ #define P1C1 (*(volatile uint8_t*)0x25)
20
+ #define P2C1 (*(volatile uint8_t*)0x29)
19
21
  #define MSTAT (*(volatile uint8_t*)0x28)
20
22
  #define DPPH (*(volatile uint8_t*)0x2C)
21
23
  #define DPPL (*(volatile uint8_t*)0x30)
@@ -47,6 +49,42 @@ MK_DL(dl_row4); MK_DL(dl_row5); MK_DL(dl_row6); MK_DL(dl_row7);
47
49
 
48
50
  static uint8_t dl_empty[2] = { 0, 0 };
49
51
 
52
+ /* ── Background playfield ─────────────────────────────────────────────
53
+ * Without a full-screen drawable the display list emits only the one
54
+ * sprite and ~99% of the screen stays the flat BACKGRND colour (reads as
55
+ * "blank"). These full-width bands fill every non-sprite zone with scenery
56
+ * so the frame has real content (same machinery as default.c).
57
+ *
58
+ * One scanline of solid pixels lives in ROM (band_pix). A single DL
59
+ * drawable is at most 32 bytes = 128 px wide, so a full 160-px line needs
60
+ * TWO drawables. Width (byte[3] low 5 bits) = 32-bytes; high 3 bits =
61
+ * palette: field uses palette 1, ground uses palette 2. */
62
+ static const uint8_t band_pix[32] = {
63
+ 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,
64
+ 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
65
+ };
66
+ #define MK_BAND(name, pal) static uint8_t name[11] = { \
67
+ 0, 0x40, 0, ((pal) << 5) | 0, 0, /* 128 px @ x0 */ \
68
+ 0, 0x40, 0, ((pal) << 5) | 24, 128, /* 32 px @ x128 */ \
69
+ 0 }
70
+ MK_BAND(dl_field, 1);
71
+ MK_BAND(dl_ground, 2);
72
+ #define GROUND_ZONE 188
73
+
74
+ static void set_band_addr(uint8_t* dl) {
75
+ uint16_t a = (uint16_t)(uintptr_t)band_pix;
76
+ dl[0] = dl[5] = (uint8_t)(a & 0xFF);
77
+ dl[2] = dl[7] = (uint8_t)(a >> 8);
78
+ }
79
+
80
+ /* Background DL for a non-sprite zone: sky (empty) up top, field in the
81
+ * middle, ground at the bottom. */
82
+ static uint16_t bg_zone_dl(int zone) {
83
+ if (zone >= GROUND_ZONE) return (uint16_t)(uintptr_t)dl_ground;
84
+ if (zone >= 28) return (uint16_t)(uintptr_t)dl_field;
85
+ return (uint16_t)(uintptr_t)dl_empty;
86
+ }
87
+
50
88
  #define DLL_ZONES 243
51
89
  static uint8_t dll[DLL_ZONES * 3];
52
90
 
@@ -62,9 +100,9 @@ static void set_dll_entry(int idx, uint16_t dl_ptr) {
62
100
  dll[idx * 3 + 2] = (uint8_t)(dl_ptr & 0xFF);
63
101
  }
64
102
 
65
- /* Build the DLL with the sprite's 8 rows placed at DLL index sprite_y. */
103
+ /* Build the DLL with the sprite's 8 rows placed at DLL index sprite_y;
104
+ * every other zone gets the background scenery band for its row. */
66
105
  static void build_dll(uint8_t sprite_y) {
67
- uint16_t empty = (uint16_t)(uintptr_t)dl_empty;
68
106
  int i;
69
107
  for (i = 0; i < DLL_ZONES; i++) {
70
108
  uint16_t dl;
@@ -78,7 +116,7 @@ static void build_dll(uint8_t sprite_y) {
78
116
  case 5: dl = (uint16_t)(uintptr_t)dl_row5; break;
79
117
  case 6: dl = (uint16_t)(uintptr_t)dl_row6; break;
80
118
  case 7: dl = (uint16_t)(uintptr_t)dl_row7; break;
81
- default: dl = empty; break;
119
+ default: dl = bg_zone_dl(i); break; /* field/ground scenery */
82
120
  }
83
121
  set_dll_entry(i, dl);
84
122
  }
@@ -99,6 +137,10 @@ void main(void) {
99
137
  uint8_t x = 80;
100
138
  uint8_t y = 110;
101
139
 
140
+ /* Point the background bands at their shared ROM pixel row. */
141
+ set_band_addr(dl_field);
142
+ set_band_addr(dl_ground);
143
+
102
144
  /* Wire each DL's address bytes to its sprite-row data. */
103
145
  set_dl_addr(dl_row0, sprite_row0);
104
146
  set_dl_addr(dl_row1, sprite_row1);
@@ -112,10 +154,12 @@ void main(void) {
112
154
  set_x(x);
113
155
  build_dll(y);
114
156
 
115
- BACKGRND = 0x88;
157
+ BACKGRND = 0x88; /* light blue sky */
116
158
  P0C1 = 0x46;
117
159
  P0C2 = 0x0F;
118
160
  P0C3 = 0x36;
161
+ P1C1 = 0xC8; /* field green (background band) */
162
+ P2C1 = 0x14; /* ground brown (background band) */
119
163
  CHARBASE = 0;
120
164
  OFFSET = 0;
121
165
 
@@ -19,6 +19,8 @@
19
19
  #define P0C1 (*(volatile uint8_t*)0x21)
20
20
  #define P0C2 (*(volatile uint8_t*)0x22)
21
21
  #define P0C3 (*(volatile uint8_t*)0x23)
22
+ #define P1C1 (*(volatile uint8_t*)0x25)
23
+ #define P2C1 (*(volatile uint8_t*)0x29)
22
24
  #define MSTAT (*(volatile uint8_t*)0x28)
23
25
  #define DPPH (*(volatile uint8_t*)0x2C)
24
26
  #define DPPL (*(volatile uint8_t*)0x30)
@@ -47,6 +49,42 @@ MK_DL(dl_row4); MK_DL(dl_row5); MK_DL(dl_row6); MK_DL(dl_row7);
47
49
 
48
50
  static uint8_t dl_empty[2] = { 0, 0 };
49
51
 
52
+ /* ── Background playfield ─────────────────────────────────────────────
53
+ * Without a full-screen drawable the display list emits only the banner
54
+ * and ~99% of the screen stays the flat BACKGRND colour (reads as
55
+ * "blank"). These full-width bands fill every non-banner zone with scenery
56
+ * so the frame has real content (same machinery as default.c).
57
+ *
58
+ * One scanline of solid pixels lives in ROM (band_pix). A 160-px line needs
59
+ * TWO drawables (a DL drawable is at most 32 bytes = 128 px). Width
60
+ * (byte[3] low 5 bits) = 32-bytes; high 3 bits = palette: field = palette
61
+ * 1, ground = palette 2. */
62
+ static const uint8_t band_pix[32] = {
63
+ 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,
64
+ 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
65
+ };
66
+ #define MK_BAND(name, pal) static uint8_t name[11] = { \
67
+ 0, 0x40, 0, ((pal) << 5) | 0, 0, /* 128 px @ x0 */ \
68
+ 0, 0x40, 0, ((pal) << 5) | 24, 128, /* 32 px @ x128 */ \
69
+ 0 }
70
+ MK_BAND(dl_field, 1);
71
+ MK_BAND(dl_ground, 2);
72
+ #define GROUND_ZONE 188
73
+
74
+ static void set_band_addr(uint8_t* dl) {
75
+ uint16_t a = (uint16_t)(uintptr_t)band_pix;
76
+ dl[0] = dl[5] = (uint8_t)(a & 0xFF);
77
+ dl[2] = dl[7] = (uint8_t)(a >> 8);
78
+ }
79
+
80
+ /* Background DL for a non-banner zone: sky (empty) up top, field in the
81
+ * middle, ground at the bottom. */
82
+ static uint16_t bg_zone_dl(int zone) {
83
+ if (zone >= GROUND_ZONE) return (uint16_t)(uintptr_t)dl_ground;
84
+ if (zone >= 28) return (uint16_t)(uintptr_t)dl_field;
85
+ return (uint16_t)(uintptr_t)dl_empty;
86
+ }
87
+
50
88
  #define DLL_ZONES 243
51
89
  static uint8_t dll[DLL_ZONES * 3];
52
90
 
@@ -71,9 +109,12 @@ static void vblank_wait(void) {
71
109
 
72
110
  void main(void) {
73
111
  uint16_t dll_addr;
74
- uint16_t empty = (uint16_t)(uintptr_t)dl_empty;
75
112
  int i;
76
113
 
114
+ /* Point the background bands at their shared ROM pixel row. */
115
+ set_band_addr(dl_field);
116
+ set_band_addr(dl_ground);
117
+
77
118
  set_dl_addr(dl_row0, banner_row0);
78
119
  set_dl_addr(dl_row1, banner_row1);
79
120
  set_dl_addr(dl_row2, banner_row2);
@@ -83,6 +124,8 @@ void main(void) {
83
124
  set_dl_addr(dl_row6, banner_row6);
84
125
  set_dl_addr(dl_row7, banner_row7);
85
126
 
127
+ /* Build the DLL: banner rows at BANNER_Y, background scenery everywhere
128
+ * else so the screen isn't an almost-blank flat field. */
86
129
  for (i = 0; i < DLL_ZONES; i++) {
87
130
  uint16_t dl;
88
131
  int d = i - BANNER_Y;
@@ -95,7 +138,7 @@ void main(void) {
95
138
  case 5: dl = (uint16_t)(uintptr_t)dl_row5; break;
96
139
  case 6: dl = (uint16_t)(uintptr_t)dl_row6; break;
97
140
  case 7: dl = (uint16_t)(uintptr_t)dl_row7; break;
98
- default: dl = empty; break;
141
+ default: dl = bg_zone_dl(i); break; /* field/ground scenery */
99
142
  }
100
143
  set_dll_entry(i, dl);
101
144
  }
@@ -104,6 +147,8 @@ void main(void) {
104
147
  P0C1 = 0x0F; /* white text (palette index 1) */
105
148
  P0C2 = 0x0F;
106
149
  P0C3 = 0x0F;
150
+ P1C1 = 0xC8; /* field green (background band) */
151
+ P2C1 = 0x14; /* ground brown (background band) */
107
152
  CHARBASE = 0;
108
153
  OFFSET = 0;
109
154
 
@@ -26,6 +26,7 @@
26
26
 
27
27
  #include "c64_registers.h"
28
28
  #include <stdint.h>
29
+ #include <string.h> /* memset — see world_draw for why we fill via memset */
29
30
 
30
31
  /* cc65 stdlib already defines POKE/PEEK in cc65/include/peekpoke.h
31
32
  * with a different shape (no volatile, address as integer). Use the
@@ -39,20 +40,29 @@
39
40
  #define SCREEN ((volatile uint8_t*)0x0400)
40
41
  #define COLORS ((volatile uint8_t*)0xD800)
41
42
  #define SPRITE_POINTERS ((volatile uint8_t*)0x07F8)
42
- #define SPRITE_DATA ((volatile uint8_t*)0x0800)
43
+ /* Sprite data at $2000, NOT $0800 — $0800 overlaps the cc65 .prg load
44
+ * address ($0801), so writing sprite bytes there clobbers the running
45
+ * program's own startup code and the demo never reaches the draw loop
46
+ * (the whole screen stays blank). $2000 is free RAM in VIC bank 0. */
47
+ #define SPRITE_DATA ((volatile uint8_t*)0x2000)
43
48
 
44
49
  #define COLS 40
45
50
  #define ROWS 25
46
51
 
47
52
  #define CHAR_BLANK 0x20 /* space */
48
- #define CHAR_BLOCK 0xA0 /* PETSCII solid block */
53
+ #define CHAR_BLOCK 0xA0 /* PETSCII solid block (reverse-space) — fills */
54
+ /* the whole cell in its foreground colour. The */
55
+ /* whole world is drawn from this one glyph in */
56
+ /* different colours (see world_draw). */
49
57
 
50
58
  #define COL_BLACK 0x00
51
59
  #define COL_WHITE 0x01
52
60
  #define COL_RED 0x02
53
61
  #define COL_CYAN 0x03
62
+ #define COL_PURPLE 0x04
54
63
  #define COL_GREEN 0x05
55
64
  #define COL_BLUE 0x06
65
+ #define COL_YELLOW 0x07
56
66
 
57
67
  #define JOY_UP 0x01
58
68
  #define JOY_DOWN 0x02
@@ -85,39 +95,74 @@ static const uint8_t sprite_data[64] = {
85
95
  0,
86
96
  };
87
97
 
88
- static uint8_t world[ROWS][COLS];
98
+ // The world is described by a function, NOT a 1000-byte RAM array. cc65
99
+ // chokes on filling a large static uint8_t[ROWS][COLS] in a tight double
100
+ // loop here (it walks off and the program never reaches the draw) — so we
101
+ // compute each cell on demand instead, exactly like the platformer
102
+ // scaffold's render_view. Cheap and crash-free.
103
+ //
104
+ // is_wall(r,c) == 1 for the perimeter and the two interior platforms.
105
+ static uint8_t is_wall(uint8_t r, uint8_t c) {
106
+ if (r == 0 || r == ROWS - 1 || c == 0 || c == COLS - 1) return 1;
107
+ if (r == 10 && c >= 6 && c < 14) return 1;
108
+ if (r == 16 && c >= 22 && c < 34) return 1;
109
+ return 0;
110
+ }
89
111
 
90
- static void world_build(void) {
91
- uint8_t r, c;
92
- for (r = 0; r < ROWS; r++) {
93
- for (c = 0; c < COLS; c++) {
94
- world[r][c] =
95
- (r == 0 || r == ROWS - 1 || c == 0 || c == COLS - 1)
96
- ? CHAR_BLOCK : CHAR_BLANK;
97
- }
98
- }
99
- // Two interior platforms.
100
- for (c = 6; c < 14; c++) world[10][c] = CHAR_BLOCK;
101
- for (c = 22; c < 34; c++) world[16][c] = CHAR_BLOCK;
112
+ // Fill a run of `n` cells in screen + colour RAM starting at cell `base`.
113
+ #define ROW_OF(r) ((r) * COLS)
114
+ static void fill_cells(uint16_t base, uint16_t n, uint8_t ch, uint8_t col) {
115
+ memset((void*)(0x0400 + base), ch, n);
116
+ memset((void*)(0xD800 + base), col, n);
102
117
  }
103
118
 
119
+ // Paint the whole 40×25 character matrix as solid blocks in horizontal
120
+ // colour bands.
121
+ //
122
+ // IMPORTANT — why memset and not a per-cell for-loop: the cc65 build for
123
+ // this scaffold miscompiles a hand-written `for (off..) SCREEN[off]=..`
124
+ // loop (it hangs after ~2 rows and the rest of the screen stays the boot
125
+ // backdrop → almost-blank). memset() fills reliably, so we lay the world
126
+ // down as a handful of solid-colour bands. Several distinct bands keep any
127
+ // single colour well under the 92% "nearlyBlank" threshold while still
128
+ // reading as a tiled floor with a wall border.
104
129
  static void world_draw(void) {
105
- uint8_t r, c;
130
+ uint8_t r;
131
+
132
+ // Whole screen → solid blocks, mid-band colour to start.
133
+ fill_cells(0, ROWS * COLS, CHAR_BLOCK, COL_GREEN);
134
+
135
+ // Three horizontal colour bands so the interior isn't one flat colour.
136
+ fill_cells(ROW_OF(1), 8 * COLS, CHAR_BLOCK, COL_GREEN); // upper field
137
+ fill_cells(ROW_OF(9), 7 * COLS, CHAR_BLOCK, COL_PURPLE); // middle field
138
+ fill_cells(ROW_OF(16), 8 * COLS, CHAR_BLOCK, COL_BLUE); // lower field
139
+
140
+ // Cyan perimeter: top + bottom rows full width.
141
+ fill_cells(ROW_OF(0), COLS, CHAR_BLOCK, COL_CYAN);
142
+ fill_cells(ROW_OF(ROWS - 1), COLS, CHAR_BLOCK, COL_CYAN);
143
+
144
+ // Cyan interior platforms (the two walls is_wall() reports for collision).
145
+ fill_cells(ROW_OF(10) + 6, 8, CHAR_BLOCK, COL_CYAN);
146
+ fill_cells(ROW_OF(16) + 22, 12, CHAR_BLOCK, COL_CYAN);
147
+
148
+ // Left + right wall columns. One cell per row — a 25-iteration loop is
149
+ // short enough to compile correctly (the hang only bites long fills).
106
150
  for (r = 0; r < ROWS; r++) {
107
- for (c = 0; c < COLS; c++) {
108
- SCREEN[r * COLS + c] = world[r][c];
109
- COLORS[r * COLS + c] = world[r][c] == CHAR_BLOCK ? COL_CYAN : COL_BLACK;
110
- }
151
+ SCREEN[ROW_OF(r)] = CHAR_BLOCK;
152
+ COLORS[ROW_OF(r)] = COL_CYAN;
153
+ SCREEN[ROW_OF(r) + COLS - 1] = CHAR_BLOCK;
154
+ COLORS[ROW_OF(r) + COLS - 1] = COL_CYAN;
111
155
  }
112
156
  }
113
157
 
114
- // Convert sprite (px, py) → character cell. C64 sprite coords are
115
- // offset 24 px (X) and 50 px (Y) from the visible top-left.
158
+ // Convert sprite (px, py) → character cell, then test the wall function.
159
+ // C64 sprite coords are offset 24 px (X) and 50 px (Y) from the visible
160
+ // top-left.
116
161
  static uint8_t solid_at(uint16_t px, uint16_t py) {
117
162
  uint16_t cx = (px - 24) >> 3;
118
163
  uint16_t cy = (py - 50) >> 3;
119
164
  if (cx >= COLS || cy >= ROWS) return 1;
120
- return world[cy][cx] == CHAR_BLOCK;
165
+ return is_wall((uint8_t)cy, (uint8_t)cx);
121
166
  }
122
167
 
123
168
  static void wait_vblank(void) {
@@ -135,15 +180,12 @@ void main(void) {
135
180
  uint8_t pad;
136
181
 
137
182
  copy_sprite();
138
- SPRITE_POINTERS[0] = 0x20; /* $0800 / $40 */
183
+ SPRITE_POINTERS[0] = 0x80; /* $2000 / 64 */
139
184
 
140
185
  WR(VIC_BORDER, 0x00);
141
186
  WR(VIC_BG0, 0x00);
142
187
  WR(VIC_SPR_COL(0), COL_RED);
143
188
 
144
- world_build();
145
- world_draw();
146
-
147
189
  WR(VIC_SPRITE_X(0), (uint8_t)(sx & 0xFF));
148
190
  WR(VIC_SPRITE_Y(0), (uint8_t)sy);
149
191
  WR(VIC_SPR_ENA, 0x01);
@@ -152,6 +194,14 @@ void main(void) {
152
194
  uint16_t nx = sx, ny = sy;
153
195
  wait_vblank();
154
196
 
197
+ /* Repaint the whole character map every frame. The KERNAL keeps
198
+ * clearing screen RAM for the first frames after boot, so a single
199
+ * draw before the loop gets wiped (almost-blank screen). Redrawing
200
+ * each frame is cheap (1000 cells) and guarantees the world is always
201
+ * on-screen regardless of boot timing — the same "redraw every frame"
202
+ * discipline the other scaffolds use. */
203
+ world_draw();
204
+
155
205
  pad = ~RD(CIA1_PRA) & 0x0F;
156
206
  if (pad & JOY_UP && sy > 52) ny--;
157
207
  if (pad & JOY_DOWN && sy < 240) ny++;
@@ -47,6 +47,8 @@ void main(void) {
47
47
  uint8_t i;
48
48
  uint8_t *vram_dst;
49
49
  const uint8_t *src;
50
+ uint8_t *bg_map;
51
+ uint16_t j;
50
52
 
51
53
  /* ── 1. LCD off (safe whether it was on or off) ──────────────────
52
54
  * lcd_init_default() checks LCDC.7 and only waits for vblank if the
@@ -60,9 +62,16 @@ void main(void) {
60
62
  * accidentally render garbage tiles that point to it. */
61
63
  vram_dst = (uint8_t *)0x8010;
62
64
  src = tile_data;
63
- for (i = 0; i < 16; i++) {
64
- vram_dst[i] = src[i];
65
- }
65
+ /* memcpy_vram (pointer-walk) NOT an indexed vram_dst[i]=src[i] loop, which
66
+ * SDCC sm83 miscompiles when the dest points into VRAM ($8000-$9FFF). */
67
+ memcpy_vram(vram_dst, src, 16);
68
+
69
+ /* ── 2b. Fill the BG tilemap so the screen isn't an empty backdrop. ──
70
+ * With LCDC_TILE_DATA_LO ($8000 addressing) BG tile index 1 == our tile
71
+ * at $8010 — so we tile the whole 32×32 BG map with it. memcpy_vram-style
72
+ * pointer walk (NOT bg_map[k]=1, which SDCC sm83 miscompiles into VRAM). */
73
+ bg_map = (uint8_t *)0x9800;
74
+ for (j = 0; j < 32u * 32u; j++) *bg_map++ = 1;
66
75
 
67
76
  /* ── 3. Object palette 0 (CGB path) ──────────────────────────────
68
77
  * OCPS bit 7 = auto-increment after each write; bits 5..3 = palette
@@ -92,9 +101,9 @@ void main(void) {
92
101
  oam_dma_flush(); /* first OAM live before the screen turns on */
93
102
 
94
103
  /* ── 5. Turn the LCD back on with BG + OBJ enabled. ──────────────
95
- * LCDC bits: 0x80=LCD on, 0x02=OBJ on, 0x10=tile data at $8000.
96
- * (BG remains off we have no BG map set up.) */
97
- LCDC = LCDC_LCD_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO;
104
+ * LCDC bits: 0x80=LCD on, 0x01=BG on, 0x02=OBJ on, 0x10=tile data at $8000.
105
+ * BG is on now that we filled the BG map in step 2b. */
106
+ LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO;
98
107
 
99
108
  /* ── 6. APU on — let the player beep ──────────────────────────── */
100
109
  sound_init();
@@ -29,11 +29,47 @@
29
29
  * project template). */
30
30
  extern const huge_song_t sample_song;
31
31
 
32
+ /* Two 8×8 2bpp tiles so the BG isn't a single flat colour (a uniform
33
+ * screen reads >=92% one colour and fails the blank-screen check):
34
+ * tile 1 — solid colour 3
35
+ * tile 2 — solid colour 1
36
+ * We checkerboard them across the BG map below. */
37
+ static const uint8_t tile_solid3[16] = {
38
+ 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
39
+ 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
40
+ };
41
+ static const uint8_t tile_solid1[16] = {
42
+ 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
43
+ 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
44
+ };
45
+
32
46
  void main(void) {
33
47
  uint8_t shade = 0;
34
48
  uint16_t frame = 0;
49
+ uint8_t *bg_map;
50
+ uint16_t j;
35
51
 
36
52
  lcd_init_default();
53
+ LCDC = 0; /* LCD off so we can write VRAM freely */
54
+
55
+ /* Upload two tiles to VRAM slots 1 ($8010) and 2 ($8020). Use
56
+ * memcpy_vram (pointer-walk) — an indexed dst[i]=src[i] loop into VRAM
57
+ * is miscompiled by SDCC sm83. */
58
+ memcpy_vram((uint8_t *)0x8010, tile_solid3, 16);
59
+ memcpy_vram((uint8_t *)0x8020, tile_solid1, 16);
60
+
61
+ /* Checkerboard the 32×32 BG map at $9800 with tiles 1 and 2 so the
62
+ * screen shows two distinct shades. Pointer-walk (NOT bg_map[k]=...,
63
+ * which SDCC sm83 miscompiles into VRAM). */
64
+ bg_map = (uint8_t *)0x9800;
65
+ for (j = 0; j < 32u * 32u; j++) {
66
+ *bg_map++ = (uint8_t)((((j ^ (j >> 5)) & 1u) ? 1u : 2u));
67
+ }
68
+
69
+ /* LCD on with BG enabled, $8000 tile-data addressing so index 1 == our
70
+ * tile at $8010. */
71
+ LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_TILE_DATA_LO;
72
+
37
73
  sound_init();
38
74
 
39
75
  hUGE_init(&sample_song);
@@ -75,8 +75,9 @@ static uint8_t on_platform(int16_t px, int16_t py) {
75
75
 
76
76
  static void upload_tile(uint8_t slot, const uint8_t *src) {
77
77
  uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
78
- uint8_t i;
79
- for (i = 0; i < 16; i++) dst[i] = src[i];
78
+ /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
79
+ * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
80
+ memcpy_vram(dst, src, 16);
80
81
  }
81
82
 
82
83
  static void paint_platforms(void) {
@@ -141,8 +141,9 @@ static void lock_piece(void) {
141
141
 
142
142
  static void upload_tile(uint8_t slot, const uint8_t *src) {
143
143
  uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
144
- uint8_t i;
145
- for (i = 0; i < 16; i++) dst[i] = src[i];
144
+ /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
145
+ * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
146
+ memcpy_vram(dst, src, 16);
146
147
  }
147
148
 
148
149
  /* Draw the well frame around the 6×12 play area. Grid cells live at
@@ -109,8 +109,9 @@ static void spawn_obstacle(void) {
109
109
 
110
110
  static void upload_tile(uint8_t slot, const uint8_t *src) {
111
111
  uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
112
- uint8_t i;
113
- for (i = 0; i < 16; i++) dst[i] = src[i];
112
+ /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
113
+ * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
114
+ memcpy_vram(dst, src, 16);
114
115
  }
115
116
 
116
117
  /* Paint the road into BG map 0 ($9800). 20×18 visible cells:
@@ -100,8 +100,9 @@ static void spawn(void) {
100
100
 
101
101
  static void upload_tile(uint8_t slot, const uint8_t *src) {
102
102
  uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
103
- uint8_t i;
104
- for (i = 0; i < 16; i++) dst[i] = src[i];
103
+ /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
104
+ * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
105
+ memcpy_vram(dst, src, 16);
105
106
  }
106
107
 
107
108
  /* Paint a starfield into BG map 0 ($9800): fill the visible 20×18 with the
@@ -77,8 +77,9 @@ static void reset_match(void) {
77
77
 
78
78
  static void upload_tile(uint8_t slot, const uint8_t *src) {
79
79
  uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
80
- uint8_t i;
81
- for (i = 0; i < 16; i++) dst[i] = src[i];
80
+ /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
81
+ * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
82
+ memcpy_vram(dst, src, 16);
82
83
  }
83
84
 
84
85
  /* Paint the Pong court into BG map 0 ($9800): dithered turf everywhere,
@@ -143,8 +143,9 @@ static const uint8_t rooms[ROOMS][ROWS * COLS] = {
143
143
 
144
144
  /* ── Helpers ────────────────────────────────────────────────────── */
145
145
  static void copy_to_vram(uint8_t *dst, const uint8_t *src, uint16_t n) {
146
- uint16_t i;
147
- for (i = 0; i < n; i++) dst[i] = src[i];
146
+ /* Delegate to the runtime's pointer-walk copy — an indexed dst[i]=src[i]
147
+ * loop into VRAM is miscompiled by SDCC sm83. */
148
+ memcpy_vram(dst, src, n);
148
149
  }
149
150
 
150
151
  static void load_bg_palette(void) {