romdevtools 0.23.0 → 0.25.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 +145 -498
- package/CHANGELOG.md +114 -3
- package/examples/atari7800/templates/sports.c +6 -2
- package/examples/sms/templates/shmup.c +5 -2
- 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 +250 -1
- package/src/http/skill-doc.js +1 -1
- package/src/mcp/tools/index.js +4 -46
- package/src/mcp/tools/input-layout.js +10 -0
- package/src/mcp/tools/input.js +31 -2
- package/src/mcp/tools/playtest.js +17 -2
- package/src/mcp/tools/project.js +39 -6
- package/src/mcp/tools/record.js +9 -3
- 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 +103 -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
- package/src/mcp/tool-manifest.js +0 -92
|
@@ -22,6 +22,32 @@ read that platform's `platform({op:'doc', platform, name:'mental_model'})`.
|
|
|
22
22
|
The cheat DB is bundled (`romdev_game_codes`). Do **not** scan the user's disk for
|
|
23
23
|
`.cht` files — if it's not in the bundled DB, treat it as absent and RE it.
|
|
24
24
|
|
|
25
|
+
**Reading a `cheats({op:'lookup'})` hit as a RAM/code map.** Each decoded part carries an
|
|
26
|
+
`address`, a `value`, a `kind` (`ram` = a labeled variable; `code` = a labeled ROM
|
|
27
|
+
patch site, has a `compare`), and a `device` (`game-genie`/`pro-action-replay`/
|
|
28
|
+
`gameshark`/`action-replay`/`raw`). So "which byte holds magic?" → one `lookup` call.
|
|
29
|
+
Filter a long list with `filter:"health"` or `kind:"ram"`. A match is by No-Intro
|
|
30
|
+
**name/filename, NOT a verified CRC** — it's PROBABLE (a different region/revision can
|
|
31
|
+
move addresses), so confirm before patching. The cheapest confirmation is to apply it
|
|
32
|
+
and watch: `cheats({op:'apply', path, desc})` → `frame({op:'screenshot'})`.
|
|
33
|
+
|
|
34
|
+
`cheats({op:'apply'})` is non-destructive (volatile core state, the RetroArch way — the ROM
|
|
35
|
+
file is never touched; `host({op:'reset'})`/`state({op:'load'})`/`cheats({op:'clear'})` removes it). It takes a
|
|
36
|
+
matched `desc`, a raw `code`, or `loadMedia({cheats:[…]})` to seed codes BEFORE frame 0.
|
|
37
|
+
`appliedAs` reports how it went in (`ram` poke / `rom` read-intercept / `raw` device
|
|
38
|
+
code). DB coverage is 13/14 (every tier-1 system except C64); GBA cheats are
|
|
39
|
+
encrypted, so apply-only (no labeled-address map — see `mapNote`).
|
|
40
|
+
|
|
41
|
+
**Creating a NEW code — `cheats({op:'make', platform, address, value, compare?})`.** The inverse of
|
|
42
|
+
decoding: turn a byte you found (via §1 or `breakpoint`) into a shareable, verified code,
|
|
43
|
+
for ANY ROM incl. your own homebrew/WIP. A RAM cheat needs just `address`+`value`; a ROM
|
|
44
|
+
patch adds `compare` (the byte currently there). It encodes for the platform's native
|
|
45
|
+
device(s) and labels each (NES/Genesis → Game Genie; SNES → Pro Action Replay **and**
|
|
46
|
+
Game Genie; GB/GBC → Game Genie + GameShark; SMS/GG → Action Replay) plus the raw
|
|
47
|
+
`ADDR:VAL`; each carries `verified:true` (round-trips against the full DB). Systems with no
|
|
48
|
+
letter-code device (Atari 2600/7800, Lynx, GBA, C64, PC Engine, MSX) get a verified raw
|
|
49
|
+
code. Works on all 14. Nothing is ever written to a ROM file.
|
|
50
|
+
|
|
25
51
|
---
|
|
26
52
|
|
|
27
53
|
## 1. To find the RAM address of a value (score / timer / stat / HP / record-id)
|
|
@@ -50,12 +76,20 @@ usually a struct/entity array, each island one record.
|
|
|
50
76
|
The #1 trap: visible names/labels are often **pre-rendered tile GRAPHICS**, not
|
|
51
77
|
font-rendered from an ASCII string. Patching the ASCII string then does nothing.
|
|
52
78
|
|
|
53
|
-
1. `text({op:'learn'})` on the on-screen text
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
1. `text({op:'learn'})` on the on-screen text — it infers the game's char→tile-ID map
|
|
80
|
+
(games use their own encoding: Excitebike A=$0A, Mario ASCII-offset, FF sparse). Two
|
|
81
|
+
modes: ROM mode `knownStrings:[{text, offset}]` when you found the bytes; **LIVE mode
|
|
82
|
+
`fromScreen:[{text, row, col}]`** reads the tile IDs straight off the live BG map at a
|
|
83
|
+
tile position (`background({view:'map'})` shows where the text sits) — this breaks the
|
|
84
|
+
chicken-and-egg of needing the offset you're still hunting. Live mode works on every
|
|
85
|
+
tilemap platform (NES/SNES/Genesis/GB/GBC/SMS/GG/C64); atari2600/7800, lynx, gba have
|
|
86
|
+
no text nametable → ROM mode only. If `learn` reports `likelyPreRenderedGraphic:true`
|
|
87
|
+
(unique sequential tiles, no font reuse), **stop** — the text is a bitmap. Editing it
|
|
88
|
+
means changing tile pixels, not a string. Do not patch any ASCII string you found.
|
|
89
|
+
2. If it IS font-rendered: `text({op:'find', romPath, text, fontMap})` locates the string
|
|
90
|
+
(returns `fileOffset`, `prgFileOffset`, and a bank-aware `cpuAddress`+`bank` to feed
|
|
91
|
+
`disasm({target:'rom'})`; flags a likely length-prefix byte to avoid the overrun trap),
|
|
92
|
+
then `text({op:'encode', text, fontMap})` → bytes for `romPatch({op:'write'})`.
|
|
59
93
|
3. To find where a graphic/text was sourced from: on **Genesis**, `watch({on:'dma', precision:'sampled'})`
|
|
60
94
|
— drive to the screen that shows the graphic, and it reports the ROM offset(s)
|
|
61
95
|
the tiles were DMA'd from (decoded from the VDP DMA registers). Edit the tile
|
|
@@ -103,6 +137,15 @@ from a SOURCE struct rather than written in place. Don't conclude "the address
|
|
|
103
137
|
is wrong." Find the source: `memory({op:'search'})` the live value to locate the struct
|
|
104
138
|
the copy reads from, then `breakpoint({on:'write'})` on THAT.
|
|
105
139
|
|
|
140
|
+
**Precision — exact vs sampled.** The default `breakpoint({on:'write'})` is a core-level write
|
|
141
|
+
watchpoint: it returns the EXACT writing instruction's PC, captured inside the CPU write
|
|
142
|
+
path — correct even for NMI/IRQ-driven writes (the common case where a frame-sampled PC
|
|
143
|
+
is just the idle loop). On a banked mapper it reports the `bank` (NES/GB/SMS-GG) so you
|
|
144
|
+
can pass `{startAddress, bank}` to `disasm({target:'rom'})`. The lighter
|
|
145
|
+
`breakpoint({on:'write', precision:'sampled'})` (a.k.a. `watch({on:'mem'})`) steps until the byte changes
|
|
146
|
+
and returns a frame-boundary PC — a lead, not a guarantee under interrupts; use it for the
|
|
147
|
+
value timeline or when you just want the change history, and cross-check the value trace.
|
|
148
|
+
|
|
106
149
|
---
|
|
107
150
|
|
|
108
151
|
## 5b. To READ a register at an instruction — execution breakpoints (all 14)
|
|
@@ -254,6 +297,101 @@ and `input({op:'set'})` state it too.)
|
|
|
254
297
|
|
|
255
298
|
---
|
|
256
299
|
|
|
300
|
+
## 7. Authoring & verifying the byte patch
|
|
301
|
+
|
|
302
|
+
Once you know WHAT to change, the write loop is a handful of calls — no custom scripts:
|
|
303
|
+
|
|
304
|
+
- **`assembleSnippet({cpu, origin, code})`** — assemble a tiny asm chunk to raw bytes (no
|
|
305
|
+
header/linker/segments). CPUs: `6502 / 65c02 / 65816 / 68k / z80 / sm83 / gb / gbc /
|
|
306
|
+
huc6280`. **Z80 gotcha:** the sdas dialect requires `#` on immediates (`ld a,#5`, not
|
|
307
|
+
`ld a,5`).
|
|
308
|
+
- **`romPatch({op:'write', path, offset, hex, expect})`** — the splicer THE other hack tools
|
|
309
|
+
compose through. **Always pass `expect`** (the current bytes) — it refuses the write if
|
|
310
|
+
they don't match, catching a hex/dec slip or a patch authored against region A applied to
|
|
311
|
+
region B. `allowExpand` for size-changing edits.
|
|
312
|
+
- **`romPatch({op:'diff', platform, a, b})`** — mapper-aware ROM diff: reports CPU addresses
|
|
313
|
+
(NROM-128 mirrors, SNES LoROM `XX:XXXX`), per-region tallies (PRG vs CHR vs header), and
|
|
314
|
+
`tile:N` annotations on CHR changes for direct sprite-hack identification. Use it to
|
|
315
|
+
confirm a patch landed where you meant.
|
|
316
|
+
- **`disasm({target:'references', path, platform, address})`** — find every instruction that
|
|
317
|
+
references a target address, classified `call/jump/branch/read/write/use/ref` (walks the
|
|
318
|
+
vector table too). The fast "who touches this?" for a STATIC image. Limitation: direct
|
|
319
|
+
addressing only — indirect/computed jumps aren't detected (use the runtime `watch`/
|
|
320
|
+
`breakpoint` tools in §5/§5d for those).
|
|
321
|
+
- **`cart({op:'extract', path, outputDir})`** — split a ROM into standard parts (NES header/
|
|
322
|
+
prg/chr; SNES copier_header+rom+internal header; Genesis vectors/header/body; GB boot/
|
|
323
|
+
header/body) + a `manifest.json` (mapper, mirroring…). **`cart({op:'wrap'})`** is the inverse:
|
|
324
|
+
emits `wrapperSource` + `linkerConfig` ready for `build({output:'rom'})`.
|
|
325
|
+
|
|
326
|
+
Verify-before-patch: `memory({op:'write', region:'system_ram', offset, hex})` on the LIVE emulator and
|
|
327
|
+
watch the screen react — cheaper than shipping a wrong ROM patch.
|
|
328
|
+
|
|
329
|
+
## 7b. Whole-ROM rebuildable disassembly — `disasm({target:'project'})`
|
|
330
|
+
|
|
331
|
+
For a STRUCTURAL hack (new logic, not a byte poke), turn the whole ROM into a
|
|
332
|
+
re-buildable project in one call: `disasm({target:'project', path, outputDir})`. It splits
|
|
333
|
+
the ROM into regions (per-16KB bank for banked NES, per-32KB for SNES LoROM, slot0+slotX
|
|
334
|
+
for GB, one flat region for SMS/Genesis/C64/Atari), disassembles each through the CPU's
|
|
335
|
+
native objdump, then **reassembles + verifies byte-exact** against the original; any line
|
|
336
|
+
that won't reproduce faithfully heals to a `.byte`/`db` of its real bytes, so the emitted
|
|
337
|
+
`.asm` ALWAYS rebuilds (`roundTrip.allByteExact`). `readablePercent` per region tells you
|
|
338
|
+
how much came back as real instructions vs. data. Alongside the `.asm` it writes the
|
|
339
|
+
turnkey **rebuild glue**: data blobs (NES CHR-ROM → `chr.bin`; stripped Genesis/GBA/Lynx/MSX
|
|
340
|
+
cartridge header → `*.bin`), a `BUILD.md` with the exact steps, and — where a one-call
|
|
341
|
+
rebuild exists — a `rebuild.json` of the precise `build({...})` args. So the loop is
|
|
342
|
+
`disasm({target:'project'})` → edit a `.asm` → rebuild → `romPatch({op:'diff'})` to confirm.
|
|
343
|
+
|
|
344
|
+
**Two rebuild tiers** (the disasm emits each CPU's native-reassembler syntax — ca65 for
|
|
345
|
+
6502/65816, GNU `as` for m68k/arm/z80/gbz80 — which only some `build()` toolchains consume):
|
|
346
|
+
- **One-call `build()` rebuild, byte-identical** — **NES, C64, Atari 7800, Lynx**. Feed
|
|
347
|
+
`rebuild.json` straight to `build`. (Lynx: `build()` yields the headerless image; prepend
|
|
348
|
+
the shipped `lnx_header.bin` for the full `.lnx`.)
|
|
349
|
+
- **Native-recipe rebuild (`buildCall:null`), byte-identical, steps in `BUILD.md`** — **SMS,
|
|
350
|
+
GG, MSX, GB, GBC, Genesis, GBA, Atari 2600**. Their `build()` toolchains (SDCC/RGBDS/asar/
|
|
351
|
+
dasm/vasm) can't reassemble ca65/GNU-as syntax, so `BUILD.md` gives the proven native
|
|
352
|
+
`as`/`ld`/`objcopy` chain.
|
|
353
|
+
- **PC Engine** is the one not-yet-byte-exact case (the region trims real padding / doesn't
|
|
354
|
+
strip a copier header) — `BUILD.md` flags it.
|
|
355
|
+
|
|
356
|
+
**Rebuilding a commercial NES (NROM CHR-ROM) game — `build({inesHeader})`:** the most common
|
|
357
|
+
NES RE rebuild. `build({output:'rom', platform:'nes', inesHeader:{prgBanks, chrBanks, mapper,
|
|
358
|
+
mirroring}, sourcesPaths:{…the PRG…}, binaryIncludePaths:{"chr.bin":…}})` auto-emits the
|
|
359
|
+
16-byte iNES header + CHARS-segment wiring + flat NROM `.cfg` — no hand-derived header bytes.
|
|
360
|
+
`disasm({target:'project'})` puts exactly this call in `rebuild.json`. (For homebrew C that
|
|
361
|
+
ships fixed tile art, `linkerConfig:"chr-rom"` is the segment-split equivalent.)
|
|
362
|
+
|
|
363
|
+
**Readability caveats** (the bytes are ALWAYS correct; only instruction-vs-`.byte` coverage
|
|
364
|
+
varies): SNES and large Genesis ROMs come back byte-exact but DATA-ONLY (flat whole-ROM
|
|
365
|
+
disasm of a mostly-data image heals to `.byte` — meaningful coverage needs recursive
|
|
366
|
+
entry-point following, a known follow-up). GBA reads LOW because GBA C compiles mostly to
|
|
367
|
+
Thumb reached via an ARM crt0 stub, so an ARM-mode disasm decodes Thumb spans as `.byte`.
|
|
368
|
+
Banked-NES is the strongest case (~100% instructions); GB/GBC, SMS/GG, C64, Atari are also
|
|
369
|
+
near-100%.
|
|
370
|
+
|
|
371
|
+
## 8. Graphics swaps — PNG ↔ tiles round-trip
|
|
372
|
+
|
|
373
|
+
For sprite/tile edits (not text), don't hand-roll the tile-format math:
|
|
374
|
+
|
|
375
|
+
- **`tiles({op:'png', source:'path', platform, path, bank, paletteFromEmulator, paletteIndex})`** —
|
|
376
|
+
a source ROM's tiles → PNG. `bank:N` (NES 4 KB CHR bank) replaces magic file-offset math;
|
|
377
|
+
`paletteFromEmulator:true`+`paletteIndex` colors the export with the LIVE palette (vs
|
|
378
|
+
grayscale) so the art is recognizable to edit.
|
|
379
|
+
- **`importArt({from:'rom', sourceRom, sourcePlatform, sourceBank, sourceTileX/Y/W/H, targetPlatform,
|
|
380
|
+
outputPng, intent, paletteIndex})`** — one-call lift of a tile region from a source ROM into
|
|
381
|
+
the target platform's format (extract+crop+quantize). `intent:"homebrew"` reads the live
|
|
382
|
+
source palette; `intent:"rom-hack"` preserves source bytes verbatim.
|
|
383
|
+
- **`encodeArt({stage:'tiles', platform, pngBase64})`** → target-platform tile bytes.
|
|
384
|
+
- **`romPatch({op:'spliceCHR', path, platform, pngBase64, tileIndex, expect, bank, paletteHint})`** —
|
|
385
|
+
PNG → tile bytes → splice into CHR at tile slot N (auto-locates iNES CHR base; `expect`
|
|
386
|
+
checks the existing tile bytes; `paletteHint:["#RRGGBB",…]` gives explicit RGB→index
|
|
387
|
+
mapping). Composes the `encodeArt`+`romPatch({op:'write'})` step in one call.
|
|
388
|
+
- **`background({view:'rendered'})`** — at the current state, the set of tile IDs actually drawn
|
|
389
|
+
(BG nametable + OAM). Sample at title/gameplay/menu and diff the sets to map tile IDs to
|
|
390
|
+
assets without scanning sheets by eye. (`romPatch({op:'findFree'})` locates $FF/$00 runs for asm
|
|
391
|
+
overlays, longest-first.)
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
257
395
|
## Quick reference
|
|
258
396
|
|
|
259
397
|
| Goal | Tool |
|
|
@@ -276,4 +414,15 @@ and `input({op:'set'})` state it too.)
|
|
|
276
414
|
| Where did a VRAM graphic come from (Genesis) | `watch({on:'dma', precision:'sampled'})` (ROM offset of the DMA source) |
|
|
277
415
|
| Drive a menu fast | `input({op:'navigate'})` (advances on screen change) |
|
|
278
416
|
| Free RAM map for a known game | `cheats({op:'lookup'})` / `cheats({op:'search'})` |
|
|
417
|
+
| Apply a cheat live (non-destructive) | `cheats({op:'apply'})` (verify a label / fun) |
|
|
418
|
+
| Create a shareable code from a byte | `cheats({op:'make'})` (verified, all 14) |
|
|
419
|
+
| Read on-screen text's tile map | `text({op:'learn', fromScreen})` (live, no offset needed) |
|
|
420
|
+
| Find / encode a font-rendered string | `text({op:'find'})` → `text({op:'encode'})` |
|
|
421
|
+
| Assemble asm → raw patch bytes | `assembleSnippet({cpu, origin, code})` |
|
|
422
|
+
| Mapper-aware diff of two ROMs | `romPatch({op:'diff'})` (CPU addrs, CHR `tile:N`) |
|
|
423
|
+
| Who references this address (static) | `disasm({target:'references'})` (direct modes only) |
|
|
424
|
+
| Split / rebuild a ROM into parts | `cart({op:'extract'})` / `cart({op:'wrap'})` |
|
|
425
|
+
| Swap a sprite/tile (PNG round-trip) | `tiles({op:'png'})` → edit → `romPatch({op:'spliceCHR'})` |
|
|
426
|
+
| Lift art from another game's ROM | `importArt({from:'rom'})` |
|
|
427
|
+
| Tile IDs actually being drawn now | `background({view:'rendered'})` |
|
|
279
428
|
| Safe patch | `romPatch({op:'write'})`/`romPatch({op:'writeMany'})` with `expect` |
|
|
@@ -192,3 +192,40 @@ When you call `build({output:'rom', platform:"atari2600", source: ...})`:
|
|
|
192
192
|
2. The result is `.a26` — loadable in stella (`loadMedia`).
|
|
193
193
|
|
|
194
194
|
There's no linker — dasm produces a complete cart in one pass.
|
|
195
|
+
|
|
196
|
+
## MCP debug & inspection tooling
|
|
197
|
+
|
|
198
|
+
The 2600 runs on the **stella2014 (patched)** core. Because the 2600 has
|
|
199
|
+
no framebuffer and no standard sound chip, its inspectors look different
|
|
200
|
+
from the tilemap consoles — they decode the live TIA snapshot instead.
|
|
201
|
+
|
|
202
|
+
What you can read:
|
|
203
|
+
|
|
204
|
+
- **`palette({source:'live'})`** — the NTSC 128-color palette as a PNG,
|
|
205
|
+
with the *current* TIA background luma+hue extracted from the live
|
|
206
|
+
snapshot so you can see what color the beam is painting right now. This
|
|
207
|
+
is the same 128-entry `HHHHLLLL` palette the 7800 uses.
|
|
208
|
+
- **`sprites({op:'inspect'})`** — there is **no OAM** on the 2600, so this
|
|
209
|
+
returns the state of the 5 TIA graphics objects (P0, P1, M0, M1, Ball)
|
|
210
|
+
plus a current-scanline PNG showing how the TIA is composing that line.
|
|
211
|
+
- **`cpu({op:'read'})`** — the 6502 register file (A / X / Y / P / SP / PC)
|
|
212
|
+
pulled from the M6502 core's internal regs.
|
|
213
|
+
- **`background({view:'renderState'})`** — decodes the 32-byte TIA snapshot
|
|
214
|
+
into the playfield pattern, per-object enables/positions, and the color
|
|
215
|
+
registers.
|
|
216
|
+
- **`disasm({target:'rom'})`** and **`disasm({target:'references'})`** —
|
|
217
|
+
both anchor to the top of the bank (`$F000-$FFFF`) and label the vector
|
|
218
|
+
table (NMI / RESET / IRQ at `$FFFA`).
|
|
219
|
+
|
|
220
|
+
Memory regions for **`memory({op:'read'})`**:
|
|
221
|
+
|
|
222
|
+
| Region | Size | What it is |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| `system_ram` | 128 bytes | the RIOT RAM — that's the *entire* console RAM |
|
|
225
|
+
| `a26_tia_regs` | 32 bytes | the live TIA register snapshot |
|
|
226
|
+
| `a26_cpu_regs` | 7 bytes | the 6502 register snapshot |
|
|
227
|
+
|
|
228
|
+
**No `audioDebug` inspector.** The 2600's sound comes from the two TIA
|
|
229
|
+
audio voices (`AUDC/AUDF/AUDV` at `$15-$1A`), not a standard PSG/FM chip,
|
|
230
|
+
so there's no `audioDebug` decode — read the audio state directly out of
|
|
231
|
+
the `a26_tia_regs` snapshot instead.
|
|
@@ -322,3 +322,39 @@ When you call `build({output:'rom', platform:"atari7800", language:"c"})`:
|
|
|
322
322
|
3. ld65 links + atari7800.cfg → flat `.a78` ROM.
|
|
323
323
|
|
|
324
324
|
Loadable via prosystem (`loadMedia`).
|
|
325
|
+
|
|
326
|
+
## MCP debug & inspection tooling
|
|
327
|
+
|
|
328
|
+
The 7800 runs on the **prosystem (patched)** core. Like the 2600 it has
|
|
329
|
+
no framebuffer and no tile/sprite-attribute tables, so its inspectors
|
|
330
|
+
decode MARIA's display-list machinery rather than a tilemap.
|
|
331
|
+
|
|
332
|
+
What you can read:
|
|
333
|
+
|
|
334
|
+
- **`palette({source:'live'})`** — a 256-color master palette PNG, with
|
|
335
|
+
the live MARIA palette block at `$20-$3F` decoded into the 8 palettes ×
|
|
336
|
+
3 colors each, plus the backdrop. This is the Atari NTSC palette shared
|
|
337
|
+
with the 2600.
|
|
338
|
+
- **`sprites({op:'inspect'})`** — there is **no OAM** on the 7800. Instead
|
|
339
|
+
this returns the MARIA control registers and the **DPP** display-list-
|
|
340
|
+
list pointer, leaving the agent to walk the DLL → DL hierarchy itself
|
|
341
|
+
(the same structure described under "MARIA: the unusual one" above).
|
|
342
|
+
- **`cpu({op:'read'})`** — the 6502 ("Sally") register file (A / X / Y /
|
|
343
|
+
P / SP / PC) read from prosystem's `sally` globals.
|
|
344
|
+
- **`background({view:'renderState'})`** — the MARIA CTRL bits, DPP,
|
|
345
|
+
CHARBASE, and the current `dlistPtr`.
|
|
346
|
+
- **`disasm({target:'rom'})`** and **`disasm({target:'references'})`** —
|
|
347
|
+
both default to the top 16 KB (`$C000-$FFFF`), where the reset vector
|
|
348
|
+
lands.
|
|
349
|
+
|
|
350
|
+
Memory regions for **`memory({op:'read'})`**:
|
|
351
|
+
|
|
352
|
+
| Region | Size | What it is |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| `system_ram` | 64 KB | the *entire* 6502 address space — MARIA regs, RAM, and ROM are all visible through this one region |
|
|
355
|
+
| `a78_cpu_regs` | — | the 6502 register snapshot |
|
|
356
|
+
|
|
357
|
+
**No `audioDebug` inspector.** The 7800's standard audio is the same TIA
|
|
358
|
+
chip carried over from the 2600 (`$15-$1A`), not a decodable PSG/FM chip,
|
|
359
|
+
so there's no `audioDebug` decode. (Some carts add a POKEY, but it's
|
|
360
|
+
non-standard — don't assume it's present.)
|
|
@@ -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,55 @@ 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
|
+
**Script the whole startup in one call.** `recordSession`'s `inputScript` takes
|
|
138
|
+
C64 `keys` alongside joystick `ports` — held from each entry until the next — so a
|
|
139
|
+
key+joystick startup is one deterministic timeline instead of many calls:
|
|
140
|
+
```js
|
|
141
|
+
recordSession({ frames:600, sampleEvery:60, outputDir:'…', inputScript:[
|
|
142
|
+
{ atFrame:0, keys:['f1'] }, // 1 player
|
|
143
|
+
{ atFrame:30, ports:[{ b:true }] }, // fire (port 2)
|
|
144
|
+
{ atFrame:90, keys:['run/stop'] }, // start
|
|
145
|
+
{ atFrame:120, keys:[] } ] }) // release
|
|
146
|
+
```
|
|
147
|
+
A step may set just `keys`, just `ports`, or both; `keys:[]` releases all. An
|
|
148
|
+
unknown key is rejected with a clear error (not a silent no-op).
|
|
149
|
+
|
|
150
|
+
**When a key seems ignored, probe it.** `input({op:'pressKey', key:'run/stop',
|
|
151
|
+
verify:true})` returns the matrix coords, active joyport, and a CIA1 snapshot
|
|
152
|
+
(`$DC00`/`$DC01`) **before / during (held) / after**. `before==during` ⇒ the key
|
|
153
|
+
never moved the matrix line (didn't reach VICE); they differ but the game didn't
|
|
154
|
+
react ⇒ it scanned a different key/port, or that screen (crack/doc/trainer)
|
|
155
|
+
ignores it. This is how you tell a romdev problem from a game-flow problem.
|
|
156
|
+
|
|
157
|
+
**Controller-alone (playtest):** a human in `playtest` needs no keyboard — the
|
|
158
|
+
pad's spare buttons map to the C64 keys (X=Space, L2=Run/Stop, R2=Return,
|
|
159
|
+
right-stick=F1/F3/F5/F7, top face=F1; d-pad+Fire=joystick). The same mapping
|
|
160
|
+
applies to the agent's `setInput`, so e.g. `input({op:'set', c64_f1:true})` also
|
|
161
|
+
presses F1. `playtest({op:'open'})` returns `c64Controls` to relay to the user.
|
|
162
|
+
|
|
163
|
+
⚠ A cc65 `.prg` only starts after BASIC auto-`RUN`s it — step ~70+ frames past
|
|
164
|
+
load before input/reads register.
|
|
116
165
|
|
|
117
166
|
## SID — three voices of fame
|
|
118
167
|
|
|
@@ -131,6 +180,54 @@ the `sid_play.s` starter snippet.
|
|
|
131
180
|
waveform, freq→note, pulse-width, ADSR — plus the filter cutoff/resonance/mode.
|
|
132
181
|
Handy for verifying a `sid_play` routine is actually gating notes.
|
|
133
182
|
|
|
183
|
+
## MCP debug & inspection tooling
|
|
184
|
+
|
|
185
|
+
romdev runs the C64 on a patched VICE (`vice_x64`) core that exposes deep live
|
|
186
|
+
state. The inspectors all read the **running** machine — use them to confirm what
|
|
187
|
+
your code actually did to the hardware, not what you think it did.
|
|
188
|
+
|
|
189
|
+
**Live visual / state inspectors:**
|
|
190
|
+
|
|
191
|
+
- `palette({source:'live'})` — the 16-color hardware-fixed C64 palette as a PNG,
|
|
192
|
+
plus the current border / background / extra-background color indices decoded
|
|
193
|
+
straight from the VIC-II registers.
|
|
194
|
+
- `sprites({op:'inspect'})` — all 8 MOBs decoded into the generic sprite shape:
|
|
195
|
+
X/Y, color, multicolor flag, expand-X / expand-Y, priority, AND the screen-RAM
|
|
196
|
+
sprite-data pointers at `$07F8` so you can locate each sprite's pixel block in
|
|
197
|
+
the VIC bank.
|
|
198
|
+
- `cpu({op:'read'})` — the 6510 registers (A/X/Y/P/SP/PC) read from a live,
|
|
199
|
+
`#define`-aliased register file, plus the `$0001` I/O port decoded into its
|
|
200
|
+
LORAM / HIRAM / CHAREN bits (so you can see which ROMs are banked in).
|
|
201
|
+
- `background({view:'renderState'})` — the VIC-II registers decoded into
|
|
202
|
+
mode / scroll / colors / sprites, the VIC bank resolved from CIA2 `$DD00`, and
|
|
203
|
+
the **absolute** screen-RAM + character-base addresses (no manual bank math).
|
|
204
|
+
- `audioDebug({op:'inspect', chip:'sid'})` — the SID voice/filter decode covered
|
|
205
|
+
in the SID section above.
|
|
206
|
+
|
|
207
|
+
**`c64_*` memory regions** (via `memory({op:'read'})`) — exact, named windows
|
|
208
|
+
onto the hardware, decoded live:
|
|
209
|
+
|
|
210
|
+
| region | size | notes |
|
|
211
|
+
| --------------- | ----- | ----------------------------------------------- |
|
|
212
|
+
| `system_ram` | 64 KB | full RAM |
|
|
213
|
+
| `c64_color_ram` | 1 KB | the nibble color RAM |
|
|
214
|
+
| `c64_vic_regs` | 64 B | VIC-II registers |
|
|
215
|
+
| `c64_sid_regs` | 29 B | SID registers (read via `sid_peek`) |
|
|
216
|
+
| `c64_cia1_regs` | 16 B | CIA1, from the `c_cia[]` array |
|
|
217
|
+
| `c64_cia2_regs` | 16 B | CIA2, from the `c_cia[]` array |
|
|
218
|
+
| `c64_cpu_regs` | 7 B | 6510 register file |
|
|
219
|
+
|
|
220
|
+
**Disassembly:** `disasm({target:'rom'})` and `disasm({target:'references'})`
|
|
221
|
+
accept `.prg` files (they understand the 2-byte little-endian load-address
|
|
222
|
+
header), and apply the C64 register-annotation table so VIC-II / SID / CIA
|
|
223
|
+
register accesses come back named rather than as bare addresses.
|
|
224
|
+
|
|
225
|
+
**Starter snippets** cover `vic_init` / `sprite_table` / `sid_play` /
|
|
226
|
+
`read_joystick` / `basic_stub`.
|
|
227
|
+
|
|
228
|
+
Disk-image loading and disk SAVE/restore tooling have their own sections below
|
|
229
|
+
("Disk images" and "Disk SAVES").
|
|
230
|
+
|
|
134
231
|
## Cartridge / load file format
|
|
135
232
|
|
|
136
233
|
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
|