romdevtools 0.21.0 → 0.22.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.
- package/AGENTS.md +15 -4
- package/CHANGELOG.md +58 -0
- package/examples/atari7800/templates/hello_sprite.c +48 -4
- package/examples/atari7800/templates/music_demo.c +47 -2
- package/examples/c64/templates/tile_engine.c +77 -27
- package/examples/gb/templates/hello_sprite.c +15 -6
- package/examples/gb/templates/music_demo.c +36 -0
- package/examples/gb/templates/platformer.c +3 -2
- package/examples/gb/templates/puzzle.c +3 -2
- package/examples/gb/templates/racing.c +3 -2
- package/examples/gb/templates/shmup.c +3 -2
- package/examples/gb/templates/sports.c +3 -2
- package/examples/gb/templates/tile_engine.c +3 -2
- package/examples/gba/templates/maxmod_demo.c +36 -2
- package/examples/gba/templates/platformer.c +3 -1
- package/examples/gba/templates/tonc_hello_sprite.c +35 -1
- package/examples/gbc/templates/hello_sprite.c +12 -3
- package/examples/gbc/templates/music_demo.c +56 -12
- package/examples/gbc/templates/platformer.c +3 -2
- package/examples/gbc/templates/puzzle.c +3 -2
- package/examples/gbc/templates/racing.c +3 -2
- package/examples/gbc/templates/shmup.c +3 -2
- package/examples/gbc/templates/sports.c +3 -2
- package/examples/gbc/templates/tile_engine.c +3 -2
- package/examples/genesis/main.s +53 -1
- package/examples/genesis/templates/hello_sprite.c +25 -3
- package/examples/genesis/templates/shmup_2p.c +31 -0
- package/examples/genesis/templates/xgm2_demo.c +20 -0
- package/examples/gg/templates/hello_sprite.c +25 -2
- package/examples/gg/templates/music_demo.c +24 -2
- package/examples/gg/templates/racing.c +7 -4
- package/examples/gg/templates/sports.c +11 -13
- package/examples/gg/templates/tile_engine.c +12 -6
- package/examples/lynx/templates/hello_sprite.c +15 -1
- package/examples/lynx/templates/music_demo.c +13 -1
- package/examples/nes/templates/hello_sprite.c +35 -0
- package/examples/nes/templates/music_demo.c +40 -0
- package/examples/pce/catch_game/main.c +22 -3
- package/examples/pce/music_sfx/main.c +28 -1
- package/examples/pce/sprite_move/main.c +7 -2
- package/examples/sms/templates/hello_sprite.c +29 -3
- package/examples/sms/templates/music_demo.c +18 -4
- package/examples/sms/templates/shmup_2p.c +24 -1
- package/examples/sms/templates/sports.c +4 -2
- package/examples/snes/main.asm +108 -17
- package/examples/snes/templates/c-hello-data.asm +23 -0
- package/examples/snes/templates/c-hello.c +18 -1
- package/examples/snes/templates/hello_sprite-data.asm +23 -0
- package/examples/snes/templates/hello_sprite.c +17 -1
- package/examples/snes/templates/music_demo-data.asm +23 -0
- package/examples/snes/templates/music_demo.c +22 -4
- package/examples/snes/templates/platformer.c +4 -1
- package/examples/snes/templates/puzzle.c +4 -1
- package/package.json +1 -1
- package/src/cheats/gamegenie.js +0 -1
- package/src/cli/smoke.js +1 -3
- package/src/host/LibretroHost.js +69 -15
- package/src/host/chafa-render.js +2 -0
- package/src/host/dsp-state.js +2 -2
- package/src/host/gpgx-state.js +4 -0
- package/src/http/routes.js +1 -1
- package/src/mcp/server.js +1 -1
- package/src/mcp/state.js +36 -0
- package/src/mcp/tools/address-to-symbol.js +0 -1
- package/src/mcp/tools/art-loaders.js +1 -1
- package/src/mcp/tools/cart-parts.js +0 -1
- package/src/mcp/tools/classify-region.js +1 -1
- package/src/mcp/tools/diff-roms.js +1 -1
- package/src/mcp/tools/disasm-rebuild.js +1 -1
- package/src/mcp/tools/disasm.js +2 -3
- package/src/mcp/tools/find-references.js +1 -2
- package/src/mcp/tools/font-map.js +1 -1
- package/src/mcp/tools/index.js +0 -49
- package/src/mcp/tools/input-layout.js +0 -1
- package/src/mcp/tools/input.js +33 -3
- package/src/mcp/tools/lifecycle.js +14 -2
- package/src/mcp/tools/lospec.js +0 -19
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/platform-tools.js +4 -4
- package/src/mcp/tools/project.js +0 -2
- package/src/mcp/tools/reinject.js +0 -1
- package/src/mcp/tools/rom-id.js +2 -2
- package/src/mcp/tools/snippets.js +2 -2
- package/src/mcp/tools/sprite-pipeline.js +1 -2
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +29 -9
- package/src/mcp/tools/watch-memory.js +13 -3
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +34 -0
- package/src/platforms/atari2600/TROUBLESHOOTING.md +6 -0
- package/src/platforms/atari7800/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/d64.js +0 -1
- package/src/platforms/c64/sid.js +0 -2
- package/src/platforms/common/metasprite-adapters.js +1 -1
- package/src/platforms/common/metasprite-codegen.js +3 -3
- package/src/platforms/common/registers.js +5 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gb/lib/c/gb_runtime.c +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gbc/lib/c/gb_runtime.c +4 -4
- package/src/platforms/genesis/TROUBLESHOOTING.md +6 -0
- package/src/platforms/gg/TROUBLESHOOTING.md +6 -0
- package/src/platforms/lynx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/image-to-tilemap.js +3 -0
- package/src/platforms/nes/lib/asm/famitone2.s +5 -1
- package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +6 -0
- package/src/platforms/snes/brr.js +0 -2
- package/src/playtest/playtest.js +0 -7
- package/src/toolchains/asar/asar.js +0 -9
- package/src/toolchains/assemble-snippet.js +30 -12
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
- package/src/toolchains/common/reassemble.js +0 -1
- package/src/toolchains/common/sdk-cache.js +1 -1
- package/src/toolchains/genesis-c/genesis-c.js +5 -3
- package/src/toolchains/index.js +27 -3
- package/src/toolchains/parse-errors.js +78 -1
- package/src/toolchains/sdcc/preflight-lint.js +5 -1
- package/src/toolchains/sdcc/sdcc.js +1 -1
- package/src/toolchains/sjasm/sjasm.js +1 -1
- package/src/toolchains/snes-c/snes-c.js +2 -2
- package/src/toolchains/vasm68k/vasm68k.js +2 -4
- 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.
|
|
157
|
-
|
|
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
|
-
##
|
|
402
|
+
## When a call fails: READ THE ERROR FIRST
|
|
401
403
|
|
|
402
|
-
|
|
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,64 @@ 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.0
|
|
8
|
+
|
|
9
|
+
**Transparency + correctness pass: every tool failure is actionable, dangerous
|
|
10
|
+
warnings are ranked first, and all 14 platforms' scaffolds build clean AND
|
|
11
|
+
render visible content.** The theme: a coding agent should never be left guessing
|
|
12
|
+
by an opaque error, never skip a crash-class warning buried in noise, and never
|
|
13
|
+
copy a scaffold that ships with warnings or a blank screen.
|
|
14
|
+
|
|
15
|
+
### Changed — actionable error messages across all 14 platforms
|
|
16
|
+
Failures now name the fix, not just the symptom:
|
|
17
|
+
- **`build`/assemble:** compile errors carry `{file, line, message, stage}`; LINK
|
|
18
|
+
errors (which have no source line) now reach `issues[]` too, each with a `hint`
|
|
19
|
+
naming the missing symbol + how to resolve it — on ALL FOUR linkers (GNU ld for
|
|
20
|
+
Genesis/GBA, ld65 for NES/C64/Lynx/A2600/A7800/PCE, sdld for GB/GBC/SMS/GG/MSX,
|
|
21
|
+
wlalink for SNES). The crt0 (startup-stub) assembly path and `assembleSnippet`
|
|
22
|
+
now surface the first `file:line: message` instead of dumping a raw log.
|
|
23
|
+
- **`loadMedia`:** a refused ROM names the likely cause (wrong platform / truncated
|
|
24
|
+
/ unsupported mapper) and points at `cart({op:'identify'})`.
|
|
25
|
+
- **Runtime/host:** `getHost`'s "No ROM loaded" echoes the EXACT `loadMedia` call
|
|
26
|
+
to recover with after a session eviction; unknown memory region lists the valid
|
|
27
|
+
names; "no save state named X" lists the existing slots; `host({op:'unload'})`
|
|
28
|
+
no longer claims success when nothing was loaded.
|
|
29
|
+
|
|
30
|
+
### Changed — build `issues[]` ranks the dangerous warnings FIRST
|
|
31
|
+
A weak agent skips a lethal warning when it's buried among unused-variable noise.
|
|
32
|
+
`issues[]` is now ordered **critical → error → warning → info** (stable within a
|
|
33
|
+
rank) on every platform. The SDCC pre-flight lint marks the unconditional
|
|
34
|
+
`uint8`-loop-bound trap as `critical: true` (it always hangs) with a `WILL HANG:`
|
|
35
|
+
message; the conditional VRAM byte-copy stays a plain warning (it can't be proven
|
|
36
|
+
unsafe statically, so it must not cry wolf).
|
|
37
|
+
|
|
38
|
+
### Fixed — `watch`/`breakpoint` inherit held input (the movement-analysis bug)
|
|
39
|
+
A `watch`/`breakpoint` run with NO `pressDuring` now inherits whatever
|
|
40
|
+
`input({op:'set'})` last held — exactly like `frame({op:'step'})`. Previously the
|
|
41
|
+
first frame reset the pad to neutral, silently dropping a held button. A
|
|
42
|
+
`pressDuring` schedule still OWNS the pad for the run (deterministic capture).
|
|
43
|
+
Documented on the `input`/`watch`/`breakpoint` schemas.
|
|
44
|
+
|
|
45
|
+
### Fixed — all 130 scaffolds: zero warnings AND render visible content
|
|
46
|
+
Swept every `scaffold({op:'project'})` template on all 14 platforms:
|
|
47
|
+
- **Warnings 65 → 0** (was concentrated in GB/GBC/Genesis/GG/GBA/SMS), fixed at
|
|
48
|
+
the SOURCE so scaffolds model the right pattern: GB/GBC VRAM tile copies use the
|
|
49
|
+
runtime's pointer-walk `memcpy_vram` (the indexed `dst[i]=src[i]` form SDCC sm83
|
|
50
|
+
miscompiles into VRAM); Genesis builds pass `-Wno-main` (SGDK mandates
|
|
51
|
+
`int main(bool)`); GBA/GG/SMS narrowing + dead-branch fixes.
|
|
52
|
+
- **Blank/broken renders 31 → 0** (verified via `frame({op:'verify'})`): added a
|
|
53
|
+
patterned background to lone-sprite/text scaffolds, and fixed real bugs found
|
|
54
|
+
along the way — **NES FamiTone2's `$0300` RAM collided with the C runtime BSS**
|
|
55
|
+
(zeroed PPUCTRL, killed rendering; the driver's RAM was relocated to `$0700`);
|
|
56
|
+
the **SNES `sfx_init()`-before-`setScreenOn()`** forced-blank trap; a **C64 cc65
|
|
57
|
+
screen-fill-loop hang** (rewritten via `memset`) + sprite-data/`$0801` overlap;
|
|
58
|
+
the **Lynx double-buffer** stale-page trap.
|
|
59
|
+
|
|
60
|
+
### Added — ESLint over romdev's own JavaScript
|
|
61
|
+
Flat config (`npm run lint`) catching real bugs (undefined refs, unused
|
|
62
|
+
imports/vars, dupe keys, self-assignment) over the monorepo's plain-JS ESM
|
|
63
|
+
sources; vendored SDK/wasm/build trees ignored. Cleaned 114 pre-existing findings.
|
|
64
|
+
|
|
7
65
|
## 0.21.0
|
|
8
66
|
|
|
9
67
|
**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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
115
|
-
// offset 24 px (X) and 50 px (Y) from the visible
|
|
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
|
|
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] =
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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) {
|