romdevtools 0.13.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.
- package/AGENTS.md +21 -14
- package/CHANGELOG.md +125 -1
- package/README.md +13 -8
- package/examples/atari2600/main.asm +1 -1
- package/examples/atari2600/templates/default.asm +1 -1
- package/examples/atari2600/templates/paddle.asm +59 -47
- package/examples/atari7800/main.c +1 -1
- package/examples/atari7800/templates/default.c +1 -1
- package/examples/atari7800/templates/music_demo.c +1 -1
- package/examples/c64/main.c +1 -1
- package/examples/c64/templates/platformer.c +2 -2
- package/examples/c64/templates/puzzle.c +1 -1
- package/examples/c64/templates/racing.c +3 -3
- package/examples/c64/templates/shmup.c +6 -5
- package/examples/c64/templates/sports.c +4 -4
- package/examples/gb/main.asm +1 -1
- package/examples/gb/main.c +1 -1
- package/examples/gb/templates/puzzle.c +1 -1
- package/examples/gb/templates/racing.c +1 -1
- package/examples/gb/templates/shmup.c +1 -1
- package/examples/gba/templates/gba_hello.c +1 -1
- package/examples/gba/templates/maxmod_demo.c +1 -1
- package/examples/gba/templates/puzzle.c +17 -3
- package/examples/gba/templates/racing.c +16 -2
- package/examples/gba/templates/shmup.c +23 -4
- package/examples/gba/templates/tonc_hello.c +6 -4
- package/examples/gbc/main.asm +1 -1
- package/examples/gbc/templates/puzzle.c +1 -1
- package/examples/gbc/templates/racing.c +1 -1
- package/examples/gbc/templates/shmup.c +1 -1
- package/examples/genesis/main.s +1 -1
- package/examples/genesis/templates/puzzle.c +1 -1
- package/examples/genesis/templates/racing.c +45 -1
- package/examples/genesis/templates/shmup.c +12 -3
- package/examples/genesis/templates/shmup_2p.c +2 -2
- package/examples/genesis/templates/sports.c +39 -0
- package/examples/gg/templates/hello_sprite.c +38 -23
- package/examples/gg/templates/music_demo.c +11 -8
- package/examples/gg/templates/platformer.c +37 -15
- package/examples/gg/templates/racing.c +25 -12
- package/examples/gg/templates/shmup.c +12 -6
- package/examples/gg/templates/sports.c +30 -16
- package/examples/gg/templates/tile_engine.c +24 -10
- package/examples/lynx/templates/platformer.c +7 -1
- package/examples/lynx/templates/puzzle.c +8 -2
- package/examples/lynx/templates/racing.c +7 -1
- package/examples/lynx/templates/sports.c +7 -1
- package/examples/nes/main.c +2 -2
- package/examples/nes/space-shooter/nes_runtime.h +1 -1
- package/examples/nes/templates/default.c +4 -1
- package/examples/nes/templates/racing.c +50 -1
- package/examples/pce/main.c +1 -1
- package/examples/sms/templates/hello_sprite.c +1 -1
- package/examples/sms/templates/music_demo.c +1 -1
- package/examples/sms/templates/puzzle.c +1 -1
- package/examples/sms/templates/racing.c +1 -1
- package/examples/sms/templates/shmup.c +1 -1
- package/examples/sms/templates/shmup_2p.c +2 -2
- package/examples/snes/main.asm +1 -1
- package/examples/snes/templates/c-hello-data.asm +309 -14
- package/examples/snes/templates/c-hello.c +13 -2
- package/examples/snes/templates/default.c +1 -1
- package/examples/snes/templates/hello_sprite-data.asm +300 -2
- package/examples/snes/templates/hello_sprite.c +10 -1
- package/examples/snes/templates/music_demo-data.asm +300 -2
- package/examples/snes/templates/music_demo.c +10 -1
- package/examples/snes/templates/platformer-data.asm +300 -2
- package/examples/snes/templates/platformer.c +10 -1
- package/examples/snes/templates/puzzle-data.asm +300 -2
- package/examples/snes/templates/puzzle.c +11 -1
- package/examples/snes/templates/racing-data.asm +300 -2
- package/examples/snes/templates/racing.c +40 -4
- package/examples/snes/templates/shmup-data.asm +299 -6
- package/examples/snes/templates/shmup.c +11 -7
- package/examples/snes/templates/sports-data.asm +300 -2
- package/examples/snes/templates/sports.c +40 -5
- package/package.json +1 -1
- package/src/cheats/lookup.js +39 -18
- package/src/http/routes.js +58 -33
- package/src/http/skill-doc.js +10 -9
- package/src/http/swagger.js +1 -1
- package/src/http/tool-registry.js +72 -5
- package/src/mcp/server.js +6 -5
- package/src/mcp/state.js +8 -6
- package/src/mcp/tool-manifest.js +7 -7
- package/src/mcp/tools/cheats.js +4 -3
- package/src/mcp/tools/index.js +18 -2
- package/src/mcp/tools/playtest.js +48 -35
- package/src/mcp/tools/project.js +39 -73
- package/src/mcp/tools/rom-id.js +49 -4
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +183 -19
- package/src/mcp/tools/trace-vram-source.js +3 -3
- package/src/mcp/tools/watch-memory.js +27 -46
- package/src/observer/livestream.html +41 -5
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +5 -5
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +1 -1
- package/src/platforms/gb/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gb/lib/c/README.md +2 -2
- package/src/platforms/gb/lib/c/SDCC_GOTCHAS.md +1 -1
- package/src/platforms/gbc/MENTAL_MODEL.md +3 -3
- package/src/platforms/gbc/TROUBLESHOOTING.md +5 -5
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +2 -2
- package/src/platforms/gbc/lib/c/README.md +2 -2
- package/src/platforms/gbc/lib/c/SDCC_GOTCHAS.md +1 -1
- package/src/platforms/gg/MENTAL_MODEL.md +14 -13
- package/src/platforms/gg/lib/c/vdp_init.c +10 -8
- package/src/platforms/msx/MENTAL_MODEL.md +1 -1
- package/src/platforms/nes/TROUBLESHOOTING.md +1 -1
- package/src/platforms/nes/lib/c/nes_runtime.c +28 -6
- package/src/platforms/pce/MENTAL_MODEL.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +1 -0
- package/src/platforms/pce/lib/c/pce_video.c +26 -0
- package/src/platforms/sms/MENTAL_MODEL.md +12 -12
- package/src/platforms/sms/lib/c/vdp_init.c +10 -8
- package/src/platforms/sms/lib/vdp_init.s +1 -1
- package/src/playtest/playtest.js +25 -0
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.cfg +1 -1
- package/src/toolchains/cc65/presets/nes/chr-ram.crt0.s +1 -1
- package/src/toolchains/genesis-c/README.md +1 -1
- package/src/toolchains/sdcc/preflight-lint.js +47 -7
- package/src/toolchains/snes-c/snes-c.js +3 -7
|
@@ -16,7 +16,7 @@ the same wall.
|
|
|
16
16
|
`build({output:'rom'})` / `build({output:'run'})` do this for you at build time: every byte
|
|
17
17
|
at $0134..$014C is filled, and on `platform:"gbc"` the CGB flag at
|
|
18
18
|
$0143 is set to $80 (CGB-aware + DMG-compatible). You do **not** run
|
|
19
|
-
`
|
|
19
|
+
`romPatch({op:'gbHeader'})` on a freshly built ROM. Reach for `romPatch({op:'gbHeader'})` only
|
|
20
20
|
to fix up an existing / externally built ROM whose header was never
|
|
21
21
|
set, or to override a field — e.g. starting from a `.gb` ROM and
|
|
22
22
|
wanting CGB color, pass `cgb: true` explicitly.
|
|
@@ -161,11 +161,11 @@ the only differences at build time are:
|
|
|
161
161
|
|
|
162
162
|
- ROM extension: `.gbc` (vs `.gb`)
|
|
163
163
|
- the build sets `$0143 = $80` to flip CGB mode on (automatic when you
|
|
164
|
-
build with `platform:"gbc"` — no manual `
|
|
164
|
+
build with `platform:"gbc"` — no manual `romPatch({op:'gbHeader'})` step)
|
|
165
165
|
- gambatte core accepts both DMG + CGB-mode ROMs
|
|
166
166
|
|
|
167
167
|
For new GBC code that wants to be CGB-only (no DMG fallback) set the
|
|
168
|
-
CGB byte to `$C0` instead of `$80` — `
|
|
168
|
+
CGB byte to `$C0` instead of `$80` — `romPatch({op:'gbHeader', path, cgb:true})`
|
|
169
169
|
on the built ROM can override it.
|
|
170
170
|
|
|
171
171
|
## Horizontal scrolling (for side-scrollers)
|
|
@@ -59,7 +59,7 @@ and falls back to DMG mode when it's `$00`.
|
|
|
59
59
|
When you build with `platform:"gbc"`, `build({output:'rom'})` / `build({output:'run'})`
|
|
60
60
|
**auto-fix the header** — Nintendo logo, header + global checksums,
|
|
61
61
|
and `$0143 = $80` (CGB-enhanced) — so a freshly built `.gbc` already
|
|
62
|
-
boots in color. You do **not** call `
|
|
62
|
+
boots in color. You do **not** call `romPatch({op:'gbHeader'})` for that.
|
|
63
63
|
|
|
64
64
|
```js
|
|
65
65
|
build({ output: 'run', platform: "gbc", language: "c", ... }); /* header auto-fixed */
|
|
@@ -67,7 +67,7 @@ build({ output: 'run', platform: "gbc", language: "c", ... }); /* header auto-f
|
|
|
67
67
|
|
|
68
68
|
If you instead see green-shade DMG mode, the ROM was almost certainly
|
|
69
69
|
built with `platform:"gb"` (so the CGB flag stayed `$00`). Rebuild with
|
|
70
|
-
`platform:"gbc"`. Reach for `
|
|
70
|
+
`platform:"gbc"`. Reach for `romPatch({op:'gbHeader'})` only to fix up an existing /
|
|
71
71
|
externally built `.gbc` whose header was never set, or to override a
|
|
72
72
|
header field (e.g. force `cgb:false`).
|
|
73
73
|
|
|
@@ -106,12 +106,12 @@ Without the attribute writes, every BG tile defaults to palette 0.
|
|
|
106
106
|
## "Game ran on Game Boy emulator but not on Game Boy Color emulator"
|
|
107
107
|
|
|
108
108
|
`loadMedia({platform:"gbc", path})` expects gambatte in CGB mode. If
|
|
109
|
-
your ROM was built with `platform:"gb"` (no
|
|
109
|
+
your ROM was built with `platform:"gb"` (no gbHeader patch) the file
|
|
110
110
|
extension is `.gb` and the header CGB byte is $00, so gambatte starts
|
|
111
111
|
in DMG mode. To switch a DMG ROM to CGB:
|
|
112
112
|
|
|
113
113
|
1. Rename / re-extension to `.gbc`
|
|
114
|
-
2. Run `
|
|
114
|
+
2. Run `romPatch({op:'gbHeader', path:"out.gbc"})` — also fixes the global
|
|
115
115
|
checksum that the boot ROM checks
|
|
116
116
|
|
|
117
117
|
## "Sound is the same as DMG"
|
|
@@ -125,7 +125,7 @@ sound channels or extra waveforms.
|
|
|
125
125
|
The bundled GBC scaffolds all fit in 32 KB (single bank, no MBC).
|
|
126
126
|
For larger projects use an MBC (memory bank controller). MBC1 / MBC3
|
|
127
127
|
work in gambatte; set the `$0147` cartridge type byte accordingly.
|
|
128
|
-
|
|
128
|
+
romPatch({op:'gbHeader'}) doesn't set this — you write it from your asm/C.
|
|
129
129
|
|
|
130
130
|
## "Frame heartbeat feels janky / slow"
|
|
131
131
|
|
|
@@ -7,7 +7,7 @@ the color-aware scaffolds; everything below is in lockstep with
|
|
|
7
7
|
the GB tree.
|
|
8
8
|
|
|
9
9
|
CGB-specific:
|
|
10
|
-
- `
|
|
10
|
+
- `romPatch({op:'gbHeader', cgb: true})` sets $0143 = $80 → gambatte boots
|
|
11
11
|
in CGB mode with color palette RAM active
|
|
12
12
|
- VRAM bank 1 (selected via VBK = $FF4F) holds per-tile attribute
|
|
13
13
|
bytes (palette index, H/V flip, BG-OAM priority)
|
|
@@ -53,7 +53,7 @@ upstream README. Songs are exported from hUGETracker
|
|
|
53
53
|
- "OAM DMA wedges sprites" → see `MENTAL_MODEL.md` § R26 footguns +
|
|
54
54
|
`gb_runtime.c` `oam_dma_copy` implementation
|
|
55
55
|
- "BGP write does nothing" → check $0143 (CGB flag) via
|
|
56
|
-
`
|
|
56
|
+
`romPatch({op:'gbHeader'})` + Pan Docs § "The Cartridge Header"
|
|
57
57
|
- "How does hUGEDriver process a song row?" → `hUGEDriver.c`
|
|
58
58
|
`hUGE_dosound` body — fully readable
|
|
59
59
|
- "Why is gambatte refusing my ROM?" → check the header, then
|
|
@@ -23,12 +23,12 @@ run rgbfix on the linked GB/GBC ROM — valid Nintendo logo at $0104,
|
|
|
23
23
|
header checksum at $014D, global checksum at $014E, cartridge-type /
|
|
24
24
|
RAM-size bytes, and the CGB flag at $0143 ($80/$C0 for `.gbc`, $00 for
|
|
25
25
|
`.gb`). A freshly built ROM boots on hardware and strict cores with **no
|
|
26
|
-
extra step** — you do not call `
|
|
26
|
+
extra step** — you do not call `romPatch({op:'gbHeader'})` after a normal build.
|
|
27
27
|
|
|
28
28
|
Reach for header tooling only when working with a ROM the build pipeline
|
|
29
29
|
didn't produce, or to override a field:
|
|
30
30
|
|
|
31
|
-
- `
|
|
31
|
+
- `romPatch({op:'gbHeader', path: "out.gb"})` — romdev tool.
|
|
32
32
|
Fixes up / overrides the header of an existing ROM on disk (title, cart
|
|
33
33
|
type, ROM/RAM size, CGB flag, etc.).
|
|
34
34
|
- `node patch-header.js out.gb` — standalone Node script, copied into
|
|
@@ -110,7 +110,7 @@ If you need to write a custom VRAM block-copy:
|
|
|
110
110
|
|
|
111
111
|
This is independent of the R26 OAM-alignment fix (`shadow_oam __at
|
|
112
112
|
(0xC100)`) and the header CGB-flag fix (now applied automatically by
|
|
113
|
-
`build({output:'rom'})` / `build({output:'run'})`, not a manual `
|
|
113
|
+
`build({output:'rom'})` / `build({output:'run'})`, not a manual `romPatch({op:'gbHeader'})` step). All
|
|
114
114
|
three are silent-failure bugs that look like "did my changes even
|
|
115
115
|
land?" and need different fixes.
|
|
116
116
|
|
|
@@ -73,19 +73,20 @@ check the live OAM Y bytes for $D0 in a slot before them. That's
|
|
|
73
73
|
still the diagnosis; the runtime just doesn't create the problem
|
|
74
74
|
on its own anymore.
|
|
75
75
|
|
|
76
|
-
### R6 sprite-tile-base default
|
|
77
|
-
|
|
78
|
-
`gg_vdp_init()` sets R6 =
|
|
79
|
-
sprite tile data —
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
76
|
+
### R6 sprite-tile-base: default is $2000 (0xFF)
|
|
77
|
+
|
|
78
|
+
`gg_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
|
|
79
|
+
sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
|
|
80
|
+
from `$2000-$3FFF`, in their **own bank** separate from BG tiles at
|
|
81
|
+
$0000. This is the baseline because every bundled scaffold uploads
|
|
82
|
+
its sprite tiles to `$2000` (`gg_load_tiles(0x2000, …)`) — the
|
|
83
|
+
default and the scaffolds match, so sprites Just Show Up.
|
|
84
|
+
|
|
85
|
+
Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
|
|
86
|
+
(sharing the BG bank). If you ever set R6=0xFB you MUST also upload
|
|
87
|
+
your sprite tiles to $0000, or the VDP reads the empty/BG bank and
|
|
88
|
+
every sprite is invisible — the classic GG/SMS "my sprites don't
|
|
89
|
+
show up" trap.
|
|
89
90
|
|
|
90
91
|
The `sprites({op:'inspect'})` tool's `spriteTileDataBase` field reports the
|
|
91
92
|
address the VDP is actually reading from — trust that over any
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* Writes the 11 mode-4 registers to a sane baseline:
|
|
4
4
|
* display OFF, vblank IRQ off, 192-line mode 4, name table at $3800,
|
|
5
5
|
* BG tile data at $0000, sprite attr table at $3F00, sprite tile data
|
|
6
|
-
* at $
|
|
6
|
+
* at $2000 (R6=0xFF → SA13 set → tiles read from $2000-$3FFF). Call
|
|
7
7
|
* once after reset before uploading palette/tiles/map.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* sprite tiles
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Sprite-tile base: R6 bit 2 (SA13) selects $0000 (clear) vs $2000 (set).
|
|
10
|
+
* We default to R6=0xFF ($2000) because EVERY bundled scaffold uploads its
|
|
11
|
+
* sprite tiles to $2000 (gg_load_tiles(0x2000, ...)) — so the baseline must
|
|
12
|
+
* match what consumers do, or sprites read from the empty/BG bank and render
|
|
13
|
+
* invisible. (Many SMS/GG references say "R6=0xFB → $2000", which is backwards:
|
|
14
|
+
* 0xFB has SA13 CLEAR = $0000.) If you instead keep sprite tiles in the BG
|
|
15
|
+
* bank at $0000, set R6=0xFB. sprites({op:'inspect'}) → spriteTileDataBase
|
|
16
|
+
* shows the address the VDP is actually reading from.
|
|
15
17
|
*
|
|
16
18
|
* After loading assets, enable display by re-writing R1 with bit 6 set:
|
|
17
19
|
* gg_vdp_display_on();
|
|
@@ -33,7 +35,7 @@ void gg_vdp_init(void) {
|
|
|
33
35
|
0xFF, /* R3: color table (ignored in M4) */
|
|
34
36
|
0xFF, /* R4: BG tile data at $0000 */
|
|
35
37
|
0xFF, /* R5: sprite attr table at $3F00 */
|
|
36
|
-
|
|
38
|
+
0xFF, /* R6: sprite tile data at $2000 (SA13 set; scaffolds upload here) */
|
|
37
39
|
0x00, /* R7: border = sprite palette entry 0 */
|
|
38
40
|
0x00, /* R8: BG X scroll */
|
|
39
41
|
0x00, /* R9: BG Y scroll */
|
|
@@ -106,7 +106,7 @@ exactly this.
|
|
|
106
106
|
- `palette({source:'live'})` — V9938 9-bit GRB (or TMS9918 fixed) 16 entries.
|
|
107
107
|
- `sprites({op:'inspect'})` — VRAM sprite-attribute table, up to 32 sprites.
|
|
108
108
|
- `symbols({op:'map', map})` — pass the sdld `.map` (the `symbols` field from
|
|
109
|
-
|
|
109
|
+
build({output:'romWithDebug'})) to see where SDCC placed your variables/code, grouped by
|
|
110
110
|
region (bios / cart_rom / work_ram).
|
|
111
111
|
- `audioDebug({op:'inspect', chip: "ay8910"})` — the AY-3-8910 PSG: 3 square-wave
|
|
112
112
|
channels (tone period→Hz, amplitude, tone/noise enable) + a shared noise
|
|
@@ -140,7 +140,7 @@ Overflow it and there's no error — your globals quietly collide with
|
|
|
140
140
|
the stack or shadow OAM → corrupted state, sprites that flicker to
|
|
141
141
|
garbage, random crashes.
|
|
142
142
|
|
|
143
|
-
**Check the `ramUsage` field in the
|
|
143
|
+
**Check the `ramUsage` field in the build response** —
|
|
144
144
|
it lists your BSS / DATA / ZEROPAGE segment sizes from the linker map.
|
|
145
145
|
If BSS+DATA is approaching the config's RAM region, shrink your state:
|
|
146
146
|
prefer `uint8_t` over `int`, bit-pack flags, use small fixed arrays,
|
|
@@ -56,6 +56,7 @@ volatile uint8_t nmi_counter = 0;
|
|
|
56
56
|
* (so OAM segment placement at $0200 is linker-enforced). oam_index
|
|
57
57
|
* tracks the next free slot for oam_spr(). */
|
|
58
58
|
static uint8_t oam_index = 0;
|
|
59
|
+
static void oam_hide_unused(void); /* fwd decl — used by ppu_wait_nmi (NES-1) */
|
|
59
60
|
|
|
60
61
|
/* ── VRAM write queue ─────────────────────────────────────────────
|
|
61
62
|
* Each entry is { hi, lo, byte }. NMI walks the queue, writes
|
|
@@ -130,7 +131,12 @@ void ppu_wait_vblank(void) {
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
void ppu_wait_nmi(void) {
|
|
133
|
-
uint8_t target
|
|
134
|
+
uint8_t target;
|
|
135
|
+
/* Hide last frame's now-unused sprite slots BEFORE waiting, so the buffer
|
|
136
|
+
* the NMI's OAM-DMA copies is fully staged (live slots written by oam_spr,
|
|
137
|
+
* stale slots parked off-screen) — never a half-cleared buffer (NES-1). */
|
|
138
|
+
oam_hide_unused();
|
|
139
|
+
target = (uint8_t)(nmi_counter + 1);
|
|
134
140
|
while (nmi_counter != target) { /* spin */ }
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -159,15 +165,31 @@ void palette_load(const uint8_t *pal32) {
|
|
|
159
165
|
|
|
160
166
|
/* ── OAM ──────────────────────────────────────────────────────── */
|
|
161
167
|
|
|
168
|
+
/* High-water mark: the largest oam_index reached last frame. Lets us blank
|
|
169
|
+
* ONLY the slots a frame stopped using, instead of the whole 256-byte buffer
|
|
170
|
+
* every frame. */
|
|
171
|
+
static uint8_t oam_high = 0;
|
|
172
|
+
|
|
162
173
|
void oam_clear(void) {
|
|
174
|
+
/* NES-1 FIX: do NOT blank the whole shadow buffer here. The old full clear
|
|
175
|
+
* wrote slot 0's Y=$FF FIRST and took ~hundreds of cycles; if the NMI's
|
|
176
|
+
* OAM-DMA fired mid-clear it copied a HALF-CLEARED buffer → the live sprite
|
|
177
|
+
* vanished every other frame (the classic "sprite flickers to black"
|
|
178
|
+
* sprite-light scaffold bug). Instead we just reset the staging index here;
|
|
179
|
+
* ppu_wait_nmi() hides the slots this frame stopped using, so the DMA only
|
|
180
|
+
* ever sees a fully-staged buffer. */
|
|
181
|
+
oam_index = 0;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* Hide slots [oam_index .. oam_high] (the ones used last frame but not this
|
|
185
|
+
* frame) by parking their Y off-screen. Called from ppu_wait_nmi AFTER the
|
|
186
|
+
* game has staged its live sprites, so live slots are never blanked. */
|
|
187
|
+
static void oam_hide_unused(void) {
|
|
163
188
|
uint16_t i;
|
|
164
|
-
for (i =
|
|
189
|
+
for (i = oam_index; i < (uint16_t)oam_high + 4 && i < 256; i += 4) {
|
|
165
190
|
shadow_oam[i] = 0xFF; /* Y off-screen */
|
|
166
|
-
shadow_oam[i + 1] = 0; /* tile */
|
|
167
|
-
shadow_oam[i + 2] = 0; /* attr */
|
|
168
|
-
shadow_oam[i + 3] = 0; /* X */
|
|
169
191
|
}
|
|
170
|
-
|
|
192
|
+
oam_high = oam_index;
|
|
171
193
|
}
|
|
172
194
|
|
|
173
195
|
void oam_spr(uint8_t x, uint8_t y, uint8_t tile, uint8_t attr) {
|
|
@@ -90,7 +90,7 @@ screen. Keep at least one (2+ byte) global. See TROUBLESHOOTING.md.
|
|
|
90
90
|
- `background({view:'renderState'})` — VDC R5 screen-enable, BG scroll, SATB source.
|
|
91
91
|
- `palette({source:'live'})` — VCE 512-entry 9-bit GRB (area:'bg'|'sprite').
|
|
92
92
|
- `sprites({op:'inspect'})` — SATB 64 sprites (x/y/tile/palette/size/flip).
|
|
93
|
-
- `symbols({op:'map'})` — where cc65 placed your variables (after
|
|
93
|
+
- `symbols({op:'map'})` — where cc65 placed your variables (after build({output:'romWithDebug'})).
|
|
94
94
|
- `audioDebug({op:'inspect', chip: "pce"})` — the HuC6280 PSG: 6 wavetable channels
|
|
95
95
|
(per-channel freq/volume/wave; channels 4-5 can also do noise) + main amplitude
|
|
96
96
|
+ LFO.
|
|
@@ -96,6 +96,7 @@ void vdc_set_reg(u8 reg, u16 val); /* select reg, write 16-bit valu
|
|
|
96
96
|
void vram_set_write_addr(u16 addr); /* point MAWR + arm VWR streaming */
|
|
97
97
|
void vram_write(u16 addr, const u16 *data, u16 n); /* upload n words to VRAM[addr] */
|
|
98
98
|
void vce_set_color(u16 idx, u16 grb); /* set VCE palette entry (0..511) */
|
|
99
|
+
void vdc_init(void); /* program VDC display timing (256x224 NTSC); auto-run by *_enable */
|
|
99
100
|
void bg_enable(void); /* VDC R5: background on + VBlank IRQ (so waitvsync works) */
|
|
100
101
|
void spr_enable(void); /* VDC R5: sprites on + VBlank IRQ */
|
|
101
102
|
void disp_enable(void); /* VDC R5: BG + SPR + VBlank IRQ on at once */
|
|
@@ -70,17 +70,43 @@ void vblank_irq_enable(void) {
|
|
|
70
70
|
vdc_set_reg(VDC_CR, _pce_cr);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/* Program the VDC display-timing registers for a standard NTSC 256x224 (H32)
|
|
74
|
+
* screen. WITHOUT this the geargrafx core falls back to power-on register
|
|
75
|
+
* defaults that composite the 32-row BAT into the display DOUBLED (the scene
|
|
76
|
+
* drawn twice, top + bottom halves, with a black right margin) — the PCE-1
|
|
77
|
+
* "doubled picture" bug. Values match cc65's pce.lib / standard PCE homebrew:
|
|
78
|
+
* MWR R9 = 32x32 virtual screen, 256px-wide BAT
|
|
79
|
+
* HSR R10 / HDR R11 = 256px (32 char) horizontal display
|
|
80
|
+
* VPR R12 / VDW R13 / VCR R14 = 224-line vertical window
|
|
81
|
+
* Called automatically the first time the display is enabled (idempotent). */
|
|
82
|
+
static u8 _pce_vdc_inited = 0;
|
|
83
|
+
void vdc_init(void) {
|
|
84
|
+
if (_pce_vdc_inited) return;
|
|
85
|
+
_pce_vdc_inited = 1;
|
|
86
|
+
vdc_set_reg(VDC_MWR, 0x0010); /* 32x32 virtual map, 256px BAT */
|
|
87
|
+
vdc_set_reg(VDC_BXR, 0x0000); /* BG X scroll = 0 */
|
|
88
|
+
vdc_set_reg(VDC_BYR, 0x0000); /* BG Y scroll = 0 */
|
|
89
|
+
vdc_set_reg(VDC_HSR, 0x0202); /* horizontal sync width/start */
|
|
90
|
+
vdc_set_reg(VDC_HDR, 0x031F); /* horizontal display = 32 chars (256px) */
|
|
91
|
+
vdc_set_reg(VDC_VPR, 0x0F02); /* vertical sync */
|
|
92
|
+
vdc_set_reg(VDC_VDW, 0x00DF); /* vertical display = 224 lines */
|
|
93
|
+
vdc_set_reg(VDC_VCR, 0x00EE); /* vertical display end */
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
void bg_enable(void) {
|
|
97
|
+
vdc_init();
|
|
74
98
|
_pce_cr |= (VDC_CR_BG_ON | VDC_CR_VBLANK_IRQ);
|
|
75
99
|
vdc_set_reg(VDC_CR, _pce_cr);
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
void spr_enable(void) {
|
|
103
|
+
vdc_init();
|
|
79
104
|
_pce_cr |= (VDC_CR_SPR_ON | VDC_CR_VBLANK_IRQ);
|
|
80
105
|
vdc_set_reg(VDC_CR, _pce_cr);
|
|
81
106
|
}
|
|
82
107
|
|
|
83
108
|
void disp_enable(void) {
|
|
109
|
+
vdc_init();
|
|
84
110
|
_pce_cr |= (VDC_CR_BG_ON | VDC_CR_SPR_ON | VDC_CR_VBLANK_IRQ);
|
|
85
111
|
vdc_set_reg(VDC_CR, _pce_cr);
|
|
86
112
|
}
|
|
@@ -78,7 +78,7 @@ R1 = 0x80 display OFF, vblank IRQ off, 192-line
|
|
|
78
78
|
R2 = 0xFF name table at $3800
|
|
79
79
|
R4 = 0xFF BG tile data at $0000
|
|
80
80
|
R5 = 0xFF sprite attr table at $3F00
|
|
81
|
-
R6 =
|
|
81
|
+
R6 = 0xFF sprite tile data at $2000 (own bank; scaffolds upload here)
|
|
82
82
|
R7 = 0x00 border colour
|
|
83
83
|
```
|
|
84
84
|
|
|
@@ -142,19 +142,19 @@ buffer in WRAM and uploads it to the SAT each vblank.
|
|
|
142
142
|
`sprites({op:'inspect'})` shows the live OAM bytes + reports
|
|
143
143
|
`spriteTileDataBase` — trust it over comments when sprites misbehave.
|
|
144
144
|
|
|
145
|
-
### R6 sprite-tile-base default
|
|
145
|
+
### R6 sprite-tile-base: default is $2000 (0xFF)
|
|
146
146
|
|
|
147
|
-
`sms_vdp_init()` sets R6 =
|
|
148
|
-
sprite tile data —
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
`sms_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
|
|
148
|
+
sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
|
|
149
|
+
from `$2000-$3FFF`, their **own bank** separate from BG tiles at
|
|
150
|
+
$0000. This matches every bundled scaffold, which uploads sprite
|
|
151
|
+
tiles to `$2000` (`sms_load_tiles(0x2000, …)`) — default and
|
|
152
|
+
scaffolds agree, so sprites render.
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
|
|
155
|
+
(shared with the BG bank). If you set R6=0xFB you MUST upload your
|
|
156
|
+
sprite tiles to $0000 too, or the VDP reads the empty/BG bank and
|
|
157
|
+
every sprite is invisible.
|
|
158
158
|
|
|
159
159
|
## Palette (CRAM)
|
|
160
160
|
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* Writes the 11 mode-4 registers to a sane baseline:
|
|
4
4
|
* display OFF, vblank IRQ off, 192-line mode 4, name table at $3800,
|
|
5
5
|
* BG tile data at $0000, sprite attr table at $3F00, sprite tile data
|
|
6
|
-
* at $
|
|
6
|
+
* at $2000 (R6=0xFF → SA13 set → tiles read from $2000-$3FFF). Call
|
|
7
7
|
* once after reset before uploading palette/tiles/map.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* sprite tiles
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* Sprite-tile base: R6 bit 2 (SA13) selects $0000 (clear) vs $2000 (set).
|
|
10
|
+
* We default to R6=0xFF ($2000) because EVERY bundled scaffold uploads its
|
|
11
|
+
* sprite tiles to $2000 (sms_load_tiles(0x2000, ...)) — so the baseline must
|
|
12
|
+
* match what consumers do, or sprites read from the empty/BG bank and render
|
|
13
|
+
* invisible. (Many SMS references say "R6=0xFB → $2000", which is backwards:
|
|
14
|
+
* 0xFB has SA13 CLEAR = $0000.) If you instead keep sprite tiles in the BG
|
|
15
|
+
* bank at $0000, set R6=0xFB. sprites({op:'inspect'}) → spriteTileDataBase
|
|
16
|
+
* shows the address the VDP is actually reading from.
|
|
15
17
|
*
|
|
16
18
|
* After loading assets, enable display by re-writing R1 with bit 6 set:
|
|
17
19
|
* sms_vdp_display_on();
|
|
@@ -33,7 +35,7 @@ void sms_vdp_init(void) {
|
|
|
33
35
|
0xFF, /* R3: color table (ignored in M4) */
|
|
34
36
|
0xFF, /* R4: BG tile data at $0000 */
|
|
35
37
|
0xFF, /* R5: sprite attr table at $3F00 */
|
|
36
|
-
|
|
38
|
+
0xFF, /* R6: sprite tile data at $2000 (SA13 set; scaffolds upload here) */
|
|
37
39
|
0x00, /* R7: border = sprite palette entry 0 */
|
|
38
40
|
0x00, /* R8: BG X scroll */
|
|
39
41
|
0x00, /* R9: BG Y scroll */
|
|
@@ -40,7 +40,7 @@ _vdp_init_regs:
|
|
|
40
40
|
.db $FF ; R3: color table — M4 ignores
|
|
41
41
|
.db $FF ; R4: BG tile data — M4: bit 2 selects $0000 vs $2000
|
|
42
42
|
.db $FF ; R5: sprite attr table base ($3F00 = $7E << 7)
|
|
43
|
-
.db $
|
|
43
|
+
.db $FF ; R6: sprite tile data at $2000 (SA13 set; scaffolds upload here)
|
|
44
44
|
.db $00 ; R7: border color = sprite palette entry 0
|
|
45
45
|
.db $00 ; R8: BG X scroll
|
|
46
46
|
.db $00 ; R9: BG Y scroll
|
package/src/playtest/playtest.js
CHANGED
|
@@ -148,8 +148,33 @@ async function getSdl() {
|
|
|
148
148
|
try {
|
|
149
149
|
const ns = await import("@kmamal/sdl");
|
|
150
150
|
_sdlModule = ns.default || ns;
|
|
151
|
+
// GROUND-TRUTH visibility check (cross-platform, NOT env-var guessing):
|
|
152
|
+
// SDL picks a video driver at init. With no presentable surface (no desktop
|
|
153
|
+
// session, no Xvfb, headless box) it falls back to "offscreen"/"dummy" —
|
|
154
|
+
// createWindow then SUCCEEDS and audio plays, but nothing appears on any
|
|
155
|
+
// physical screen. That's the silent "agent says the window's up, user sees
|
|
156
|
+
// nothing (but hears sound)" failure. We catch it HERE by asking SDL which
|
|
157
|
+
// driver it actually selected — works the same on Linux/macOS/Windows, and
|
|
158
|
+
// correctly ALLOWS a real offscreen X server (Xvfb reports "x11", not
|
|
159
|
+
// "offscreen"). Headless rendering (screenshot/runSource) never calls this,
|
|
160
|
+
// so offscreen stays perfectly fine for everything except opening a window
|
|
161
|
+
// for a human.
|
|
162
|
+
const driver = _sdlModule?.info?.drivers?.video?.current;
|
|
163
|
+
if (driver === "offscreen" || driver === "dummy") {
|
|
164
|
+
throw tag(new Error(
|
|
165
|
+
`SDL selected the "${driver}" video driver — there is no presentable display, ` +
|
|
166
|
+
"so a playtest window would render but never appear on a physical screen " +
|
|
167
|
+
"(you'd hear audio but see nothing). The server must run where it has a real " +
|
|
168
|
+
"display: start it from a terminal INSIDE your logged-in desktop session " +
|
|
169
|
+
"(`npx romdevtools`), then point your agent at that server. (A server spawned " +
|
|
170
|
+
"by your agent host, over plain SSH, or from a tty/headless box has no display. " +
|
|
171
|
+
"A virtual display like Xvfb works too — it reports as the real driver, not " +
|
|
172
|
+
"\"offscreen\".)",
|
|
173
|
+
), "no-display");
|
|
174
|
+
}
|
|
151
175
|
return _sdlModule;
|
|
152
176
|
} catch (e) {
|
|
177
|
+
if (e?.sdlKind) throw e; // already-tagged (e.g. the offscreen check above)
|
|
153
178
|
const isModuleErr = e?.code === "ERR_MODULE_NOT_FOUND" ||
|
|
154
179
|
/sdl\.node|dist[\\/]/.test(e?.message || "");
|
|
155
180
|
throw tag(new Error(e?.message ?? String(e)),
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# runtime. There is no CHARS segment.
|
|
13
13
|
#
|
|
14
14
|
# To use this preset:
|
|
15
|
-
# 1. linkerConfig: "chr-ram" on
|
|
15
|
+
# 1. linkerConfig: "chr-ram" on build({output:'rom'})
|
|
16
16
|
# 2. Supply your own crt0/header source that writes the 16-byte iNES
|
|
17
17
|
# header with byte 5 = 0. Easiest: paste the snippet
|
|
18
18
|
# getStarterSnippet({platform:"nes", name:"chr_ram_header", language:"asm"}).
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# runtime. There is no CHARS segment.
|
|
13
13
|
#
|
|
14
14
|
# To use this preset:
|
|
15
|
-
# 1. linkerConfig: "chr-ram" on
|
|
15
|
+
# 1. linkerConfig: "chr-ram" on build({output:'rom'})
|
|
16
16
|
# 2. Supply your own crt0/header source that writes the 16-byte iNES
|
|
17
17
|
# header with byte 5 = 0. Easiest: paste the snippet
|
|
18
18
|
# getStarterSnippet({platform:"nes", name:"chr_ram_header", language:"asm"}).
|
|
@@ -91,7 +91,7 @@ _exit: jsr donelib
|
|
|
91
91
|
; rti
|
|
92
92
|
;
|
|
93
93
|
; ; then DELETE the `nmi: rti` line below from this crt0 (or load a
|
|
94
|
-
; ; modified crt0 via your own
|
|
94
|
+
; ; modified crt0 via your own build({output:'rom'}) sources entry).
|
|
95
95
|
|
|
96
96
|
.segment "STARTUP"
|
|
97
97
|
|
|
@@ -23,7 +23,7 @@ once the WASM artifacts ship. Pipeline shape:
|
|
|
23
23
|
- **WASM port of cc1/as/ld (stage 2)**: pending.
|
|
24
24
|
- **SGDK native build against the cross-toolchain (stage 3)**: pending.
|
|
25
25
|
- **JS driver `buildGenesisC()`**: pending.
|
|
26
|
-
- **
|
|
26
|
+
- **build({output:'rom'}) wiring**: pending.
|
|
27
27
|
|
|
28
28
|
## Why a 3-stage build
|
|
29
29
|
|
|
@@ -68,13 +68,53 @@ export function lintSdccSource(source, file = "main.c", opts = {}) {
|
|
|
68
68
|
// CONSERVATIVE: only flag when we can SEE the counter declared u8 and
|
|
69
69
|
// the bound is a constant we can evaluate — never guess.
|
|
70
70
|
{
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// Build a SCOPE-AWARE map of where each name is declared and at what
|
|
72
|
+
// width. A name like `i` is commonly re-declared in several functions
|
|
73
|
+
// — some as uint8_t, some as uint16_t. A flat "is this name ever u8"
|
|
74
|
+
// set wrongly flags the uint16_t loop just because a DIFFERENT
|
|
75
|
+
// function declared its own `i` as uint8_t (the SMS/GG default
|
|
76
|
+
// scaffold false-positive). Instead we record EVERY declaration's
|
|
77
|
+
// line + width, then for each loop consult the nearest declaration of
|
|
78
|
+
// the counter that appears ABOVE the loop — i.e. the one actually in
|
|
79
|
+
// scope — and only flag it when that declaration is 8-bit.
|
|
80
|
+
//
|
|
81
|
+
// decls: Map<name, Array<{line:number, u8:boolean}>> (line is 1-based)
|
|
82
|
+
const decls = new Map();
|
|
83
|
+
const addDecl = (names, line, u8) => {
|
|
84
|
+
for (const raw of names.split(",")) {
|
|
85
|
+
const n = raw.trim();
|
|
86
|
+
if (!n) continue;
|
|
87
|
+
if (!decls.has(n)) decls.set(n, []);
|
|
88
|
+
decls.get(n).push({ line, u8 });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const u8re = /\b(?:unsigned\s+char|char|u8|uint8_t|uint8|int8_t|int8|signed\s+char)\s+([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)\s*;/;
|
|
92
|
+
// 16-bit-or-wider integer declarations (and float/double, which also
|
|
93
|
+
// can't overflow at 255). Anything not 8-bit is "wide" for our purpose.
|
|
94
|
+
const wideRe = /\b(?:unsigned\s+(?:short|int|long)|signed\s+(?:short|int|long)|short|int|long|u16|u32|u64|uint16_t|uint16|int16_t|int16|uint32_t|uint32|int32_t|int32|uint64_t|uint64|int64_t|int64|size_t|ptrdiff_t)\s+([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)\s*;/;
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const code = lines[i].replace(/\/\/.*$/, "").replace(/\/\*.*?\*\//g, "");
|
|
97
|
+
const m8 = code.match(u8re);
|
|
98
|
+
if (m8) { addDecl(m8[1], i + 1, true); continue; }
|
|
99
|
+
const mw = code.match(wideRe);
|
|
100
|
+
if (mw) addDecl(mw[1], i + 1, false);
|
|
77
101
|
}
|
|
102
|
+
// Is the counter declared 8-bit in the scope visible at `loopLine`?
|
|
103
|
+
// Use the NEAREST declaration above the loop. If the nearest visible
|
|
104
|
+
// declaration is wide (uint16_t etc.), the loop is fine.
|
|
105
|
+
const counterIsU8AtLine = (counter, loopLine) => {
|
|
106
|
+
const ds = decls.get(counter);
|
|
107
|
+
if (!ds) return false;
|
|
108
|
+
let best = null;
|
|
109
|
+
for (const d of ds) {
|
|
110
|
+
if (d.line <= loopLine && (best === null || d.line > best.line)) best = d;
|
|
111
|
+
}
|
|
112
|
+
// No declaration above the loop (e.g. param/global declared after, or
|
|
113
|
+
// out-of-order) — fall back to "flag only if EVERY decl is u8" so we
|
|
114
|
+
// never wolf-cry on a name that is also declared wide somewhere.
|
|
115
|
+
if (best === null) return ds.every((d) => d.u8);
|
|
116
|
+
return best.u8;
|
|
117
|
+
};
|
|
78
118
|
const evalConst = (expr) => {
|
|
79
119
|
// Only literals and pure `A * B [* C]` products of decimal/hex ints.
|
|
80
120
|
const t = expr.trim();
|
|
@@ -91,7 +131,7 @@ export function lintSdccSource(source, file = "main.c", opts = {}) {
|
|
|
91
131
|
const m = code.match(/\bfor\s*\([^;]*;\s*([A-Za-z_]\w*)\s*<\s*([^;]+?)\s*;/);
|
|
92
132
|
if (!m) continue;
|
|
93
133
|
const [, counter, boundExpr] = m;
|
|
94
|
-
if (!
|
|
134
|
+
if (!counterIsU8AtLine(counter, i + 1)) continue;
|
|
95
135
|
const bound = evalConst(boundExpr);
|
|
96
136
|
if (bound !== null && bound > 255) {
|
|
97
137
|
issues.push({
|
|
@@ -148,13 +148,9 @@ function normalizeSnesSources(args) {
|
|
|
148
148
|
if (cFiles.length === 0) {
|
|
149
149
|
throw new Error("buildSnesC: `sources` must include at least one .c file.");
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
`Today only one .c file is supported per build — combine via #include or wait for ` +
|
|
155
|
-
`multi-TU support. .asm/.s siblings work fine.`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
151
|
+
// Multiple .c files ARE supported: buildWithPvSnesLib compiles each to its
|
|
152
|
+
// own .obj (tcc→wla) and links them all (Stage 1 + Stage 3). The genre
|
|
153
|
+
// scaffolds ship main.c + snes_sfx.c and rely on this.
|
|
158
154
|
return args.sources;
|
|
159
155
|
}
|
|
160
156
|
if (typeof args.source === "string") {
|