romdevtools 0.23.0 → 0.24.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 +139 -494
- package/CHANGELOG.md +41 -3
- package/package.json +2 -2
- 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 +170 -1
- package/src/http/skill-doc.js +1 -1
- package/src/mcp/tools/input-layout.js +10 -0
- package/src/mcp/tools/input.js +26 -2
- package/src/mcp/tools/playtest.js +17 -2
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +155 -6
- package/src/platforms/atari2600/MENTAL_MODEL.md +37 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +36 -0
- package/src/platforms/c64/MENTAL_MODEL.md +83 -6
- package/src/platforms/gb/MENTAL_MODEL.md +56 -0
- package/src/platforms/gba/MENTAL_MODEL.md +57 -3
- package/src/platforms/gba/lib/arm-archives/libc.a +0 -0
- package/src/platforms/gba/lib/arm-archives/libgcc.a +0 -0
- package/src/platforms/gba/lib/arm-archives/libnosys.a +0 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +21 -0
- package/src/platforms/genesis/MENTAL_MODEL.md +19 -0
- package/src/platforms/genesis/lib/c/libc.a +0 -0
- package/src/platforms/genesis/lib/c/libgcc.a +0 -0
- package/src/platforms/genesis/lib/c/libm.a +0 -0
- package/src/platforms/gg/MENTAL_MODEL.md +24 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +33 -7
- package/src/platforms/msx/MENTAL_MODEL.md +27 -0
- package/src/platforms/nes/MENTAL_MODEL.md +35 -0
- package/src/platforms/sms/MENTAL_MODEL.md +51 -0
- package/src/platforms/snes/MENTAL_MODEL.md +21 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +43 -0
- package/src/playtest/playtest.js +48 -0
- package/examples/msx/catch_game/_verify.mjs +0 -93
- package/examples/pce/catch_game/_verify.mjs +0 -75
|
@@ -41,6 +41,12 @@ Default after boot = `$37` (all three set) — BASIC + KERNAL ROMs +
|
|
|
41
41
|
I/O regs. Set CHAREN=0 to read the character-set bitmaps from ROM;
|
|
42
42
|
set HIRAM=0 to swap KERNAL out for RAM (useful for low-memory tricks).
|
|
43
43
|
|
|
44
|
+
> **cc65 zero-page starts at $02 (same cc65 trap as NES/Atari/Lynx).** cc65
|
|
45
|
+
> reserves `$00-$01` (here it's also the 6510 I/O port), so your first
|
|
46
|
+
> `.res 1` in `ZEROPAGE` lands at **$02**, not $00. Don't hand-write asm that
|
|
47
|
+
> assumes a ZP var is at $00. Confirm with `symbols({op:'map'})` after
|
|
48
|
+
> `build({output:'romWithDebug'})`.
|
|
49
|
+
|
|
44
50
|
## VIC-II — character cells, NOT tiles
|
|
45
51
|
|
|
46
52
|
The C64's video chip displays 25 rows × 40 cols of 8×8 character
|
|
@@ -107,12 +113,35 @@ which is what the KERNAL's IRQ uses to update key state every
|
|
|
107
113
|
|
|
108
114
|
### Driving input over MCP
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
**Joystick.** One fire button. Press it with `input({op:'set', b: true})` (or
|
|
117
|
+
spatial `south`) — both clear `$DC00` bit 4 (verified live). `a` is a **no-op**
|
|
118
|
+
(no second button). Drive fire with `b`/`south` + the d-pad. The joystick reads
|
|
119
|
+
**port 2** by default; switch with `input({op:'joyport', joyport:1})` /
|
|
120
|
+
`input({op:'joyport'})` to read it.
|
|
121
|
+
|
|
122
|
+
**Keyboard (the C64-specific part — many games NEED it).** Unlike consoles, most
|
|
123
|
+
C64 games (and cracktros) gate gameplay behind a KEYBOARD setup screen — **F1**
|
|
124
|
+
to pick 1 player, RUN/STOP, SPACE/RETURN — that the joystick can't reach. So if a
|
|
125
|
+
joystick gets you to a title/intro but not into play, you almost certainly need a
|
|
126
|
+
key:
|
|
127
|
+
- `input({op:'pressKey', key:'f1'})` — press one C64 key (held + auto-released).
|
|
128
|
+
Keys: `f1/f3/f5/f7`, `return`, `space`, `run/stop`, `a-z`, `0-9`, `ctrl`, `cbm`,
|
|
129
|
+
`home`, `down`, `right`, `lshift`, `rshift`.
|
|
130
|
+
- `input({op:'typeText', text:'LOAD"*",8,1\rRUN\r'})` — type a string (`\r` =
|
|
131
|
+
RETURN). For BASIC commands / filenames.
|
|
132
|
+
- `input({op:'layout', platform:'c64'})` lists the keyboard keys + the joyport.
|
|
133
|
+
|
|
134
|
+
A typical C64 RE startup: load → step to the title → `pressKey f1` (1 player) →
|
|
135
|
+
`set {b:true}` (fire to start) → step → you're in gameplay → `state({op:'save'})`.
|
|
136
|
+
|
|
137
|
+
**Controller-alone (playtest):** a human in `playtest` needs no keyboard — the
|
|
138
|
+
pad's spare buttons map to the C64 keys (X=Space, L2=Run/Stop, R2=Return,
|
|
139
|
+
right-stick=F1/F3/F5/F7, top face=F1; d-pad+Fire=joystick). The same mapping
|
|
140
|
+
applies to the agent's `setInput`, so e.g. `input({op:'set', c64_f1:true})` also
|
|
141
|
+
presses F1. `playtest({op:'open'})` returns `c64Controls` to relay to the user.
|
|
142
|
+
|
|
143
|
+
⚠ A cc65 `.prg` only starts after BASIC auto-`RUN`s it — step ~70+ frames past
|
|
144
|
+
load before input/reads register.
|
|
116
145
|
|
|
117
146
|
## SID — three voices of fame
|
|
118
147
|
|
|
@@ -131,6 +160,54 @@ the `sid_play.s` starter snippet.
|
|
|
131
160
|
waveform, freq→note, pulse-width, ADSR — plus the filter cutoff/resonance/mode.
|
|
132
161
|
Handy for verifying a `sid_play` routine is actually gating notes.
|
|
133
162
|
|
|
163
|
+
## MCP debug & inspection tooling
|
|
164
|
+
|
|
165
|
+
romdev runs the C64 on a patched VICE (`vice_x64`) core that exposes deep live
|
|
166
|
+
state. The inspectors all read the **running** machine — use them to confirm what
|
|
167
|
+
your code actually did to the hardware, not what you think it did.
|
|
168
|
+
|
|
169
|
+
**Live visual / state inspectors:**
|
|
170
|
+
|
|
171
|
+
- `palette({source:'live'})` — the 16-color hardware-fixed C64 palette as a PNG,
|
|
172
|
+
plus the current border / background / extra-background color indices decoded
|
|
173
|
+
straight from the VIC-II registers.
|
|
174
|
+
- `sprites({op:'inspect'})` — all 8 MOBs decoded into the generic sprite shape:
|
|
175
|
+
X/Y, color, multicolor flag, expand-X / expand-Y, priority, AND the screen-RAM
|
|
176
|
+
sprite-data pointers at `$07F8` so you can locate each sprite's pixel block in
|
|
177
|
+
the VIC bank.
|
|
178
|
+
- `cpu({op:'read'})` — the 6510 registers (A/X/Y/P/SP/PC) read from a live,
|
|
179
|
+
`#define`-aliased register file, plus the `$0001` I/O port decoded into its
|
|
180
|
+
LORAM / HIRAM / CHAREN bits (so you can see which ROMs are banked in).
|
|
181
|
+
- `background({view:'renderState'})` — the VIC-II registers decoded into
|
|
182
|
+
mode / scroll / colors / sprites, the VIC bank resolved from CIA2 `$DD00`, and
|
|
183
|
+
the **absolute** screen-RAM + character-base addresses (no manual bank math).
|
|
184
|
+
- `audioDebug({op:'inspect', chip:'sid'})` — the SID voice/filter decode covered
|
|
185
|
+
in the SID section above.
|
|
186
|
+
|
|
187
|
+
**`c64_*` memory regions** (via `memory({op:'read'})`) — exact, named windows
|
|
188
|
+
onto the hardware, decoded live:
|
|
189
|
+
|
|
190
|
+
| region | size | notes |
|
|
191
|
+
| --------------- | ----- | ----------------------------------------------- |
|
|
192
|
+
| `system_ram` | 64 KB | full RAM |
|
|
193
|
+
| `c64_color_ram` | 1 KB | the nibble color RAM |
|
|
194
|
+
| `c64_vic_regs` | 64 B | VIC-II registers |
|
|
195
|
+
| `c64_sid_regs` | 29 B | SID registers (read via `sid_peek`) |
|
|
196
|
+
| `c64_cia1_regs` | 16 B | CIA1, from the `c_cia[]` array |
|
|
197
|
+
| `c64_cia2_regs` | 16 B | CIA2, from the `c_cia[]` array |
|
|
198
|
+
| `c64_cpu_regs` | 7 B | 6510 register file |
|
|
199
|
+
|
|
200
|
+
**Disassembly:** `disasm({target:'rom'})` and `disasm({target:'references'})`
|
|
201
|
+
accept `.prg` files (they understand the 2-byte little-endian load-address
|
|
202
|
+
header), and apply the C64 register-annotation table so VIC-II / SID / CIA
|
|
203
|
+
register accesses come back named rather than as bare addresses.
|
|
204
|
+
|
|
205
|
+
**Starter snippets** cover `vic_init` / `sprite_table` / `sid_play` /
|
|
206
|
+
`read_joystick` / `basic_stub`.
|
|
207
|
+
|
|
208
|
+
Disk-image loading and disk SAVE/restore tooling have their own sections below
|
|
209
|
+
("Disk images" and "Disk SAVES").
|
|
210
|
+
|
|
134
211
|
## Cartridge / load file format
|
|
135
212
|
|
|
136
213
|
The .prg format is dead simple:
|
|
@@ -14,6 +14,62 @@ one call fuses a framebuffer pixel scan with the live LCDC and returns
|
|
|
14
14
|
`verified:null` = step a frame first. Zero image tokens, frame-0-guarded — use it
|
|
15
15
|
as the first move when a change "did nothing."
|
|
16
16
|
|
|
17
|
+
## Toolchains
|
|
18
|
+
|
|
19
|
+
Default is **C** via SDCC's sm83 port (the same SDCC that powers SMS/GG/MSX/
|
|
20
|
+
Coleco). For hand-tuned asm, pass `language:"asm"` to route through RGBDS. The C
|
|
21
|
+
path uses `__sfr __at 0xFFNN` to bind GB I/O regs; the helper headers under
|
|
22
|
+
`src/platforms/gb/lib/c/gb_hardware.h` define LCDC/STAT/SCY/SCX/LY/BGP/OBP0/OBP1/
|
|
23
|
+
etc. for both DMG and CGB. ⚠ SDCC 4.4.0 codegen quirk: `for (;;) { switch + write
|
|
24
|
+
to __sfr }` crashes the register allocator — use `do { ... } while (1)` and
|
|
25
|
+
table-lookup writes instead. (See the GB/GBC SDCC_GOTCHAS for the full set of
|
|
26
|
+
sm83 codegen footguns.)
|
|
27
|
+
|
|
28
|
+
## MCP debug & inspection tooling
|
|
29
|
+
|
|
30
|
+
The bundled gambatte core is patched to expose deep live state — sprites,
|
|
31
|
+
palettes, tiles, background/LCDC, CPU, and raw memory regions. This applies to
|
|
32
|
+
**both `gb` and `gbc`** builds (one shared gambatte core).
|
|
33
|
+
|
|
34
|
+
**Live inspectors (decode hardware state, no manual byte-twiddling):**
|
|
35
|
+
|
|
36
|
+
- **`sprites({op:'inspect'})`** — decodes all 40 OAM slots and renders a
|
|
37
|
+
sprite-sheet PNG with sprite-priority + horizontal/vertical flip applied.
|
|
38
|
+
- **`palette({source:'live'})`** — DMG path decodes the BGP / OBP0 / OBP1 bytes
|
|
39
|
+
into 4 shades each; CGB path decodes the 64-byte BCPS/OCPS palette RAM into
|
|
40
|
+
8 palettes × 4 colors in BGR555.
|
|
41
|
+
- **`tiles({op:'png'})`** — renders all 384 tiles from $8000-$97FF.
|
|
42
|
+
- **`cpu({op:'read'})`** — SM83 register file: A/F/BC/DE/HL + flags + IME/halt.
|
|
43
|
+
- **`audioDebug({op:'inspect', chip:'gb'})`** — DMG APU decode: 2 pulse + wave
|
|
44
|
+
+ noise channels, with timer→freq→note conversion, sweep, duty, and panning,
|
|
45
|
+
read straight from the live `NR*` registers.
|
|
46
|
+
- **`background({view:'renderState'})`** — LCDC bit-by-bit, scroll (SCX/SCY),
|
|
47
|
+
LY/LYC, window state, plus CGB extras: current VRAM bank, KEY1, and the
|
|
48
|
+
BCPS/OCPS palette index.
|
|
49
|
+
|
|
50
|
+
**Raw memory regions** via `memory({op:'read', region:...})`:
|
|
51
|
+
|
|
52
|
+
| Region | Contents |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `gb_vram` | VRAM ($8000-$9FFF) — tile data + BG maps (CGB: the active bank) |
|
|
55
|
+
| `gb_oam` | OAM ($FE00-$FE9F) — 40 sprites × 4 bytes |
|
|
56
|
+
| `gb_io` | I/O register page ($FF00-$FF7F) — LCDC, BGP, JOYP, CGB regs, etc. |
|
|
57
|
+
| `gb_hram` | HRAM ($FF80-$FFFE) — fast scratch |
|
|
58
|
+
| `gb_bgpdata` | CGB BG palette RAM (64 bytes) |
|
|
59
|
+
| `gb_objpdata` | CGB OBJ palette RAM (64 bytes) |
|
|
60
|
+
| `gb_cpu_regs` | SM83 register snapshot |
|
|
61
|
+
|
|
62
|
+
⚠ **Gotcha: gambatte exposes `gb_vram`, NOT the generic `video_ram` region.**
|
|
63
|
+
Other platforms' cores expose video memory under `video_ram`; on GB/GBC you must
|
|
64
|
+
ask for `gb_vram` (and the other `gb_*` names above). A `video_ram` read here
|
|
65
|
+
returns nothing.
|
|
66
|
+
|
|
67
|
+
**Disassembly:** `disasm({target:'rom'})` + `disasm({target:'references'})` +
|
|
68
|
+
`disasm({target:'project'})` route through the native binutils z80 `objdump` in
|
|
69
|
+
its `gbz80` machine (WASM, `-m gbz80`) — full CB-prefix coverage plus the
|
|
70
|
+
SM83-specific opcodes (`ld (hl+),a`, `ldh`, `reti`, `ld hl,sp+e8`). One z80-elf
|
|
71
|
+
binutils serves both plain Z80 (SMS/GG/MSX) and the GB CPU.
|
|
72
|
+
|
|
17
73
|
## Five silent-failure footguns to know before you start (R26 + R27)
|
|
18
74
|
|
|
19
75
|
If your ROM compiles cleanly but doesn't render — or sprites land in
|
|
@@ -125,9 +125,9 @@ maxmod (separate library, not bundled here).
|
|
|
125
125
|
|
|
126
126
|
**Debugging sound:** `audioDebug({op:'inspect', chip:"gba"})` decodes the live APU —
|
|
127
127
|
per-channel freq→note/duty/volume for the 4 tone channels plus the 2 Direct
|
|
128
|
-
Sound FIFO states.
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
Sound FIFO states. See "MCP debug & inspection tooling" below for the rest of
|
|
129
|
+
the live-debug loop (sprites / palette / background / cpu / breakpoint + the
|
|
130
|
+
memory regions and disasm pipeline).
|
|
131
131
|
|
|
132
132
|
**For scaffold-level sfx**, the libtonc runtime ships a minimal
|
|
133
133
|
`gba_sfx.h` / `gba_sfx.c` pair (3 functions: `sfx_init`, `sfx_tone`,
|
|
@@ -136,6 +136,60 @@ as the NES/GB scaffold sound API, so cross-platform game ports feel
|
|
|
136
136
|
the same. All 5 GBA genre scaffolds (shmup/platformer/puzzle/sports/
|
|
137
137
|
racing) use it.
|
|
138
138
|
|
|
139
|
+
## MCP debug & inspection tooling
|
|
140
|
+
|
|
141
|
+
GBA runs on mGBA (patched). These inspectors read the *live* core state —
|
|
142
|
+
reach for them when a sprite, palette, or BG renders wrong and the source
|
|
143
|
+
alone doesn't explain it. (The audio inspector is also summarized under
|
|
144
|
+
"Sound" above.)
|
|
145
|
+
|
|
146
|
+
- **`sprites({op:'inspect'})`** — decodes all **128 OAM sprites** into a
|
|
147
|
+
generic shape: attr0/1/2 unpacked to shape + size, **9-bit signed X**,
|
|
148
|
+
the affine and hidden flags, and tile / palette / priority.
|
|
149
|
+
- **`palette({source:'live'})`** — reads the palette as **15-bit BGR555**:
|
|
150
|
+
256 BG entries + 256 OBJ entries. Pass `area:'bg'` or `area:'sprite'` to
|
|
151
|
+
pick the half.
|
|
152
|
+
- **`cpu({op:'read'})`** — ARM7TDMI dump: the 16 general regs **r0-r15**,
|
|
153
|
+
`cpsr` + `spsr`, the processor mode, the ARM/THUMB state bit, and an
|
|
154
|
+
**`execPc`** field that is r15 adjusted back for the pipeline prefetch
|
|
155
|
+
(r15 reads ahead of the executing instruction, so raw r15 is misleading —
|
|
156
|
+
use `execPc` for "where am I really").
|
|
157
|
+
- **`audioDebug({op:'inspect', chip:'gba'})`** — the 4 DMG-compatible PSG
|
|
158
|
+
channels (per-channel freq→note / duty / volume) plus the **2 Direct Sound
|
|
159
|
+
DMA FIFO** states, and master / bias. See "Sound" above.
|
|
160
|
+
- **`background({view:'renderState'})`** — decodes DISPCNT: the BG mode, and
|
|
161
|
+
per-BG enable / priority / char-base / map-base / color-mode, the
|
|
162
|
+
forced-blank bit, and OBJ enable. Use it to confirm REG_DISPCNT and the
|
|
163
|
+
REG_BGxCNT bases match where you uploaded tiles + maps.
|
|
164
|
+
|
|
165
|
+
### Memory regions (`memory({op:'read', region:…})`)
|
|
166
|
+
|
|
167
|
+
| Region | Address / size | Contents |
|
|
168
|
+
|-----------------|------------------------------------|-------------------------------------------|
|
|
169
|
+
| `gba_cpu_regs` | — | ARM7TDMI register snapshot |
|
|
170
|
+
| `gba_io_regs` | $04000000-$040003FE (1 KB) | the I/O page — **video AND audio** MMIO |
|
|
171
|
+
| `gba_palette` | $05000000-$050003FF (1 KB) | 256 BG + 256 OBJ BGR555 entries |
|
|
172
|
+
| `gba_oam` | $07000000-$070003FF (1 KB) | 128 sprite attribute entries (8 B each) |
|
|
173
|
+
| `system_ram` | $02000000 EWRAM / $03000000 IWRAM | main + on-chip work RAM |
|
|
174
|
+
| `video_ram` | $06000000-$06017FFF (96 KB) | BG + sprite tile data + framebuffer |
|
|
175
|
+
| `save_ram` | $0E000000-$0E00FFFF (64 KB) | battery-backed SRAM |
|
|
176
|
+
|
|
177
|
+
Pair `sprites` / `palette` / `background` / `cpu` with
|
|
178
|
+
`breakpoint({on:'write'})` for the full live-debug loop.
|
|
179
|
+
|
|
180
|
+
### Disassembly (`disasm({target:…})`)
|
|
181
|
+
|
|
182
|
+
`disasm({target:'rom'})`, `disasm({target:'references'})`, and
|
|
183
|
+
`disasm({target:'project'})` run the native binutils
|
|
184
|
+
**`arm-none-eabi-objdump`** (WASM) — **ARM mode by default**, pass
|
|
185
|
+
`thumb:true` for Thumb code. The byte-exact project reassembles through
|
|
186
|
+
`arm-none-eabi-as` / `ld` / `objcopy`.
|
|
187
|
+
|
|
188
|
+
**Gotcha (until ARM/Thumb mode-tracking lands):** GBA C compiles mostly to
|
|
189
|
+
**Thumb** reached via an **ARM crt0 stub**, so an ARM-mode disasm of a full
|
|
190
|
+
ROM decodes the Thumb spans as `.byte` — still byte-exact, just less readable.
|
|
191
|
+
Disasm the Thumb spans with `thumb:true` to get real mnemonics.
|
|
192
|
+
|
|
139
193
|
## Frame heartbeat
|
|
140
194
|
|
|
141
195
|
```c
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -74,6 +74,27 @@ the same wall.
|
|
|
74
74
|
- **HDMA** ($FF51-$FF55) for fast block transfers during HBlank —
|
|
75
75
|
used for live tile streaming.
|
|
76
76
|
|
|
77
|
+
## MCP debug & inspection tooling
|
|
78
|
+
|
|
79
|
+
GBC shares the patched gambatte core with DMG, so **all the live inspectors
|
|
80
|
+
and `gb_*` memory regions documented in the GB MENTAL_MODEL apply unchanged
|
|
81
|
+
here** — `sprites({op:'inspect'})`, `tiles({op:'png'})`, `cpu({op:'read'})`,
|
|
82
|
+
`audioDebug({op:'inspect', chip:'gb'})`, and the `gb_vram` / `gb_oam` / `gb_io`
|
|
83
|
+
/ `gb_hram` / `gb_cpu_regs` regions (same gotcha: it's `gb_vram`, NOT the
|
|
84
|
+
generic `video_ram`). Disassembly routes through the same `-m gbz80` objdump.
|
|
85
|
+
See the GB MENTAL_MODEL for the shared gambatte debug tooling.
|
|
86
|
+
|
|
87
|
+
CGB-only deltas on top of that shared set:
|
|
88
|
+
|
|
89
|
+
- **`palette({source:'live'})`** on a CGB ROM decodes the **64-byte BCPS/OCPS
|
|
90
|
+
palette RAM** into **8 palettes × 4 colors in BGR555** (the DMG path that
|
|
91
|
+
decodes BGP/OBP0/OBP1 bytes is what runs on a `gb` build instead). The raw
|
|
92
|
+
CGB palette RAM is also readable directly via the **`gb_bgpdata`** (BG, 64
|
|
93
|
+
bytes) and **`gb_objpdata`** (OBJ, 64 bytes) memory regions.
|
|
94
|
+
- **`background({view:'renderState'})`** reports the CGB extras the DMG path
|
|
95
|
+
doesn't have: the current **VRAM bank** (VBK), **KEY1** (double-speed state),
|
|
96
|
+
and the live **BCPS/OCPS palette index**.
|
|
97
|
+
|
|
77
98
|
## CGB vs DMG mode
|
|
78
99
|
|
|
79
100
|
The CGB boot ROM checks header byte **`$0143`**:
|
|
@@ -413,6 +413,25 @@ headless per-PCM-channel "is it playing" readout for Genesis yet (it would need
|
|
|
413
413
|
core patch to expose the XGM2 Z80 driver state), so audio verification here is
|
|
414
414
|
record-and-listen, not assert.
|
|
415
415
|
|
|
416
|
+
## MCP debug & inspection tooling
|
|
417
|
+
|
|
418
|
+
The shipped genesis_plus_gx (gpgx) core is patched for live introspection.
|
|
419
|
+
Video is deeply readable; the FM audio chip is only partially exposed:
|
|
420
|
+
|
|
421
|
+
- **Sprites:** `sprites({op:'inspect'})` decodes the live SAT.
|
|
422
|
+
- **Palette:** `palette({source:'live'})` reads live CRAM.
|
|
423
|
+
- **CPU:** `cpu({op:'read', cpu:'main'})` reads the 68000.
|
|
424
|
+
- **Audio (limited):** `getYm2612State` returns the YM2612's internal
|
|
425
|
+
struct as a raw blob — gpgx doesn't expose it in a safely per-channel
|
|
426
|
+
decodable form (good for frame-to-frame diffing, see "Debugging sound").
|
|
427
|
+
`getPsgState` decodes the SN76489 (3 tone + 1 noise channels).
|
|
428
|
+
- **Memory regions:** `memory({op:'read'})` exposes CRAM, VSRAM, VDP_REGS,
|
|
429
|
+
Z80_RAM (the sound CPU's RAM), M68K work RAM, YM2612, PSG, and VRAM.
|
|
430
|
+
Remember the gpgx byte-swap quirk: VRAM and WRAM read host-LE
|
|
431
|
+
word-byte-swapped (a 16-bit value's two bytes are swapped at the offset)
|
|
432
|
+
— account for it or read single bytes (see "Reading your C globals
|
|
433
|
+
headlessly").
|
|
434
|
+
|
|
416
435
|
## ROM layout
|
|
417
436
|
|
|
418
437
|
```
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -160,6 +160,30 @@ void main(void) {
|
|
|
160
160
|
}
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
## MCP debug & inspection tooling
|
|
164
|
+
|
|
165
|
+
Game Gear runs on the same genesis_plus_gx (gpgx, patched) core as SMS, so the
|
|
166
|
+
inspectors are identical. **The canonical reference lives in the SMS
|
|
167
|
+
MENTAL_MODEL** (`src/platforms/sms/MENTAL_MODEL.md`, "MCP debug & inspection
|
|
168
|
+
tooling" section): `sprites({op:'inspect'})`, `tiles({op:'png'})`,
|
|
169
|
+
`cpu({op:'read'})` (Z80), `background({view:'renderState'})`,
|
|
170
|
+
`audioDebug({op:'inspect', chip:'psg'})` (SN76489, the shared
|
|
171
|
+
SMS/GG/Genesis region), and the z80 `objdump` disasm pipeline all apply
|
|
172
|
+
unchanged.
|
|
173
|
+
|
|
174
|
+
Game-Gear-only deltas:
|
|
175
|
+
|
|
176
|
+
- **`palette({source:'live'})`** decodes **12-bit BGR (4-4-4)**, twice the
|
|
177
|
+
depth of SMS's 6-bit. CRAM is **64 bytes** (2 little-endian bytes per
|
|
178
|
+
entry) instead of 32.
|
|
179
|
+
- The Game-Gear memory regions are **`gg_vram`** and **`gg_cram`** (the
|
|
180
|
+
64-byte palette); use these instead of `sms_vram` / `sms_cram`. The
|
|
181
|
+
`sms_vdp_regs` / `sms_z80_regs` register regions are shared (same VDP and
|
|
182
|
+
Z80).
|
|
183
|
+
- `sprites({op:'inspect'})` X/Y fields are reported in **256×192 hardware
|
|
184
|
+
coordinates**, not the 160×144 visible window — match them with
|
|
185
|
+
hardware-coord arithmetic (see "Sprite coords are hardware-space" above).
|
|
186
|
+
|
|
163
187
|
## Differences from SMS — quick reference
|
|
164
188
|
|
|
165
189
|
- Visible 160×144 vs 256×192 — center content
|
|
@@ -35,13 +35,39 @@ Mikey handles:
|
|
|
35
35
|
- **Joystick** — read via SWITCHES register at `$FCB0`. cc65 provides
|
|
36
36
|
`joy_read(JOY_1)` + `JOY_LEFT/RIGHT/UP/DOWN/BTN_1/BTN_2` macros.
|
|
37
37
|
|
|
38
|
-
**Live debug:**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
**Live debug:** the MCP inspectors (`palette` / `cpu` / `audioDebug` /
|
|
39
|
+
`background` / `breakpoint`, and the SCB-list-head `sprites` special case) are
|
|
40
|
+
documented in "MCP debug & inspection tooling" below.
|
|
41
|
+
|
|
42
|
+
## MCP debug & inspection tooling
|
|
43
|
+
|
|
44
|
+
The Lynx runs on handy (patched). The inspectors read the *live* core state —
|
|
45
|
+
reach for them when a sprite or palette renders wrong and the source alone
|
|
46
|
+
doesn't explain it. Details and the per-tool facts:
|
|
47
|
+
|
|
48
|
+
- **`palette({source:'live'})`** — the **16-entry, 12-bit RGB** Mikey palette
|
|
49
|
+
(`$FDA0-$FDBF`) converted to RGB.
|
|
50
|
+
- **`cpu({op:'read'})`** — 65C02 dump: A / X / Y / P / SP / PC plus the
|
|
51
|
+
decoded flag bits.
|
|
52
|
+
- **`audioDebug({op:'inspect', chip:'mikey'})`** — the 4 Mikey voices: volume,
|
|
53
|
+
the timer→period→frequency→note chain, and the **12-bit LFSR** state.
|
|
54
|
+
- **`background({view:'renderState'})`** — decodes DISPCTL: the DMA-enable,
|
|
55
|
+
flip, and color-mode bits plus the display base address.
|
|
56
|
+
- **`sprites({op:'inspect'})` is the special case.** The Lynx has **no fixed
|
|
57
|
+
OAM** — sprites are **SCB (Sprite Control Block) linked lists in RAM** that
|
|
58
|
+
Suzy walks at blit time. So this tool can't return a sprite table; instead
|
|
59
|
+
it returns the **SCB list head (SCBNEXT, `$FC10`/`$FC11`)** plus
|
|
60
|
+
instructions to walk the chain yourself over `system_ram`.
|
|
61
|
+
|
|
62
|
+
### Memory regions (`memory({op:'read', region:…})`)
|
|
63
|
+
|
|
64
|
+
| Region | Address / size | Contents |
|
|
65
|
+
|-----------------|-----------------------|-------------------------------------------------------|
|
|
66
|
+
| `lynx_cpu_regs` | — | 65C02 register snapshot |
|
|
67
|
+
| `lynx_hw_regs` | $FC00-$FDFF window | the **Suzy + Mikey** register window — sprite-engine regs, LCD control, audio, palette |
|
|
68
|
+
| `system_ram` | 64 KB | full address space (also where the SCB chain lives) |
|
|
69
|
+
|
|
70
|
+
Pair these with `breakpoint({on:'write'})` for the full live-debug loop.
|
|
45
71
|
|
|
46
72
|
## Frame heartbeat (cc65 + tgi)
|
|
47
73
|
|
|
@@ -117,3 +117,30 @@ exactly this.
|
|
|
117
117
|
generator + the envelope (period + shape bits).
|
|
118
118
|
- `memory({op:'read'})` regions: `msx_vram`, `msx_vdp_regs`, `msx_vdp_status`,
|
|
119
119
|
`msx_palette`, `msx_cpu_regs`, `msx_psg_regs`, plus `system_ram` (work RAM).
|
|
120
|
+
|
|
121
|
+
## MCP debug & inspection tooling
|
|
122
|
+
|
|
123
|
+
MSX is a **Tier-1** platform with deep introspection — the full set of
|
|
124
|
+
inspectors and memory regions is listed under **"Debugging tools"** above
|
|
125
|
+
(`cpu` / `background` / `palette` / `sprites` / `symbols` / `audioDebug` for
|
|
126
|
+
the AY-3-8910 PSG, and the `msx_vram` / `msx_vdp_regs` / `msx_vdp_status` /
|
|
127
|
+
`msx_palette` / `msx_cpu_regs` / `msx_psg_regs` / `system_ram` regions). The
|
|
128
|
+
PSG-channel decode means `audioDebug({op:'inspect', chip:'ay8910'})` gives
|
|
129
|
+
you the 3 square-wave channels plus the shared noise generator and envelope
|
|
130
|
+
without poking at `msx_psg_regs` by hand.
|
|
131
|
+
|
|
132
|
+
### ColecoVision shares this core family — but is bring-up only
|
|
133
|
+
|
|
134
|
+
ColecoVision runs the same toolchain family and exposes only the **standard**
|
|
135
|
+
introspection: `system_ram` + `save_ram` + `video_ram`. It has **no deep
|
|
136
|
+
inspectors** (no `palette` / `sprites` / `background` / `audioDebug` decode)
|
|
137
|
+
and **no MENTAL_MODEL of its own** — treat it as a bring-up target, not a
|
|
138
|
+
finished Tier-1 platform.
|
|
139
|
+
|
|
140
|
+
### Extending introspection (for whoever adds a platform)
|
|
141
|
+
|
|
142
|
+
Deeper, decoded inspectors are not free — each is implemented by **patching
|
|
143
|
+
the emulator core** to expose the extra register/VRAM regions, then wiring a
|
|
144
|
+
decoder. To add deep introspection to ColecoVision (or any thin platform),
|
|
145
|
+
follow the existing core-patch pattern used for snes9x / gpgx / fceumm / vice
|
|
146
|
+
under **`scripts/patches/`**.
|
|
@@ -25,6 +25,13 @@ The cc65 runtime claims:
|
|
|
25
25
|
- ZP $1C+ available to your game (with our chr-ram crt0)
|
|
26
26
|
- `$0500-$07FF` (3 pages): cc65 C parameter stack
|
|
27
27
|
|
|
28
|
+
> **cc65 zero-page starts at $02, not $00 (applies to every cc65 platform —
|
|
29
|
+
> NES, C64, Atari, Lynx, …).** cc65 reserves `$00-$01` for its runtime, so your
|
|
30
|
+
> first `.res 1` in the `ZEROPAGE` segment lands at **$02**, not $00. If you
|
|
31
|
+
> hand-write asm that assumes a zero-page var is at $00 you'll clobber the
|
|
32
|
+
> runtime. Confirm actual addresses with `symbols({op:'map'})` after
|
|
33
|
+
> `build({output:'romWithDebug'})`.
|
|
34
|
+
|
|
28
35
|
## PPU memory map (separate from CPU bus!)
|
|
29
36
|
|
|
30
37
|
```
|
|
@@ -94,6 +101,15 @@ The `nes_runtime` helper `tile_set_palette(nt, x, y, palette)` does
|
|
|
94
101
|
the read-modify-write dance and the bit-twiddling — use it instead
|
|
95
102
|
of writing attributes by hand.
|
|
96
103
|
|
|
104
|
+
> **256-tile cap per pattern table (the busy-image trap).** The nametable's
|
|
105
|
+
> tile index is 8-bit, so a single pattern table holds at most **256 unique
|
|
106
|
+
> tiles** — and a per-frame BG can therefore use at most 256 distinct tiles.
|
|
107
|
+
> Auto-converting a busy full-screen illustration almost always needs more than
|
|
108
|
+
> 256 unique 8×8 tiles and **overflows**; `encodeArt({stage:'tilemap'})` warns
|
|
109
|
+
> when it does. The only real workaround is mid-frame CHR bank switching
|
|
110
|
+
> (an MMC3-class mapper) — the bundled NROM presets can't do it, so design BG
|
|
111
|
+
> art to reuse tiles (≤256 unique per table).
|
|
112
|
+
|
|
97
113
|
## Palettes
|
|
98
114
|
|
|
99
115
|
32 bytes at $3F00-$3F1F:
|
|
@@ -329,6 +345,25 @@ incorrectly aligned."
|
|
|
329
345
|
scrolling worlds you need to manage the nametable buffer + bank
|
|
330
346
|
switching yourself.
|
|
331
347
|
|
|
348
|
+
## MCP debug & inspection tooling
|
|
349
|
+
|
|
350
|
+
The shipped fceumm core is patched for live introspection — read state
|
|
351
|
+
instead of guessing:
|
|
352
|
+
|
|
353
|
+
- **Sprites:** `sprites({op:'inspect'})` decodes live OAM.
|
|
354
|
+
- **Palette:** `palette({source:'live'})` reads the live 32-byte palette RAM.
|
|
355
|
+
- **CPU:** `cpu({op:'read'})` reads the 6502.
|
|
356
|
+
- **Background render state:** `background({view:'renderState'})` decodes
|
|
357
|
+
PPUCTRL/PPUMASK and resolves the active CHR bank (plus its file offset) —
|
|
358
|
+
this is what tells you which pattern table BG vs sprites are fetching from
|
|
359
|
+
(the bit-4 footgun above).
|
|
360
|
+
- **Memory regions:** `memory({op:'read'})` exposes OAM, Palette,
|
|
361
|
+
Nametables (CIRAM — including the 2-bit-per-16x16 attribute data that
|
|
362
|
+
selects each tile group's sub-palette, decoded by `inspectBackgroundMap`),
|
|
363
|
+
CHR (live MMC1-banked CHR — don't parse the iNES file), CPU_REGS,
|
|
364
|
+
PPU_REGS, and APU_REGS (the synthesized $4000-$4017 snapshot consumed by
|
|
365
|
+
`audioDebug`).
|
|
366
|
+
|
|
332
367
|
## Rebuilding a CHR-ROM NROM image (reverse-engineering)
|
|
333
368
|
|
|
334
369
|
The homebrew presets above are CHR-**RAM** (the CPU uploads tiles at runtime).
|
|
@@ -267,6 +267,57 @@ Loadable via genesis_plus_gx (`loadMedia`).
|
|
|
267
267
|
- Game Gear-specific buttons add port $00 read (Start) and stereo
|
|
268
268
|
PSG control on port $06.
|
|
269
269
|
|
|
270
|
+
## MCP debug & inspection tooling
|
|
271
|
+
|
|
272
|
+
SMS and Game Gear share the genesis_plus_gx (gpgx, patched) core, so the
|
|
273
|
+
inspectors below behave identically on both. This section is the **canonical
|
|
274
|
+
shared reference**; the Game Gear MENTAL_MODEL points back here and only lists
|
|
275
|
+
its own deltas (12-bit CRAM, etc.). Reach for these when something renders
|
|
276
|
+
wrong and you can't see why from the source alone — they read the *live* core
|
|
277
|
+
state, which beats trusting comments.
|
|
278
|
+
|
|
279
|
+
- **`sprites({op:'inspect'})`** — decodes the live SAT (sprite attribute
|
|
280
|
+
table) and renders a sprite-sheet PNG. Reports each slot's X/Y/tile plus
|
|
281
|
+
`spriteTileDataBase` (the VRAM address the VDP is actually fetching sprite
|
|
282
|
+
tiles from). This is the tool that catches the $D0-terminator and the
|
|
283
|
+
R6/$2000-vs-$0000 sprite-bank footguns described above — trust its bytes
|
|
284
|
+
over any comment.
|
|
285
|
+
- **`palette({source:'live'})`** — reads CRAM and converts to RGB. On SMS
|
|
286
|
+
that's **6-bit BGR** (2-2-2, 32 bytes). On Game Gear it's **12-bit BGR**
|
|
287
|
+
(4-4-4, 64 bytes) — see the GG MENTAL_MODEL for that delta.
|
|
288
|
+
- **`tiles({op:'png'})`** — dumps VRAM tile patterns as a sheet. The format
|
|
289
|
+
is 4bpp bitplane-interleaved (32 bytes/tile); the 16 KB VRAM renders as a
|
|
290
|
+
512-tile sheet.
|
|
291
|
+
- **`cpu({op:'read'})`** — Z80 register dump: A/F, BC/DE/HL, IX/IY, the
|
|
292
|
+
shadow register set, the flag bits, and interrupt state (IM mode / IFF).
|
|
293
|
+
- **`audioDebug({op:'inspect', chip:'psg'})`** — decodes the live SN76489:
|
|
294
|
+
3 tone channels + 1 noise channel. The PSG region is shared by SMS, GG, and
|
|
295
|
+
Genesis (same gpgx region).
|
|
296
|
+
- **`background({view:'renderState'})`** — reads the VDP registers and reports
|
|
297
|
+
the derived addresses (name table, BG-tile base, sprite-tile base, SAT base),
|
|
298
|
+
the scroll registers, and the display-enable / mode state. Use this to
|
|
299
|
+
confirm the R2/R4/R5/R6 baseline matches where you actually uploaded data.
|
|
300
|
+
|
|
301
|
+
### Memory regions (`memory({op:'read', region:…})`)
|
|
302
|
+
|
|
303
|
+
| Region | Contents |
|
|
304
|
+
|-----------------|------------------------------------------------------|
|
|
305
|
+
| `sms_vram` | 16 KB VRAM — tiles + name table + SAT + sprite tiles |
|
|
306
|
+
| `sms_cram` | 32-byte palette (2-2-2 BGR) |
|
|
307
|
+
| `sms_vdp_regs` | the 11 VDP control registers ($00-$0A) |
|
|
308
|
+
| `sms_z80_regs` | Z80 register snapshot |
|
|
309
|
+
| `gg_vram` | Game Gear VRAM |
|
|
310
|
+
| `gg_cram` | Game Gear 64-byte palette (4-4-4 BGR) |
|
|
311
|
+
|
|
312
|
+
### Disassembly (`disasm({target:…})`)
|
|
313
|
+
|
|
314
|
+
`disasm({target:'rom'})`, `disasm({target:'references'})`, and
|
|
315
|
+
`disasm({target:'project'})` all run the live ROM through the native binutils
|
|
316
|
+
**z80 `objdump`** (WASM, `-m z80`). It has full prefix coverage —
|
|
317
|
+
CB/ED/DD/FD/DDCB/FDCB — and feeds the same auto-label, register-annotation,
|
|
318
|
+
file-offset, and `untilReturn` pipeline used by the NES and SNES
|
|
319
|
+
disassemblers.
|
|
320
|
+
|
|
270
321
|
## Horizontal scrolling (for side-scrollers)
|
|
271
322
|
|
|
272
323
|
The `platformer` scaffold is single-screen. To make it a side-scroller:
|
|
@@ -208,6 +208,27 @@ per-voice vol/pitch/ADSR + `env` (0 = silent regardless of vol) + `bufLastSample
|
|
|
208
208
|
produced output" from "muted by mixer." GOTCHA: S-DSP FLG is $6C, KOFF is $5C
|
|
209
209
|
(many refs swap them); power-on FLG=$E0 means your driver MUST clear bit 6.
|
|
210
210
|
|
|
211
|
+
## MCP debug & inspection tooling
|
|
212
|
+
|
|
213
|
+
The shipped snes9x core is patched for deep introspection — both audio and
|
|
214
|
+
video are fully readable, so you assert live state instead of guessing:
|
|
215
|
+
|
|
216
|
+
- **Sprites:** `sprites({op:'inspect'})` decodes live OAM (per-sprite
|
|
217
|
+
renderable/hidden, resolved tile VRAM addr, CGRAM palette range, and the
|
|
218
|
+
uninitialized-OBJ-palette warning described under "The OBJ stable-path
|
|
219
|
+
recipe").
|
|
220
|
+
- **Palette:** `palette({source:'live'})` reads live CGRAM.
|
|
221
|
+
- **CPUs:** `cpu({op:'read', cpu:'main'})` for the 65816, `cpu({op:'read',
|
|
222
|
+
cpu:'spc700'})` for the sound CPU.
|
|
223
|
+
- **Audio:** the S-DSP is fully decodable — full per-voice state plus the
|
|
224
|
+
master mixer (see "Debugging sound" above for `audioDebug`).
|
|
225
|
+
- **Memory regions:** `memory({op:'read'})` exposes OAM, CGRAM, ARAM (SPC700
|
|
226
|
+
audio RAM), and **FillRAM**. Note the FillRAM quirk: snes9x mirrors the
|
|
227
|
+
PPU registers $2100-$213F (OBSEL/BGMODE/TM/TS/color-math, etc.) into
|
|
228
|
+
FillRAM indexed by the FULL address (e.g. `FillRAM[0x2101]` = OBSEL), so
|
|
229
|
+
the PPU register state is readable through the `snes_fillram` region — no
|
|
230
|
+
core patch needed.
|
|
231
|
+
|
|
211
232
|
## ROM layout (LoROM)
|
|
212
233
|
|
|
213
234
|
```
|
|
@@ -193,6 +193,49 @@ Expected. tcc-65816.wasm + wla-65816.wasm cold-load + compile takes
|
|
|
193
193
|
1-2s. Subsequent builds reuse the warm worker pool (R12 crash
|
|
194
194
|
isolation infrastructure). Steady-state builds are sub-second.
|
|
195
195
|
|
|
196
|
+
## "asm build fails with `ok:false` and an empty/cryptic `issues[]`" (asar idioms)
|
|
197
|
+
|
|
198
|
+
The `language:"asm"` SNES path runs through **asar**, which **silently crashes**
|
|
199
|
+
(a heap-pointer Emscripten exit code, no diagnostic) on a handful of source
|
|
200
|
+
idioms. romdev's asar wrapper has a preflight that catches the common cases and,
|
|
201
|
+
when asar dies with `ok:false` and no parsed errors, retries with `--verbose` and
|
|
202
|
+
synthesizes a fallback `issues[]` entry with a hint. The idioms to avoid:
|
|
203
|
+
|
|
204
|
+
- **`$ - label` size expressions** (current-PC minus a label) crash asar. Use an
|
|
205
|
+
explicit `end_label - start_label` difference instead:
|
|
206
|
+
```asm
|
|
207
|
+
; WRONG — crashes asar silently
|
|
208
|
+
my_size = $ - my_data
|
|
209
|
+
; RIGHT
|
|
210
|
+
my_data_end:
|
|
211
|
+
my_size = my_data_end - my_data
|
|
212
|
+
```
|
|
213
|
+
- **Opcode + operand arithmetic on an `=`-defined symbol**, e.g.
|
|
214
|
+
`STA SYMBOL + N` where `SYMBOL` is a `=` constant (not a label), can also crash
|
|
215
|
+
silently. The preflight flags the label-arithmetic-across-bank case
|
|
216
|
+
(`label-arithmetic constant`); for the rest, if you see `ok:false` with no real
|
|
217
|
+
error, suspect a `SYMBOL + N` and rewrite to a plain label or pre-compute the
|
|
218
|
+
address.
|
|
219
|
+
- **Bank-border crossed.** If your `org` + `dw`/data runs past `$00FFFF` you've
|
|
220
|
+
crossed a bank boundary and the layout is wrong. Native interrupt vectors live
|
|
221
|
+
at `$FFE4-$FFEE`, emulation vectors at `$FFF4-$FFFF` — keep your header/vector
|
|
222
|
+
block where the layout expects it. Use
|
|
223
|
+
`scaffold({op:'snippets', platform:"snes", mode:"get", name:"lorom_header.asm"})`
|
|
224
|
+
for the canonical layout (and `lorom_multibank.asm` for multi-bank).
|
|
225
|
+
|
|
226
|
+
(This is the asar/asm path. The default PVSnesLib **C** path goes through
|
|
227
|
+
tcc-65816 + wla-65816 and has its own C89 trap, above.)
|
|
228
|
+
|
|
229
|
+
## "CHR and tilemap overlap in VRAM / background renders as garbage tiles"
|
|
230
|
+
|
|
231
|
+
SNES CHR (tile patterns) and the BG tilemap share the 64 KB VRAM, addressed in
|
|
232
|
+
**words**. CHR starts at word `$0000`; if your CHR is 16 KB it occupies words
|
|
233
|
+
`$0000-$1FFF`, so a tilemap placed at word `$2000` collides with the tail of CHR
|
|
234
|
+
and you get garbage. **Put the tilemap at word `$4000` or later when your CHR is
|
|
235
|
+
big.** Verify the bases with `background({view:'renderState', platform:'snes'})`
|
|
236
|
+
(it decodes the BG char/map base) and cross-check the tile region with
|
|
237
|
+
`tiles({op:'png'})` at the CHR base.
|
|
238
|
+
|
|
196
239
|
## "I want to use real graphics from a PNG"
|
|
197
240
|
|
|
198
241
|
`gfx4snes` (the PVSnesLib companion tool) converts PNG → .pic + .pal.
|