Promotes Lunar Pool
This commit is contained in:
1 parent
837007843d
commit
62866563ba
53 files changed
+115746
-4100
No files matched your search
@@ -0,0 +1,268 @@
|
||||
# Lunar Pool (U)
|
||||
|
||||
ROM: `Lunar Pool (U).nes` (24,592 bytes, iNES). Status: **PASSES 1-3 COMPLETE; PROMOTED TO THE TOP LEVEL 2026-10-18** — 5,300 instructions / **11,077 code bytes (67.6%)** / 5,301 data bytes / the 6 vector bytes as `.word`, byte accounting exact (11,077 + 5,301 + 6 = 16,384), and **BYTE-IDENTICAL for the whole ROM** (header + PRG + CHR) via `cmp reconstructed.nes rom.nes`. Pass 2 named all **705** labels — `Label_<addr>` is gone — and Pass 3 named 33 data structures and finished the zero-page audit (38 semantic aliases, 66 deliberately neutral `ZpXX`, ten former phantom aliases deleted). The ~354 bytes the CDL could not settle stay `.byte` on purpose; they are itemised under *Known remaining gaps*.
|
||||
|
||||
|
||||
## ROM Facts
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Mapper | 0 (NROM) — from the iNES header |
|
||||
| PRG | 16 KB (header claims map at CPU $C000–$FFFF) |
|
||||
| CHR | 8 KB |
|
||||
| Mirroring | Vertical |
|
||||
| Battery-backed flag | clear |
|
||||
| ROM SHA-256 | `6da37fd34e8f4eb3df34e9f446726a193db77d0e0bba32aa328ef225993fc8a1` |
|
||||
| PRG SHA-256 | `43a2f54cd48b38fc1e4a33051e019528e9789a774ace773fa7549c33dc9492c9` |
|
||||
|
||||
## Interrupt Vectors (taken from PRG $3FFA–$3FFF)
|
||||
|
||||
| Vector | Target | Note |
|
||||
|--------|--------|------|
|
||||
| NMI | `$FF65` | per-frame heartbeat |
|
||||
| RESET | `$E80C` | non-$C000 entry — the tracer has to seed it explicitly (`--extra-seed 0xE80C`) |
|
||||
| IRQ | `$FF64` | a bare `rti` sitting immediately before the NMI handler, so IRQs are effectively ignored |
|
||||
|
||||
## Pass Status
|
||||
|
||||
| Pass | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| Pass 0 | Superseded | `prg.s` was a 12-line `.incbin` of the whole PRG — no disassembly at all |
|
||||
| Pass 1 | **Complete** | Static code/data split, then reclassified against a Mesen Code Data Logger capture (see below). As Pass 1 delivered it: 5,285 instructions / **11,041 bytes emitted as instructions (67.4%)** / 5,343 bytes emitted as data (`.byte`, plus the six vector bytes as `.word`); 11,041 + 5,343 = 16,384 exactly. Labels: 640 `Label_<addr>` plus `Reset`/`NmiHandler`/`IrqHandler`. |
|
||||
| Pass 2 | **Complete** | NES hardware constants in `symbols.inc`; zero-page aliases in `locals.inc`; and **all 705 labels named — `Label_<addr>` is gone from the file**. 17 dispatch targets that carried no label at all (`UpdateObjects_Render_Sound` `$C05C`, `SetupRound` `$EB60`, `PrintStageNumber` `$E21A`, `SetPpuAddr` `$F989`, …) were given labels and routine headers. Naming rule used: PascalCase for `jsr`/`jmp` targets and logical block heads, `<Block>_<Role>` (`_Loop`, `_Next`, `_Alt`, `_Done`, `_Skip`, `_Return`, `_Store`, …) for targets *inside* a routine; `jsr`/`jmp` operands stay raw numeric — this file's convention, as in `Joypad Test Cartridge (U)` — while branch operands are symbolic. (Earlier revisions claimed 218 entries renamed, 195 of them `SubC<addr>`; no `SubC` label ever existed — `git log -S SubC` finds no commit that added one.) |
|
||||
| Pass 3 | **Complete** | Zero-page audit: **38 semantic aliases** (object core, VRAM write queue, VRAM read-back, PPU helpers) and **66 neutral `ZpXX`** whose roles are documented as deliberately unproven in `locals.inc`. Data structures: **33 tables/blocks named** (table below). Vectors are `.word NmiHandler, Reset, IrqHandler`. What is still unnamed is the residue listed under *Known remaining gaps* — it stays `.byte` on purpose. |
|
||||
|
||||
## Pass 3 — the named data structures
|
||||
|
||||
33 tables and blocks were named. The code in this ROM bases several tables one entry
|
||||
early (`lda $C478,y`, `lda $D4F8,y`, `lda $E9B9,x`), so entry 0 is dead and the label
|
||||
sits on the first *live* entry; that is noted per table in `prg.s`.
|
||||
|
||||
| Address | Label | What it is |
|
||||
|---------|-------|-----------|
|
||||
| `$C000` | `ResetStub` | Unreachable three-byte `jmp Reset` — the reset vector points at `$E80C` directly |
|
||||
| `$C0C0` | `ObjectOpcodeHandlerTable` | 19 words: object-script opcodes `$80`-`$92` → handler address − 1 |
|
||||
| `$C3B4` | `SoundChannelMask` | `01 02 04 08 FE FD FB F7` — APU channel mask/enable bytes |
|
||||
| `$C47A` | `ObjectStatePointerTable` | 84 words: per-object state pointers, indexed by `(obj_ptr_lo + $0519,x) * 2` |
|
||||
| `$C522` | `SpawnObjectTable` | 16 words: spawn-record pointers for ids 1-15 (id 0 dead), picked from the `$0060-$0067` queue |
|
||||
| `$C543` | `StateTimerTable` | Per-state timers `1,2,3,4,6,8,12,16,24,32,48,64,96,128,192,0`, indexed by script opcode − `$DF` |
|
||||
| `$C552` | `AnimationPointerTable` | Frame-sequence pointers |
|
||||
| `$C559` | `SpriteAnimationTable` | Per-sprite animation descriptors used by the draw path |
|
||||
| `$CF40` | `VsCommandList` | Inline `RunCommandList` script issued when loading a VS-COMP scene |
|
||||
| `$D057` | `DrawCellRun_TileTable` | Corner tiles `$3C $69 $C3 $96` selected by row bits in `DrawCellRun` |
|
||||
| `$D4FA` | `LayoutScriptTable` | 30 words: per-stage layout-script pointers |
|
||||
| `$D559` | `LayoutData` | 2,757 bytes of per-stage layout records |
|
||||
| `$E862` | `BootSignatureText` | `"JEMINI DAZO!!"` plus the warm-boot signature compared against the RAM copy at `$0440-$044F`; the title attribute row at `$E870-$E87F` overlaps its tail |
|
||||
| `$E9A3` | `TitleColorCycleTable` | 22 colour codes cycled into the `$0085-$0087` palette shadow each frame |
|
||||
| `$E9B9` | `StartGameAttributeRow` | 16 attribute bytes copied to the `$0140-$014F` shadow when a game starts |
|
||||
| `$EA00` | `TitleCommandList` | Inline `RunCommandList` script for the title screen |
|
||||
| `$EC2A` | `StageAttributeTable` | 16 per-stage attribute bytes copied to the attribute shadow |
|
||||
| `$ED3F` | `SpritePaletteTable` | 4 × 4 sprite palettes, copied to the `$0090-$009F` palette shadow by `Reset_LoadSpritePalettes` |
|
||||
| `$EEE9` | `BallAppearTiles` | Tiles for the ball-appear step animation (indexed by the step at `$0340`) |
|
||||
| `$EF2A` | `BallSinkTiles` | Tiles for the ball-sink step animation |
|
||||
| `$F098` | `TileCollisionTable` | 27 words: collision flags per tile |
|
||||
| `$F5E4` | `EndingCommandList` | Inline `RunCommandList` script for the ending screen |
|
||||
| `$F762` | `AimPhaseTable0` | Three parallel 8-byte colour sequences shimmered into the `$008B`/`$008F`/`$0092` palette shadow while aiming |
|
||||
| `$F786` | `StrPlayer1`, `StrShot`, `StrPlayer2`, `StrGameOver`, `StrPlayer3`, `StrHiScore`, `StrPerfect` | The seven `jsr $CBDC` inline strings ($F786-$F861) |
|
||||
| `$FAC9` | `PaletteColorTable` | 12 rows of 4 colour codes → PPU palette entries (`MapColorCodeToPalette`) |
|
||||
| `$FECE` | `SineTable` | 64 entries read by `GetSine` |
|
||||
| `$FF90` | `PaddingFf` | 106 bytes of `$FF` filler between the last table and the vectors |
|
||||
|
||||
|
||||
|
||||
The Pass 1 split came from a static trace only, and a static trace cannot see code that
|
||||
is reached through computed dispatch, nor can it tell inline string data from an
|
||||
instruction that happens to decode from the same bytes. Both errors were present.
|
||||
|
||||
Ground truth came from the emulator: Mesen's Code Data Logger marks every byte the CPU
|
||||
**fetched as an opcode** (Code) and every byte it **read as data** (Data). Playing the
|
||||
ROM through title → 1P → game over → VS COMP marked 10,028 PRG bytes Code (61.2%) and
|
||||
1,534 Data; that is the classification oracle for every byte it observed. Where the CDL
|
||||
is silent the previous classification is kept, never guessed.
|
||||
|
||||
| | code bytes | data bytes |
|
||||
|---|---|---|
|
||||
| As originally emitted by Pass 1 | 9,403 | 6,981 |
|
||||
| After the CDL reclassification | **11,041** | **5,343** |
|
||||
|
||||
1,738 bytes that Pass 1 had emitted as `.byte` are executed code and are now
|
||||
instructions; 100 bytes it had emitted as instructions are read as data and are now
|
||||
`.byte`.
|
||||
|
||||
The result reconciles with the CDL exactly, with no disagreement in either direction:
|
||||
|
||||
| emitted as | CDL says Code | CDL says Data | CDL never saw it |
|
||||
|---|---:|---:|---:|
|
||||
| instructions | 10,028 | **0** | 1,013 |
|
||||
| data (`.byte`/`.word`) | **0** | 1,534 | 3,809 |
|
||||
|
||||
Every byte the CDL observed is therefore classified the way the CPU used it. The 1,013
|
||||
instruction bytes the CDL never saw are Pass 1 static code plus the two inference steps
|
||||
below; the 3,809 unseen data bytes are the residue — 45 of them carry an undocumented
|
||||
opcode's disassembly in the audit comment (Pass 1 read them as code statically, but the
|
||||
CDL never watched the session fetch them, and ca65 V2.19 cannot encode those opcodes
|
||||
anyway, so they stay `.byte` with the disassembly recorded).
|
||||
|
||||
As an independent structural check, all 1,183 control-flow targets in the file — 633
|
||||
`jsr`/`jmp` operands and 550 branch targets — land exactly on an instruction boundary;
|
||||
none points into an operand or a `.byte` run.
|
||||
|
||||
The rewrite is reproducible with `_tools/cdl_reclass.py` (dry run by default). Capture a
|
||||
CDL from the Mesen MCP with
|
||||
`mesen_cdl_file(action:'save', memoryType:'NesPrgRom', filepath:'lunar.cdl')` — the
|
||||
capture is cumulative, so it does not reset — then:
|
||||
|
||||
```bash
|
||||
python3 _tools/cdl_reclass.py --dir "copilot" --cdl lunar.cdl \
|
||||
--override 0xC0C0-0xC0E5:D \
|
||||
--override 0xF81A-0xF825:C --override 0xF826-0xF82E:D --override 0xF82F-0xF83A:C \
|
||||
--override 0xF83B-0xF846:D --override 0xF847-0xF84B:C --override 0xF84C-0xF857:C \
|
||||
--override 0xF858-0xF861:D --override 0xF862-0xF866:C
|
||||
```
|
||||
|
||||
Its output differs from this `prg.s` only in this provenance header, the `.word` vector
|
||||
split, and three `jsr Label_CAA3` → `jsr $CAA3` operand spellings (this file spells
|
||||
`jsr`/`jmp` operands as raw addresses and branch operands as `Label_<addr>`). Always run
|
||||
`make` afterwards.
|
||||
|
||||
Two inference steps were needed beyond the CDL because the capture never reached the
|
||||
hi-score screen. They are marked as inference, not ground truth:
|
||||
|
||||
* `$C0C0–$C0E5` is a 19-entry little-endian 16-bit pointer table (read as
|
||||
`lda $C0C0,y` / `lda $C0C1,y`). Pass 1 had decoded it as a `cmp (ZpXX,x)` chain; the
|
||||
CDL only observed the few entries the current game state walked. Code resumes at
|
||||
`$C0E6` (`tax`), which is a real branch target from `$C101`.
|
||||
* `$F81A` (hi-score) and `$F84C` ("perfect") are text routines. They are never reached
|
||||
in normal play until a hi-score is beaten, so the CDL is silent; their inline-string
|
||||
structure is inferred from the CDL-confirmed twins `$F77A` and `$F7E8`.
|
||||
|
||||
### The inline-string dispatcher (this is not a jump gate)
|
||||
|
||||
`$CBDC` (and its variant `$CBC9`, which also clears the PPU destination first) is a
|
||||
**print-inline-string-and-return-after-it** helper:
|
||||
|
||||
```
|
||||
$CBDC pla / sta ptr_lo ; pop the caller's return address = the string address
|
||||
$CBDF pla / sta ptr_hi
|
||||
$CBE2 jsr $CB5A ; prepare the PPU destination
|
||||
$CBE5 ldy #$00
|
||||
$CBE7 lda (ptr_lo),y ; next character
|
||||
$CBE9 beq $CBF4 ; $00 terminates
|
||||
$CBEB jsr $CC29 ; emit it (advances ptr_lo/hi)
|
||||
$CBEE jsr $CB5A
|
||||
$CBF1 jmp $CBE5
|
||||
$CBF4 lda ptr_hi / pha ; push the address of the byte after the $00 ...
|
||||
$CBF7 lda ptr_lo / pha
|
||||
$CBFA rts ; ... and rts to it
|
||||
```
|
||||
|
||||
So every `jsr $CBDC` in the image is immediately followed by a `$00`-terminated string
|
||||
that begins with the control byte `$81` (`$81` also acts as the word separator), and
|
||||
execution resumes after the terminator:
|
||||
|
||||
| site | bytes | text |
|
||||
|------|-------|------|
|
||||
| `$F786` | `$81player$81$00` | player |
|
||||
| `$F79B` | `$81shot$81$00` | shot |
|
||||
| `$F7F4` | `$81player$81$00` | player |
|
||||
| `$F809` | `$81game$81over$81$00` | game over |
|
||||
| `$F826` | `$81player$81$00` | player |
|
||||
| `$F83B` | ``$81hi`score__$00`` | hi`score__ |
|
||||
| `$F858` | `$81perfect_$00` | perfect_ |
|
||||
|
||||
**Earlier revisions of this document claimed a `jmp ($7961)` RAM-pointer dispatch at
|
||||
`$F788`/`$F7F6`/`$F828`. That was a flat-tracer phantom.** The bytes after `jsr $CBDC`
|
||||
are the strings above; the decoder happened to start an instruction two bytes into
|
||||
`player`, reading `$6C $61 $79` as `jmp ($7961)` — the operand is the middle of the word
|
||||
"player". Nothing in the ROM ever writes `$7961`/`$7962`, and `$6000–$7FFF` is unmapped
|
||||
on NROM, so such a jump could not work. The three sites are gone from the classification;
|
||||
no instruction in the corrected file is an indirect `jmp`.
|
||||
|
||||
The genuine computed dispatch that remains is small and resolved. `$CBE5` re-reads its
|
||||
string pointer through `ptr_lo`/`ptr_hi` and re-enters its loop with it — plain code.
|
||||
`$F193` does the reverse: it pushes `Zp17`/`Zp16` and `rts`es, so a caller sets that pair
|
||||
and the `rts` transfers control to it; the pattern is named in `locals.inc` and every
|
||||
target is inside CDL-covered code.
|
||||
|
||||
### Code the CDL proved but Pass 1 missed (largest runs)
|
||||
|
||||
| Range | Bytes | What it is |
|
||||
|-------|------:|-----------|
|
||||
| `$EA00–$EBAF` | 432 | VS-COMP / select-screen logic (`$EB16` is still unreached and stays data) |
|
||||
| `$EBF3–$ED4E` | 348 | menu/round state machine |
|
||||
| `$CE35–$CF9A` | 358 | collision/entity maths |
|
||||
| `$D4FA–$E01D` | 2,852 | `$D4FA` 30-entry pointer table (`$D4FA–$D535`), the 35-byte code helper at `$D536–$D558`, then variable-length records from `$D559` |
|
||||
| `$F897–$F98F` | 249 | high-score / attract handling |
|
||||
| `$C0F2–$C18C` | 155 | object update chain (branches back to `$C0E6`) |
|
||||
| `$D325–$D391`, `$D2A0–$D2D3`, `$D1BB–$D209` | 240 | further record/table walkers |
|
||||
|
||||
### Tracer fixes required during Pass 1
|
||||
|
||||
Three undocumented opcodes were missing from `_tools/nesdis.py` and were added, with the
|
||||
`--undocumented` switch (default off, so existing `make regen` pipelines stay byte-stable):
|
||||
|
||||
* `$72` — `RRA abs` (the `RRA` family had `$63`/`$67`/`$6F`/`$73`/`$77`/`$7B`/`$7F` but not `$72`).
|
||||
* `$42` — `NOP #imm` (the family had `$80`/`$82`/`$89`/`$C2`/`$E2` but not `$42`).
|
||||
* `$FA` — `NOP abs,X` (the family had `$1C`/`$3C`/`$5C`/`$7C`/`$DC`/`$FC` but not `$FA`).
|
||||
|
||||
One emitter fix: ca65 silently rewrites an operand whose high byte is `$00` into the
|
||||
shorter zero-page form (`sta $00FF,x` becomes `sta $FF,x`, emitting `$95` instead of
|
||||
`$9D`). `render()` now emits the `a:` prefix for ABS/ABSX/ABSY/IND operands with a high
|
||||
byte of `$00`, forcing the absolute form so the bytes match the ROM. `operand_value()`
|
||||
strips the prefix when parsing.
|
||||
|
||||
## Known remaining gaps
|
||||
|
||||
* The static tracer reported two overlaps (`$C3BB` — resolved with `--data 0xC3BB:1` —
|
||||
and `$F0EF`) and three rejected mis-aligned stubs. The residue that remains is the
|
||||
CDL's: 5,343 bytes (32.6%) are still emitted as data (`.byte`, plus the six vector
|
||||
bytes as `.word`).
|
||||
* Unreached in the CDL session, therefore still unclassified: the hi-score entry screen
|
||||
(`$F81A`+ / `$F84C`+ are inferred by symmetry, above), the 2-player-only branches, and
|
||||
`$EB16` (a poll loop behind `beq` at `$EB11`, emitted as a raw `beq *+$05`).
|
||||
* `locals.inc` carries 104 aliases: **38 semantic** (see the Pass 3 notes at the foot of
|
||||
the file) and 66 neutral `ZpXX`. Ten further aliases were deleted by the
|
||||
reclassification: they were phantoms — `$49`/`$84`/`$8A`/`$AA`/`$C0` came from the
|
||||
mis-decoded `$C0C0` table, and `$3B`/`$65`/`$67`/`$72`/`$81` were ASCII characters of
|
||||
the inline strings read as operands.
|
||||
|
||||
## `copilot/` Contents
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Lunar Pool (U).nes` | original ROM image (reference) |
|
||||
| `prg.bin` / `header.bin` / `chr.bin` | split image, produced by `_tools/splitnes.py` |
|
||||
| `prg.s` | disassembly: 5,285 instructions with per-instruction address/byte audit comments, unclassified bytes kept as `.byte`/`.word` |
|
||||
| `symbols.inc` | shared NES hardware constants (PPU/APU/controller, from the skeleton) |
|
||||
| `locals.inc` | project-local zero-page aliases — 38 semantic, 66 neutral `ZpXX`; Pass 3 evidence notes at the foot of the file |
|
||||
| `linker.cfg` | PRG at $C000, 16 KB, `fill = yes` |
|
||||
| `Makefile` | links `reconstructed_prg.bin` and `cmp`s it against `prg.bin` |
|
||||
| `reconstructed_prg.bin` | build output from the last verified build |
|
||||
|
||||
Header/CHR are reproduced verbatim from `header.bin`/`chr.bin` (not linked): `make`
|
||||
verifies **PRG bytes only** at this stage, which is exactly what a skeleton proves. When
|
||||
this project is promoted to a full disassembly, `header.s` / `chr.s` (as e.g.
|
||||
`Burger Time (U)`) and a full-ROM `make` target get added.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cd "_wip/Lunar Pool (U)/copilot"
|
||||
make # → cmp reconstructed_prg.bin prg.bin → "BYTE-IDENTICAL"
|
||||
```
|
||||
|
||||
## Next Steps (per `PROMPTS.md`)
|
||||
|
||||
1. **Pass 1** — complete. The split is CDL-verified; the remaining `.byte` runs are either
|
||||
data (string/table/record bytes) or CDL-unreached code (above).
|
||||
2. **Pass 2** — partial: the hardware constants and the zero-page aliases are named. The
|
||||
remaining Pass 2 work is routine-label renaming — all 640 entries are address-derived
|
||||
`Label_<addr>`, and `git log -S SubC` shows this was never done for this project.
|
||||
3. **Pass 3** — in progress. Zero-page audit complete (38 semantic + 66 neutral), and the
|
||||
`$FFFA–$FFFF` vectors are now `.word NmiHandler, Reset, IrqHandler`.
|
||||
Remaining: name the data tables and structures — the `$D4FA` 30-entry pointer table and
|
||||
the variable-length records at `$D559+`, the `$C0C0` entry-point table, the
|
||||
`$C47A–$CA20` block.
|
||||
4. **Promotion** — move to a top-level `<name>/copilot/` folder, add a full-ROM build
|
||||
(header.s + chr.s + vectors), and write the power-on flow `OVERVIEW.md` in the
|
||||
established style; then it joins the root index's numbered inventory.
|
||||
Reference in new issue
Block a user