romdevtools 0.16.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 +75 -16
- package/CHANGELOG.md +316 -0
- package/examples/README.md +2 -0
- package/examples/atari2600/templates/platformer.asm +460 -0
- package/examples/atari2600/templates/racing.asm +463 -0
- package/examples/atari2600/templates/shmup.asm +386 -0
- package/examples/atari2600/templates/sports.asm +362 -0
- package/examples/atari7800/templates/default.c +49 -5
- package/examples/atari7800/templates/hello_sprite.c +48 -4
- package/examples/atari7800/templates/music_demo.c +47 -2
- package/examples/atari7800/templates/platformer.c +43 -4
- package/examples/atari7800/templates/puzzle.c +39 -4
- package/examples/atari7800/templates/racing.c +39 -4
- package/examples/atari7800/templates/shmup.c +40 -2
- package/examples/atari7800/templates/sports.c +36 -5
- package/examples/c64/templates/platformer.c +19 -5
- package/examples/c64/templates/puzzle.c +32 -2
- package/examples/c64/templates/shmup.c +28 -2
- package/examples/c64/templates/sports.c +30 -2
- package/examples/c64/templates/tile_engine.c +77 -27
- package/examples/gb/templates/default.c +110 -16
- package/examples/gb/templates/hello_sprite.c +15 -6
- package/examples/gb/templates/music_demo.c +36 -0
- package/examples/gb/templates/platformer.c +28 -6
- package/examples/gb/templates/puzzle.c +35 -4
- package/examples/gb/templates/racing.c +75 -10
- package/examples/gb/templates/shmup.c +41 -3
- package/examples/gb/templates/sports.c +51 -3
- package/examples/gb/templates/tile_engine.c +3 -2
- package/examples/gba/templates/gba_hello.c +29 -11
- package/examples/gba/templates/maxmod_demo.c +36 -2
- package/examples/gba/templates/platformer.c +3 -1
- package/examples/gba/templates/puzzle.c +15 -3
- package/examples/gba/templates/racing.c +65 -3
- package/examples/gba/templates/shmup.c +41 -4
- package/examples/gba/templates/sports.c +36 -2
- package/examples/gba/templates/tonc_hello.c +41 -5
- package/examples/gba/templates/tonc_hello_sprite.c +35 -1
- package/examples/gbc/templates/default.c +103 -26
- package/examples/gbc/templates/hello_sprite.c +12 -3
- package/examples/gbc/templates/music_demo.c +56 -12
- package/examples/gbc/templates/platformer.c +28 -6
- package/examples/gbc/templates/puzzle.c +35 -4
- package/examples/gbc/templates/racing.c +88 -21
- package/examples/gbc/templates/shmup.c +37 -3
- package/examples/gbc/templates/sports.c +48 -3
- 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/puzzle.c +37 -3
- package/examples/genesis/templates/racing.c +44 -11
- package/examples/genesis/templates/sgdk_hello.c +34 -1
- package/examples/genesis/templates/shmup.c +31 -1
- package/examples/genesis/templates/shmup_2p.c +31 -0
- package/examples/genesis/templates/xgm2_demo.c +20 -0
- package/examples/gg/templates/default.c +56 -18
- package/examples/gg/templates/hello_sprite.c +25 -2
- package/examples/gg/templates/music_demo.c +24 -2
- package/examples/gg/templates/platformer.c +18 -12
- package/examples/gg/templates/puzzle.c +38 -7
- package/examples/gg/templates/racing.c +58 -9
- package/examples/gg/templates/shmup.c +47 -3
- package/examples/gg/templates/sports.c +57 -16
- package/examples/gg/templates/tile_engine.c +12 -6
- package/examples/lynx/templates/default.c +39 -8
- package/examples/lynx/templates/hello_sprite.c +15 -1
- package/examples/lynx/templates/music_demo.c +13 -1
- package/examples/lynx/templates/puzzle.c +28 -1
- package/examples/lynx/templates/racing.c +34 -7
- package/examples/lynx/templates/shmup.c +42 -3
- package/examples/lynx/templates/sports.c +29 -2
- package/examples/msx/platformer/main.c +213 -0
- package/examples/msx/puzzle/main.c +250 -0
- package/examples/msx/racing/main.c +249 -0
- package/examples/msx/shmup/main.c +288 -0
- package/examples/msx/sports/main.c +182 -0
- package/examples/nes/templates/default.c +67 -19
- package/examples/nes/templates/hello_sprite.c +35 -0
- package/examples/nes/templates/music_demo.c +40 -0
- package/examples/nes/templates/platformer.c +65 -6
- package/examples/nes/templates/puzzle.c +67 -6
- package/examples/nes/templates/racing.c +45 -13
- package/examples/nes/templates/shmup.c +51 -2
- package/examples/nes/templates/sports.c +51 -6
- package/examples/pce/catch_game/main.c +22 -3
- package/examples/pce/music_sfx/main.c +28 -1
- package/examples/pce/platformer/main.c +283 -0
- package/examples/pce/puzzle/main.c +304 -0
- package/examples/pce/racing/main.c +304 -0
- package/examples/pce/shmup/main.c +346 -0
- package/examples/pce/sports/main.c +254 -0
- package/examples/pce/sprite_move/main.c +7 -2
- package/examples/sms/main.c +35 -6
- package/examples/sms/templates/hello_sprite.c +29 -3
- package/examples/sms/templates/music_demo.c +18 -4
- package/examples/sms/templates/puzzle.c +34 -5
- package/examples/sms/templates/racing.c +39 -2
- package/examples/sms/templates/shmup.c +41 -2
- package/examples/sms/templates/shmup_2p.c +24 -1
- package/examples/sms/templates/sports.c +47 -4
- 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/default.c +50 -28
- 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-data.asm +22 -0
- package/examples/snes/templates/platformer.c +20 -2
- package/examples/snes/templates/puzzle-data.asm +22 -0
- package/examples/snes/templates/puzzle.c +21 -2
- package/examples/snes/templates/racing-data.asm +22 -0
- package/examples/snes/templates/racing.c +17 -1
- package/examples/snes/templates/shmup-data.asm +22 -0
- package/examples/snes/templates/shmup.c +20 -1
- package/examples/snes/templates/sports-data.asm +22 -0
- package/examples/snes/templates/sports.c +16 -1
- package/package.json +1 -1
- package/src/cheats/gamegenie.js +0 -1
- package/src/cli/smoke.js +1 -3
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +191 -16
- package/src/host/callbacks.js +9 -1
- 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/host/types.js +15 -8
- package/src/http/routes.js +1 -1
- package/src/http/tool-registry.js +26 -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 +75 -4
- 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 +507 -0
- package/src/mcp/tools/disasm.js +97 -9
- package/src/mcp/tools/find-references.js +1 -2
- package/src/mcp/tools/font-map.js +1 -1
- package/src/mcp/tools/frame.js +168 -3
- 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 +18 -4
- 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 +54 -11
- 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/state.js +201 -14
- package/src/mcp/tools/tile-inspect.js +1 -1
- package/src/mcp/tools/toolchain.js +105 -12
- package/src/mcp/tools/watch-memory.js +137 -16
- 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/MENTAL_MODEL.md +45 -1
- package/src/platforms/c64/TROUBLESHOOTING.md +6 -0
- package/src/platforms/c64/d64.js +280 -0
- 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/MENTAL_MODEL.md +10 -0
- 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/MENTAL_MODEL.md +10 -6
- package/src/platforms/msx/TROUBLESHOOTING.md +6 -0
- package/src/platforms/nes/MENTAL_MODEL.md +63 -2
- 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/MENTAL_MODEL.md +9 -4
- package/src/platforms/pce/TROUBLESHOOTING.md +6 -0
- package/src/platforms/pce/lib/c/pce_video.c +1 -1
- 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/rom-id/identifier.js +15 -0
- package/src/toolchains/asar/asar.js +0 -9
- package/src/toolchains/assemble-snippet.js +30 -12
- package/src/toolchains/cc65/ines.js +145 -0
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +14 -1
- package/src/toolchains/cc65/presets/nes/chr-rom.cfg +83 -0
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +153 -0
- package/src/toolchains/common/reassemble.js +10 -3
- 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
|
@@ -211,6 +211,21 @@ Build calls explicitly point at these files via `sourcesPaths` /
|
|
|
211
211
|
`includePaths` + `linkerConfig: <contents of chr-ram-runtime.cfg>`. The
|
|
212
212
|
project README shows the exact incantation.
|
|
213
213
|
|
|
214
|
+
## Blank screen? Verify rendering before you guess (no vision needed)
|
|
215
|
+
|
|
216
|
+
If the screen looks black/blank, don't iterate blind — call
|
|
217
|
+
**`frame({op:'verify', frames:60})`**. One call fuses a framebuffer pixel scan
|
|
218
|
+
with the live PPU registers and tells you `{verified:true|false|null, issues[]}`:
|
|
219
|
+
- `renderDisabled` → PPUMASK has BG+sprites off (footgun, see below) — set
|
|
220
|
+
PPUMASK bits 3/4.
|
|
221
|
+
- `blankScreen`/`nearlyBlank` but render IS enabled → the PPU is on but nothing's
|
|
222
|
+
in the nametable/OAM/palette: check the loop-order + OAM-DMA footguns below, and
|
|
223
|
+
read the raw regions (`memory({op:'read', region:'nes_nametables'/'nes_oam'/'nes_palette'})`).
|
|
224
|
+
- `verified:null` (unsettled) → you haven't stepped a frame yet; step first.
|
|
225
|
+
|
|
226
|
+
It won't false-fire on boot, and it costs zero image tokens. Use it as the first
|
|
227
|
+
move whenever a change "did nothing" on screen.
|
|
228
|
+
|
|
214
229
|
## Five footguns to know before you start
|
|
215
230
|
|
|
216
231
|
Read these BEFORE writing your game-loop. Each one cost a previous
|
|
@@ -304,14 +319,60 @@ incorrectly aligned."
|
|
|
304
319
|
onChange:"reset", outputPath:...})` logs each note onset, or
|
|
305
320
|
`recordSession({memorySamples:[{region:"nes_apu_regs",...}], sampleEvery:1,
|
|
306
321
|
memoryOutputPath:...})` streams per-frame samples to disk.
|
|
307
|
-
- Mapper support —
|
|
308
|
-
MMC1/MMC3/UNROM you'll need a different linker config.
|
|
322
|
+
- Mapper support — the homebrew presets target NROM (no PRG banking). For
|
|
323
|
+
MMC1/MMC3/UNROM you'll need a different linker config. (For *rebuilding* an
|
|
324
|
+
existing CHR-ROM NROM game byte-identical, see "Rebuilding a CHR-ROM NROM
|
|
325
|
+
image" below — `inesHeader` / the `chr-rom` preset / `disasm({target:'project'})`.)
|
|
309
326
|
- IRQ — the IRQ vector returns. Most NES games use a custom IRQ
|
|
310
327
|
handler for mid-frame scroll splits; you'll need to write that asm.
|
|
311
328
|
- Multi-screen scrolling — the runtime sets one nametable; for big
|
|
312
329
|
scrolling worlds you need to manage the nametable buffer + bank
|
|
313
330
|
switching yourself.
|
|
314
331
|
|
|
332
|
+
## Rebuilding a CHR-ROM NROM image (reverse-engineering)
|
|
333
|
+
|
|
334
|
+
The homebrew presets above are CHR-**RAM** (the CPU uploads tiles at runtime).
|
|
335
|
+
Most *commercial* games are CHR-**ROM**: an 8 KB (or more) bank of fixed tile
|
|
336
|
+
data the PPU reads pattern tables from directly. When you rebuild a commercial
|
|
337
|
+
game from its disassembly into a byte-identical `.nes`, you need the iNES
|
|
338
|
+
header + the CHR-ROM blob + a linker config that concatenates HEADER + PRG +
|
|
339
|
+
CHR. romdev has three ways to do this so you never hand-derive header bytes or
|
|
340
|
+
write glue `.s`/`.cfg` files.
|
|
341
|
+
|
|
342
|
+
**The iNES header** (16 bytes at the very start of a `.nes`): `4E 45 53 1A`
|
|
343
|
+
("NES"+EOF), then byte 4 = PRG-ROM 16 KB bank count, byte 5 = CHR-ROM 8 KB bank
|
|
344
|
+
count (**0 = CHR-RAM**), byte 6 = flags6 (bit0 mirroring 0=horizontal/1=vertical,
|
|
345
|
+
bit1 battery, high nibble = mapper low nibble), byte 7 = flags7 (high nibble =
|
|
346
|
+
mapper high nibble), bytes 8-15 = 0. NROM is mapper 0; NROM-128 = 1 PRG bank
|
|
347
|
+
(maps at $C000, mirrored to $8000), NROM-256 = 2 PRG banks (maps at $8000).
|
|
348
|
+
|
|
349
|
+
**1. `build({inesHeader:{...}})` — the parametric, no-glue path (recommended).**
|
|
350
|
+
Pass `inesHeader: {prgBanks, chrBanks, mapper, mirroring}` and the build
|
|
351
|
+
auto-emits the HEADER segment, wires your CHR blob (from `binaryIncludePaths`)
|
|
352
|
+
into a CHARS segment, and uses a flat NROM `.cfg`. You supply only the PRG
|
|
353
|
+
source(s) + the CHR blob:
|
|
354
|
+
```
|
|
355
|
+
build({ output:'rom', platform:'nes',
|
|
356
|
+
sourcesPaths:{ "prg.asm": "bank0.asm" }, // the PRG disassembly
|
|
357
|
+
binaryIncludePaths:{ "chr.bin": "chr.bin" }, // extracted CHR-ROM
|
|
358
|
+
inesHeader:{ prgBanks:2, chrBanks:1, mapper:0, mirroring:"vertical" } })
|
|
359
|
+
```
|
|
360
|
+
Mutually exclusive with `linkerConfig`. Works for any NROM (mapper 0, ≤2 PRG
|
|
361
|
+
banks); for a banked mapper supply a linker `.cfg` that places each bank.
|
|
362
|
+
|
|
363
|
+
**2. `linkerConfig:"chr-rom"` — for homebrew C that ships FIXED tile art.**
|
|
364
|
+
A cc65-C preset (segment split + a CHARS segment in an 8 KB ROM2 bank). Put your
|
|
365
|
+
tiles in `.segment "CHARS"` (`.incbin "tiles.chr"`) + pass the blob via
|
|
366
|
+
`binaryIncludePaths`. It ships a companion crt0 with an 8 KB-CHR-ROM header. For
|
|
367
|
+
other bank configs, prefer `inesHeader`.
|
|
368
|
+
|
|
369
|
+
**3. `disasm({target:'project'})` — disassemble → rebuild, in two calls.**
|
|
370
|
+
For NES it now extracts the CHR-ROM to `chr.bin`, writes a `rebuild.json` (the
|
|
371
|
+
exact `build({inesHeader})` call, with absolute paths) and a `BUILD.md`. Feed
|
|
372
|
+
`rebuild.json` straight back to `build` and you get a byte-identical ROM. This
|
|
373
|
+
is the RE workhorse loop: `disasm({target:'project'})` → edit the `.asm` →
|
|
374
|
+
rebuild → `diffRoms` to confirm your patch landed.
|
|
375
|
+
|
|
315
376
|
## When to drop to asm
|
|
316
377
|
|
|
317
378
|
Game-loop in C is fine for ~80% of homebrew. Drop to asm when:
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# NES — symptom → fix
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
Find your symptom below; each entry has the 1-line diagnosis + the
|
|
4
10
|
MCP tool call that confirms it. Run these BEFORE you start bisecting
|
|
5
11
|
your C source.
|
|
@@ -275,6 +275,9 @@ export function nesImageToTilemap(args) {
|
|
|
275
275
|
// unique 8×8 patterns exist naturally. Returning the unmerged result
|
|
276
276
|
// gives the caller a chance to retry; just be aware nametable indices
|
|
277
277
|
// > 255 will wrap on hardware.
|
|
278
|
+
// Permanently disabled (see note above) but kept as documentation of the
|
|
279
|
+
// rejected greedy-merge approach.
|
|
280
|
+
// eslint-disable-next-line no-constant-condition, no-constant-binary-expression
|
|
278
281
|
if (false && dedup && tileList.length > maxTiles) {
|
|
279
282
|
const tileDist = (a, b) => {
|
|
280
283
|
let d = 0;
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
;settings, uncomment or put them into your main program; the latter makes possible updates easier
|
|
10
10
|
|
|
11
|
-
FT_BASE_ADR = $
|
|
11
|
+
FT_BASE_ADR = $0700 ;page in the RAM used for FT2 variables, should be $xx00
|
|
12
|
+
;romdev: pinned to $0700 (the SNDRAM page reserved in
|
|
13
|
+
;chr-ram-runtime.cfg). $0300 — the cc65 default — overlaps
|
|
14
|
+
;the C BSS/DATA region, so FT2's per-frame writes would
|
|
15
|
+
;clobber _ppuctrl_value / NMI state and stall rendering.
|
|
12
16
|
FT_TEMP = $fd ;3 bytes in zeropage used by the library as a scratchpad
|
|
13
17
|
FT_DPCM_OFF = $fc00 ;$c000..$ffc0, 64-byte steps
|
|
14
18
|
FT_SFX_STREAMS = 1 ;number of sound effects played at once, 1..4
|
|
@@ -12,10 +12,15 @@ romdev ships a **hardware helper library** (`src/platforms/pce/lib/c/`:
|
|
|
12
12
|
`psg_tone()` instead of poking VDC/VCE registers by hand. cc65 has **no** sprite
|
|
13
13
|
library, so this lib is how you get pixels on screen.
|
|
14
14
|
|
|
15
|
-
The fastest way to a working game: **`scaffold({op:'
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
The fastest way to a working game: **`scaffold({op:'game', platform: "pce", genre:
|
|
16
|
+
"shmup"})`** — or any of `platformer` / `puzzle` / `sports` / `racing`, the full
|
|
17
|
+
genre set. For a smaller starting point use **`scaffold({op:'project', platform:
|
|
18
|
+
"pce", template: "sprite_move"})`** (also `music_sfx`, `catch_game`). Either drops
|
|
19
|
+
a complete, *building* project — a verified playable example + the helper lib +
|
|
20
|
+
docs. Read the example's `main.c`, then change it. The examples live in
|
|
21
|
+
`examples/pce/`. The genre scaffolds fill the BAT (32×32 virtual screen); the
|
|
22
|
+
`platformer` smooth-scrolls the background via the VDC BXR (R7) register.
|
|
23
|
+
**Gotcha:** `#include <stdint.h>` for int8/16/32_t — `pce.h` only typedefs u8/u16.
|
|
19
24
|
|
|
20
25
|
## CPU — HuC6280 (a 65C02 superset)
|
|
21
26
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# PC Engine — troubleshooting (symptom → cause → fix)
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
Read this when something's broken. For the "how it works" overview, read
|
|
4
10
|
MENTAL_MODEL.md first.
|
|
5
11
|
|
|
@@ -83,7 +83,7 @@ static u8 _pce_vdc_inited = 0;
|
|
|
83
83
|
void vdc_init(void) {
|
|
84
84
|
if (_pce_vdc_inited) return;
|
|
85
85
|
_pce_vdc_inited = 1;
|
|
86
|
-
vdc_set_reg(VDC_MWR,
|
|
86
|
+
vdc_set_reg(VDC_MWR, 0x0000); /* 32x32 virtual map (SCREEN field=000); 256px BAT. (0x10 was 64x32 — its 64-wide stride left the bottom rows as uninitialized VRAM = vertical-stripe garbage.) */
|
|
87
87
|
vdc_set_reg(VDC_BXR, 0x0000); /* BG X scroll = 0 */
|
|
88
88
|
vdc_set_reg(VDC_BYR, 0x0000); /* BG Y scroll = 0 */
|
|
89
89
|
vdc_set_reg(VDC_HSR, 0x0202); /* horizontal sync width/start */
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Sega Master System / Game Gear — troubleshooting
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
When something's broken. Read MENTAL_MODEL.md first for the
|
|
4
10
|
"what's going on" version (via `platform({op:'doc', platform:"sms", name:"mental_model"})`).
|
|
5
11
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Super Nintendo / Super Famicom — troubleshooting
|
|
2
2
|
|
|
3
|
+
> **A build failed? Read `issues[]` FIRST.** Every build/compile call returns
|
|
4
|
+
> `issues: [{file, line, col, severity, message, stage}]` — the structured error
|
|
5
|
+
> list. It almost always names the exact line to fix. Read that before matching a
|
|
6
|
+
> symptom below or touching your source. Fall back to the raw `log` only if
|
|
7
|
+
> `issues[]` is empty but `ok:false`.
|
|
8
|
+
|
|
3
9
|
When something's broken. Read MENTAL_MODEL.md first for the "what's
|
|
4
10
|
going on" version (via `platform({op:'doc', platform:"snes", name:"mental_model"})`).
|
|
5
11
|
|
|
@@ -23,8 +23,6 @@
|
|
|
23
23
|
// lowest error and use that. The first block of any sample MUST use
|
|
24
24
|
// filter 0 (no other choice has valid p1/p2 history).
|
|
25
25
|
|
|
26
|
-
const BRR_BUF_DECODE = 16; // samples per block
|
|
27
|
-
|
|
28
26
|
/** snes9x CLAMP16: saturate to int16. */
|
|
29
27
|
function clamp16(io) {
|
|
30
28
|
// The macro: if int16(io) != io, io = (io >> 31) ^ 0x7FFF
|
package/src/playtest/playtest.js
CHANGED
|
@@ -17,9 +17,6 @@ import { createRequire } from "node:module";
|
|
|
17
17
|
const execFileAsync = promisify(execFile);
|
|
18
18
|
const require = createRequire(import.meta.url);
|
|
19
19
|
|
|
20
|
-
// One-pixel solid-black RGBA buffer; we stretch it across the letterbox
|
|
21
|
-
// bars each frame so they don't smear with leftover pixels.
|
|
22
|
-
const BLACK_PIXEL = Buffer.from([0, 0, 0, 0xFF]);
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
22
|
* Choose a default window title from the loaded host. Prefers the loaded
|
|
@@ -796,10 +793,6 @@ export async function playtest(args) {
|
|
|
796
793
|
};
|
|
797
794
|
}
|
|
798
795
|
|
|
799
|
-
function sleep(ms) {
|
|
800
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
801
|
-
}
|
|
802
|
-
|
|
803
796
|
function bitToName(bit) {
|
|
804
797
|
return ({
|
|
805
798
|
0: "b", 1: "y", 2: "select", 3: "start",
|
package/src/rom-id/identifier.js
CHANGED
|
@@ -107,6 +107,7 @@ function parseINes(b) {
|
|
|
107
107
|
if (f6 & 0x04) notes.push("trainer present");
|
|
108
108
|
if (f6 & 0x08) notes.push("four-screen VRAM");
|
|
109
109
|
|
|
110
|
+
const hasBattery = !!(f6 & 0x02);
|
|
110
111
|
return {
|
|
111
112
|
platform: "nes",
|
|
112
113
|
format: ".nes",
|
|
@@ -117,6 +118,10 @@ function parseINes(b) {
|
|
|
117
118
|
chr: chrBanks * 8192,
|
|
118
119
|
total: b.length,
|
|
119
120
|
},
|
|
121
|
+
// Battery-backed cartridge SAVE RAM: present iff the iNES battery flag is set
|
|
122
|
+
// (8 KB is the standard PRG-RAM window). Read/write live via
|
|
123
|
+
// memory({region:'save_ram'}); persist with state({op:'exportSram'/'importSram'}).
|
|
124
|
+
saveRam: { hasBattery, bytes: hasBattery ? 8192 : 0 },
|
|
120
125
|
notes: [...notes, `mirroring: ${mirroring}`],
|
|
121
126
|
confidence: 1,
|
|
122
127
|
};
|
|
@@ -208,6 +213,13 @@ function parseGameBoy(b) {
|
|
|
208
213
|
0x1E: "MBC5+RUMBLE+RAM+BATTERY", 0xFC: "Pocket Camera", 0xFE: "HuC3", 0xFF: "HuC1+RAM+BATTERY",
|
|
209
214
|
};
|
|
210
215
|
|
|
216
|
+
// Battery save = cart type whose name carries BATTERY. MBC2 has 512×4 bits of
|
|
217
|
+
// internal RAM (ramSizeCode is 0 but it still saves), so treat it specially.
|
|
218
|
+
const typeName = cartTypeNames[cartType] ?? "";
|
|
219
|
+
const hasBattery = /BATTERY/.test(typeName);
|
|
220
|
+
const isMbc2 = cartType === 0x05 || cartType === 0x06;
|
|
221
|
+
const saveBytes = hasBattery ? (isMbc2 ? 512 : Math.max(ramSize, 0)) : 0;
|
|
222
|
+
|
|
211
223
|
return {
|
|
212
224
|
platform,
|
|
213
225
|
format: isGbc ? ".gbc" : ".gb",
|
|
@@ -215,6 +227,9 @@ function parseGameBoy(b) {
|
|
|
215
227
|
region,
|
|
216
228
|
mapper: cartTypeNames[cartType] ?? `0x${cartType.toString(16)}`,
|
|
217
229
|
sizes: { rom: romSize, ram: ramSize, total: b.length },
|
|
230
|
+
// Battery-backed cartridge SAVE RAM (the .sav). Read/write live via
|
|
231
|
+
// memory({region:'save_ram'}); persist with state({op:'exportSram'/'importSram'}).
|
|
232
|
+
saveRam: { hasBattery, bytes: saveBytes },
|
|
218
233
|
notes: [
|
|
219
234
|
isGbc ? "Game Boy Color cartridge" : "Original Game Boy cartridge",
|
|
220
235
|
sgbFlag === 0x03 && "supports Super Game Boy enhancements",
|
|
@@ -260,15 +260,6 @@ function lorom(fileStart, fileEnd) {
|
|
|
260
260
|
return `${fmt(bankStart, offStart)}..${fmt(bankEnd, offEnd)} (spans banks)`;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
function ensureDir(FS, dir) {
|
|
264
|
-
const parts = dir.split("/").filter(Boolean);
|
|
265
|
-
let cur = "";
|
|
266
|
-
for (const p of parts) {
|
|
267
|
-
cur += "/" + p;
|
|
268
|
-
try { FS.mkdir(cur); } catch {}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
263
|
// Static analyzer for known asar landmines. Runs before the WASM call so
|
|
273
264
|
// we can return a helpful error instead of letting asar abort silently.
|
|
274
265
|
// Returns null when source looks clean, or a string with the diagnostic.
|
|
@@ -25,11 +25,30 @@ import { runCa65, runLd65 } from "./cc65/cc65.js";
|
|
|
25
25
|
import { runAsar } from "./asar/asar.js";
|
|
26
26
|
import { runVasm68k } from "./vasm68k/vasm68k.js";
|
|
27
27
|
import { runSdasz80, runSdld, ihxToBin } from "./sdcc/sdcc.js";
|
|
28
|
-
import { runRgbasm, runRgblink
|
|
28
|
+
import { runRgbasm, runRgblink } from "./rgbds/rgbds.js";
|
|
29
|
+
import { parseBuildLog } from "./parse-errors.js";
|
|
29
30
|
|
|
30
31
|
const __filename = fileURLToPath(import.meta.url);
|
|
31
32
|
const __dirname = path.dirname(__filename);
|
|
32
33
|
|
|
34
|
+
// Build a transparent snippet-assembly error: lead with the FIRST structured
|
|
35
|
+
// diagnostic (file:line: message) parsed out of the raw log — the same
|
|
36
|
+
// issues[]-style surfacing build() does — instead of dumping the unparsed
|
|
37
|
+
// assembler stdout and making the agent grep it. Full log still appended for
|
|
38
|
+
// fallback. `where` is e.g. "ca65" / "ld65 link" / "asar".
|
|
39
|
+
function asmError(where, log) {
|
|
40
|
+
const issues = parseBuildLog(log ?? "");
|
|
41
|
+
const first = issues.find((i) => i.severity === "error") ?? issues[0];
|
|
42
|
+
const headline = first
|
|
43
|
+
? `${first.file ? first.file + ":" : ""}${first.line ? first.line + ": " : ""}${first.message}`
|
|
44
|
+
: "no structured diagnostic found";
|
|
45
|
+
return new Error(
|
|
46
|
+
`assembleSnippet[${where}] failed: ${headline}` +
|
|
47
|
+
(issues.length > 1 ? ` (+${issues.length - 1} more issue(s))` : "") +
|
|
48
|
+
`\nFix the source line above. Full assembler log:\n${log ?? ""}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
33
52
|
/**
|
|
34
53
|
* CPU dialect → assembler dispatch. Keys are also the public API.
|
|
35
54
|
*/
|
|
@@ -102,7 +121,7 @@ async function assembleCa65({ origin, code }, cpu = "6502") {
|
|
|
102
121
|
|
|
103
122
|
const asm = await runCa65({ source });
|
|
104
123
|
if (!asm.object) {
|
|
105
|
-
throw
|
|
124
|
+
throw asmError("ca65", asm.log);
|
|
106
125
|
}
|
|
107
126
|
|
|
108
127
|
// Minimal linker config: one MEMORY block at `origin`, one SEGMENT
|
|
@@ -116,7 +135,7 @@ SEGMENTS { CODE: load = OUT, type = ro; }
|
|
|
116
135
|
linkerConfig: cfg,
|
|
117
136
|
});
|
|
118
137
|
if (!linked.binary) {
|
|
119
|
-
throw
|
|
138
|
+
throw asmError("ld65 link", linked.log);
|
|
120
139
|
}
|
|
121
140
|
return { bytes: linked.binary, log: (asm.log ?? "") + (linked.log ?? "") };
|
|
122
141
|
}
|
|
@@ -137,7 +156,7 @@ async function assembleAsar({ origin, code }) {
|
|
|
137
156
|
const source = `org ${hex24}\n${code}\n`;
|
|
138
157
|
const r = await runAsar({ source, baseRom, symbols: false });
|
|
139
158
|
if (!r.binary) {
|
|
140
|
-
throw
|
|
159
|
+
throw asmError("asar", r.log);
|
|
141
160
|
}
|
|
142
161
|
const bin = r.binary;
|
|
143
162
|
// Find first non-sentinel byte (asar may have written anywhere depending
|
|
@@ -148,7 +167,7 @@ async function assembleAsar({ origin, code }) {
|
|
|
148
167
|
if (bin[i] !== SENTINEL) { start = i; break; }
|
|
149
168
|
}
|
|
150
169
|
if (start < 0) {
|
|
151
|
-
throw new Error(`assembleSnippet[asar]: no bytes written (origin 0x${origin.toString(16)})
|
|
170
|
+
throw new Error(`assembleSnippet[asar]: no bytes written (origin 0x${origin.toString(16)}) — the source assembled but emitted nothing at this origin. Check the org address and that the code actually emits bytes.\nFull log:\n${r.log ?? ""}`);
|
|
152
171
|
}
|
|
153
172
|
let end = start + 1;
|
|
154
173
|
let sentinelRun = 0;
|
|
@@ -181,7 +200,7 @@ async function assembleVasm68k({ origin, code }) {
|
|
|
181
200
|
const source = `\torg $${origin.toString(16).toUpperCase()}\n${indented}\n`;
|
|
182
201
|
const r = await runVasm68k({ source, options: ["-Fbin"] });
|
|
183
202
|
if (!r.binary || (r.exitCode != null && r.exitCode !== 0)) {
|
|
184
|
-
throw
|
|
203
|
+
throw asmError("vasm68k", r.log);
|
|
185
204
|
}
|
|
186
205
|
return { bytes: r.binary, log: r.log ?? "" };
|
|
187
206
|
}
|
|
@@ -199,7 +218,7 @@ async function assembleSdcc({ origin, code }) {
|
|
|
199
218
|
const source = `\t.module snippet\n\t.area _CODE\n${indented}\n`;
|
|
200
219
|
const asm = await runSdasz80({ source });
|
|
201
220
|
if (!asm.rel) {
|
|
202
|
-
throw
|
|
221
|
+
throw asmError("sdasz80", asm.log);
|
|
203
222
|
}
|
|
204
223
|
// Minimal empty crt0 rel: just declares _HEADER0 (zero bytes) and is
|
|
205
224
|
// enough for sdld to satisfy its hardcoded crt0.rel dependency.
|
|
@@ -212,7 +231,7 @@ async function assembleSdcc({ origin, code }) {
|
|
|
212
231
|
libraries: [],
|
|
213
232
|
});
|
|
214
233
|
if (!linked.ihx) {
|
|
215
|
-
throw
|
|
234
|
+
throw asmError("sdld link", linked.log);
|
|
216
235
|
}
|
|
217
236
|
const padded = ihxToBin(linked.ihx, 0x10000, 0xFF);
|
|
218
237
|
// Find first and last non-FF byte from origin onwards. The snippet bytes
|
|
@@ -232,19 +251,18 @@ async function assembleSdcc({ origin, code }) {
|
|
|
232
251
|
async function assembleRgbds({ origin, code }) {
|
|
233
252
|
// rgbasm needs SECTION declarations to place code at an address.
|
|
234
253
|
const sectionAt = `$${origin.toString(16).toUpperCase()}`;
|
|
235
|
-
|
|
236
|
-
// Actually GB has no BANK[0] for fixed ROM — use ROM0 if origin < 0x4000.
|
|
254
|
+
// GB has no BANK[0] for fixed ROM — use ROM0 if origin < 0x4000, else ROMX BANK[1].
|
|
237
255
|
const useRom0 = origin < 0x4000;
|
|
238
256
|
const realSource = useRom0
|
|
239
257
|
? `SECTION "snippet", ROM0[${sectionAt}]\n${code}\n`
|
|
240
258
|
: `SECTION "snippet", ROMX[${sectionAt}], BANK[1]\n${code}\n`;
|
|
241
259
|
const asm = await runRgbasm({ source: realSource });
|
|
242
260
|
if (!asm.object) {
|
|
243
|
-
throw
|
|
261
|
+
throw asmError("rgbasm", asm.log);
|
|
244
262
|
}
|
|
245
263
|
const linked = await runRgblink({ objects: { "snippet.o": asm.object }, padValue: 0x00 });
|
|
246
264
|
if (!linked.binary) {
|
|
247
|
-
throw
|
|
265
|
+
throw asmError("rgblink link", linked.log);
|
|
248
266
|
}
|
|
249
267
|
// Slice from origin, find last non-zero byte.
|
|
250
268
|
const start = useRom0 ? origin : (origin - 0x4000 + 0x4000); // bank 1 lives at file 0x4000
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// iNES header + NROM linker-config synthesis for cc65/ca65 NES builds.
|
|
2
|
+
//
|
|
3
|
+
// The most common NES *reverse-engineering* build shape — rebuilding a
|
|
4
|
+
// commercial NROM game from its disassembly (e.g. SMBDIS) into a byte-identical
|
|
5
|
+
// `.nes` — needs three pieces of pure boilerplate that are identical for every
|
|
6
|
+
// NROM CHR-ROM cart:
|
|
7
|
+
// 1. the 16-byte iNES header (`.segment "HEADER"`),
|
|
8
|
+
// 2. a CHARS segment fed from the extracted CHR-ROM blob (`.incbin`),
|
|
9
|
+
// 3. a 3-region MEMORY/SEGMENTS .cfg concatenating HEADER + PRG + CHARS into
|
|
10
|
+
// one output file.
|
|
11
|
+
//
|
|
12
|
+
// `build({inesHeader:{prgBanks, chrBanks, mapper, mirroring}})` and
|
|
13
|
+
// `disasm({target:'project'})` both use this so the agent never hand-derives
|
|
14
|
+
// header bytes or writes glue `.s`/`.cfg` files. Proven byte-identical against
|
|
15
|
+
// nestest.nes (NROM-128, 16K PRG + 8K CHR) and 32K-PRG NROM-256 carts.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} InesHeaderSpec
|
|
19
|
+
* @property {number} prgBanks - 16KB PRG-ROM banks (1 = NROM-128, 2 = NROM-256).
|
|
20
|
+
* @property {number} [chrBanks] - 8KB CHR-ROM banks (0 = CHR-RAM, no CHARS). Default 0.
|
|
21
|
+
* @property {number} [mapper] - iNES mapper number. Default 0 (NROM).
|
|
22
|
+
* @property {"horizontal"|"vertical"} [mirroring] - nametable mirroring. Default "horizontal".
|
|
23
|
+
* @property {boolean} [battery] - PRG-RAM battery (flags6 bit 1). Default false.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the 16 raw iNES header bytes for a spec.
|
|
28
|
+
* @param {InesHeaderSpec} spec
|
|
29
|
+
* @returns {Uint8Array} exactly 16 bytes
|
|
30
|
+
*/
|
|
31
|
+
export function inesHeaderBytes(spec) {
|
|
32
|
+
const prg = spec.prgBanks;
|
|
33
|
+
const chr = spec.chrBanks ?? 0;
|
|
34
|
+
const mapper = spec.mapper ?? 0;
|
|
35
|
+
const mirroring = spec.mirroring ?? "horizontal";
|
|
36
|
+
if (!Number.isInteger(prg) || prg < 1 || prg > 255) {
|
|
37
|
+
throw new Error(`inesHeader.prgBanks must be an integer 1..255 (16KB each); got ${spec.prgBanks}`);
|
|
38
|
+
}
|
|
39
|
+
if (!Number.isInteger(chr) || chr < 0 || chr > 255) {
|
|
40
|
+
throw new Error(`inesHeader.chrBanks must be an integer 0..255 (8KB each; 0 = CHR-RAM); got ${spec.chrBanks}`);
|
|
41
|
+
}
|
|
42
|
+
if (!Number.isInteger(mapper) || mapper < 0 || mapper > 255) {
|
|
43
|
+
throw new Error(`inesHeader.mapper must be an integer 0..255; got ${spec.mapper}`);
|
|
44
|
+
}
|
|
45
|
+
if (mirroring !== "horizontal" && mirroring !== "vertical") {
|
|
46
|
+
throw new Error(`inesHeader.mirroring must be "horizontal" or "vertical"; got ${JSON.stringify(spec.mirroring)}`);
|
|
47
|
+
}
|
|
48
|
+
const flags6 = (mirroring === "vertical" ? 0x01 : 0x00) | (spec.battery ? 0x02 : 0x00) | ((mapper & 0x0f) << 4);
|
|
49
|
+
const flags7 = mapper & 0xf0;
|
|
50
|
+
const h = new Uint8Array(16);
|
|
51
|
+
h[0] = 0x4e; h[1] = 0x45; h[2] = 0x53; h[3] = 0x1a; // "NES\x1a"
|
|
52
|
+
h[4] = prg;
|
|
53
|
+
h[5] = chr;
|
|
54
|
+
h[6] = flags6;
|
|
55
|
+
h[7] = flags7;
|
|
56
|
+
// bytes 8..15 stay 0 (iNES 1.0 padding)
|
|
57
|
+
return h;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* ca65 source that emits the iNES header as `.segment "HEADER"`.
|
|
62
|
+
* @param {InesHeaderSpec} spec
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function inesHeaderSource(spec) {
|
|
66
|
+
const h = inesHeaderBytes(spec);
|
|
67
|
+
const prg = spec.prgBanks;
|
|
68
|
+
const chr = spec.chrBanks ?? 0;
|
|
69
|
+
const mapper = spec.mapper ?? 0;
|
|
70
|
+
const mirroring = spec.mirroring ?? "horizontal";
|
|
71
|
+
const hex = (b) => "$" + b.toString(16).padStart(2, "0").toUpperCase();
|
|
72
|
+
return [
|
|
73
|
+
"; iNES header — auto-generated by build({inesHeader:{...}}).",
|
|
74
|
+
`; prgBanks=${prg} (${prg * 16}KB) chrBanks=${chr} (${chr * 8}KB${chr === 0 ? " = CHR-RAM" : ""})` +
|
|
75
|
+
` mapper=${mapper} mirroring=${mirroring}`,
|
|
76
|
+
'.segment "HEADER"',
|
|
77
|
+
` .byte ${hex(h[0])},${hex(h[1])},${hex(h[2])},${hex(h[3])} ; "NES"+EOF`,
|
|
78
|
+
` .byte ${h[4]} ; PRG-ROM 16KB banks`,
|
|
79
|
+
` .byte ${h[5]} ; CHR-ROM 8KB banks${chr === 0 ? " (0 = CHR-RAM)" : ""}`,
|
|
80
|
+
` .byte ${hex(h[6])} ; flags6 (mapper lo nibble + mirroring${spec.battery ? " + battery" : ""})`,
|
|
81
|
+
` .byte ${hex(h[7])} ; flags7 (mapper hi nibble)`,
|
|
82
|
+
" .byte 0,0,0,0,0,0,0,0 ; padding (iNES 1.0)",
|
|
83
|
+
"",
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* ca65 source that pulls the CHR-ROM blob into the CHARS segment.
|
|
89
|
+
* @param {string} incbinName - the binaryInclude file name to `.incbin`.
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
export function charsSource(incbinName) {
|
|
93
|
+
return [
|
|
94
|
+
"; CHR-ROM data — auto-generated by build({inesHeader:{...}}).",
|
|
95
|
+
'.segment "CHARS"',
|
|
96
|
+
` .incbin "${incbinName}"`,
|
|
97
|
+
"",
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A flat NROM linker .cfg: HEADER(16B) + PRG + (optional) CHARS, all concatenated
|
|
103
|
+
* into one %O output file. "Flat" = one CODE segment carrying the whole PRG image
|
|
104
|
+
* with its OWN embedded reset/NMI/IRQ vectors (the shape a disassembly produces),
|
|
105
|
+
* NOT cc65's STARTUP/VECTORS/CONDES split. Use `chr-rom` (the named preset) for
|
|
106
|
+
* cc65-C-with-segments builds instead.
|
|
107
|
+
*
|
|
108
|
+
* - prgBanks=1 (NROM-128): the 16KB image maps at $C000 (mirrored to $8000).
|
|
109
|
+
* - prgBanks=2 (NROM-256): the 32KB image maps at $8000.
|
|
110
|
+
*
|
|
111
|
+
* @param {InesHeaderSpec} spec
|
|
112
|
+
* @returns {string} ld65 config text
|
|
113
|
+
*/
|
|
114
|
+
export function nromFlatCfg(spec) {
|
|
115
|
+
const prg = spec.prgBanks;
|
|
116
|
+
const chr = spec.chrBanks ?? 0;
|
|
117
|
+
const prgSize = prg * 0x4000;
|
|
118
|
+
// NROM-128's single 16KB bank lives in the upper half so $FFFA vectors land;
|
|
119
|
+
// anything larger fills from $8000 down.
|
|
120
|
+
const prgStart = prg === 1 ? 0xc000 : 0x10000 - prgSize;
|
|
121
|
+
const lines = [
|
|
122
|
+
"# Flat NROM linker config — auto-generated by build({inesHeader:{...}}).",
|
|
123
|
+
"# HEADER(16B) + PRG + " + (chr > 0 ? "CHARS(CHR-ROM)" : "(no CHARS — CHR-RAM)") +
|
|
124
|
+
", concatenated into one .nes.",
|
|
125
|
+
"# 'Flat' CODE segment = the whole PRG image with its own embedded vectors",
|
|
126
|
+
"# (disassembly shape). For cc65-C with segment split, use linkerConfig:'chr-rom'.",
|
|
127
|
+
"MEMORY {",
|
|
128
|
+
" HEADER: file = %O, start = $0000, size = $0010, fill = yes;",
|
|
129
|
+
` PRG: file = %O, start = $${prgStart.toString(16).toUpperCase()}, size = $${prgSize.toString(16).toUpperCase()}, fill = yes, fillval = $FF;`,
|
|
130
|
+
];
|
|
131
|
+
if (chr > 0) {
|
|
132
|
+
lines.push(` CHARS: file = %O, start = $0000, size = $${(chr * 0x2000).toString(16).toUpperCase()}, fill = yes;`);
|
|
133
|
+
}
|
|
134
|
+
lines.push(
|
|
135
|
+
"}",
|
|
136
|
+
"SEGMENTS {",
|
|
137
|
+
" HEADER: load = HEADER, type = ro;",
|
|
138
|
+
" CODE: load = PRG, type = ro;",
|
|
139
|
+
);
|
|
140
|
+
if (chr > 0) {
|
|
141
|
+
lines.push(" CHARS: load = CHARS, type = ro;");
|
|
142
|
+
}
|
|
143
|
+
lines.push("}", "");
|
|
144
|
+
return lines.join("\n");
|
|
145
|
+
}
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
# 3. Write CHR data from C at runtime: PPUADDR = 0x00; PPUDATA = byte; etc.
|
|
20
20
|
|
|
21
21
|
SYMBOLS {
|
|
22
|
-
|
|
22
|
+
# Stack is $0200 (512 B) so the top RAM page ($0700-$07FF) can be
|
|
23
|
+
# reserved below for a music driver's scratch RAM (FamiTone2 et al.),
|
|
24
|
+
# which needs a dedicated, page-aligned block that the C BSS/DATA
|
|
25
|
+
# region must NOT overlap. Tiny NROM scaffolds use far less than 512 B
|
|
26
|
+
# of stack, so this is safe; scaffolds with no music driver simply
|
|
27
|
+
# leave the reserved page unused.
|
|
28
|
+
__STACKSIZE__: type = weak, value = $0200;
|
|
23
29
|
}
|
|
24
30
|
MEMORY {
|
|
25
31
|
ZP: file = "", start = $0002, size = $001A, type = rw, define = yes;
|
|
@@ -37,6 +43,13 @@ MEMORY {
|
|
|
37
43
|
|
|
38
44
|
SRAM: file = "", start = $0500, size = __STACKSIZE__, define = yes;
|
|
39
45
|
|
|
46
|
+
# Reserved page for a sound-driver's RAM scratch ($0700-$07FF). The
|
|
47
|
+
# bundled FamiTone2 engine (music_demo scaffold) pins FT_BASE_ADR here
|
|
48
|
+
# so its ~90 bytes of channel/envelope state can't collide with the C
|
|
49
|
+
# BSS/DATA at $0300-$04FF — that collision silently clobbers the NMI's
|
|
50
|
+
# cached PPUCTRL and stalls rendering. Unused by non-music scaffolds.
|
|
51
|
+
SNDRAM: file = "", start = $0700, size = $0100, define = yes;
|
|
52
|
+
|
|
40
53
|
# BSS / DATA live in real RAM ($0300-$04FF, 512 bytes). NROM (mapper 0)
|
|
41
54
|
# with no battery has $6000-$7FFF UNMAPPED — reads return open bus.
|
|
42
55
|
# Putting BSS there means `int counter = 0;` reads garbage (intermittent
|