Promotes Lunar Pool

This commit is contained in:
tmerritt committed 2026-10-18 16:47:03 -04:00
1 parent 837007843d
commit 62866563ba
53 files changed
+115746 -4100

No files matched your search

+148 -18
View File
@@ -17,18 +17,26 @@ A build that assembles but does not print that message is a failure. Requires
Two tiers exist, and the difference is real:
- **Top level (16 projects)** — `make` compares the whole ROM against `copilot/rom.nes`.
- **`_wip/` (20 projects)** — `make` only runs `cmp reconstructed_prg.bin prg.bin`.
- **Top level (17 projects)** — `make` compares the whole ROM against `copilot/rom.nes`.
- **`_wip/` (19 projects)** — `make` only runs `cmp reconstructed_prg.bin prg.bin`.
Header and CHR are **not** proven for those. Promotion means adding
`copilot/rom.nes`, switching to a whole-ROM compare, and changing
`../../../_tools/` to `../../_tools/` in the Makefile.
`../../../_tools/` to `../../_tools/` in the Makefile. A promoted project also
needs `header.s`, `chr.s` and a HEADER/PRG/CHR `linker.cfg`, which a Pass-0
skeleton does not carry — `Lunar Pool (U)` (2026-10-18) is the worked example.
## Current state
- 16 top-level projects, all byte-identical.
- 20 `_wip/` projects, all PRG-identical. `Pyramid (U)` is the only one with Pass 1.
- 17 top-level projects, all byte-identical.
- 19 `_wip/` projects, all PRG-identical. `Pyramid (U)` is the only one with Pass 1.
- 9 commits ahead of `origin/master`, nothing pushed.
- `_tools/verify_pass1.py` rebuilds and re-traces everything: **all 36 faithful, none truncated.**
- `_tools/verify_pass1.py` rebuilds and re-traces everything: **all 36 faithful, none
truncated** — but its *re-trace* half now crashes on `MUSCLE (U)` and
`Donkey Kong Jr. (JU)` (`ValueError: ... int() with base 16: ':$0010'` / `':$0002'`)
because `nesdis.trace()` parses operands with `int(text[1:], 16)` while the emitter
writes the `a:` prefix for ABS/ABSY/ABSX/IND operands with a `$00` high byte. The
`make` half still runs. Fixing it needs `nesdis.py`, which feeds `make regen`, so
MUSCLE regen must be re-checked afterwards.
```bash
python3 _tools/verify_pass1.py # all projects
@@ -88,29 +96,125 @@ Resolving the pushed table addresses is the key to the remaining 28%.
phase dispatcher. Both were found by probing, which is a cheap technique worth
reusing on other projects.
### 2b. Lunar Pool (U) — Pass 1 complete, Pass 2/3 outstanding
### 2b. Lunar Pool (U) — Pass 2 and Pass 3 complete, promoted to the top level
`_wip/Lunar Pool (U)/`. Pass 1 complete: **57.8% code** (4,586 instructions,
9,475 bytes), byte accounting exact, BYTE-IDENTICAL.
`Lunar Pool (U)/` (promoted out of `_wip/` 2026-10-18 by `git mv`, so the relative
`copilot/rom.nes` symlink is live and tool paths are `../../_tools/`). Pass 1
complete: **67.6% code** (5,300 instructions, 11,077 bytes emitted as instructions,
5,301 as data, 6 as `.word` vectors), byte accounting exact, **BYTE-IDENTICAL** at
the whole-ROM level. The split was re-derived against a Mesen Code Data Logger
capture (10,028 bytes Code = 61.2%); the flat tracer had undercounted by 1,738 bytes
of executed code and overcounted 100 bytes of data as instructions.
**Promotion detail worth reusing:** a Pass-0 skeleton has no `header.s`, `chr.s` or
HEADER/PRG/CHR `linker.cfg`, so promotion is not just a symlink and a `cmp` edit —
copy the shape from `Burger Time (U)/copilot/` (`header.s`/`chr.s` are one-line
`.incbin`s; the linker needs `HEADER: start = $0000, size = $0010` before PRG).
Result here: `reconstructed.nes` 24,592 bytes, SHA-256 `6da37fd3…c8a1`, equal to the
original `Lunar Pool (U).nes`.
- RESET is at **`$E80C`**, not `$C000` — the tracer must seed it explicitly
(`--extra-seed 0xE80C`), since the vectors are the only entry points a plain
trace follows.
- Code is gated behind two dispatch mechanisms, both of the Mario Bros shape:
`jmp ($7961)` at three sites (`$F788`/`$F7F6`/`$F828`, RAM-pointer dispatch —
the data blocks after them hold the screen text "game over" and "hi`score__"),
and stack-based dispatch at `$CBDC` (pointer arrives via `pla`).
- 6,909 bytes (42.2%) still data. ~585 `Label_Cxxxx` to name in Pass 2.
- **Correction to an earlier claim in this file and in the two `OVERVIEW.md`s:**
there is no `jmp ($7961)` RAM-pointer dispatch at `$F788`/`$F7F6`/`$F828`. Those
were flat-tracer phantoms. `$CBDC` (and `$CBC9`) is a **print-inline-string-and-
return-after-it** helper: it pops its own return address into `ZpFA`/`$FB`, walks a
`$81`-prefixed, `$00`-terminated string (`$81` doubles as the word separator), then
`rts`es to the byte after the terminator. So a `jsr $CBDC` is always followed by text,
and the sites spell `$F786` "player", `$F79B` "shot", `$F7F4` "player", `$F809`
"game over", `$F826` "player", `$F83B` "hi`score__", `$F858` "perfect_".
Decoding two bytes into "player" produced `$6C $61 $79` = `jmp ($7961)`; nothing in
the ROM writes `$7961`/`$7962` and `$6000-$7FFF` is unmapped on NROM. Same trap to
watch for in other titles.
- **Also corrected: the old Pass 2 claim.** No `SubC<addr>` label exists in this
`prg.s` and `git log -S SubC` finds no commit that added one; the file used to be
all address-derived `Label_<addr>` with only `Reset`/`NmiHandler`/`IrqHandler`
semantic. Do not copy the old "218 renamed / 374 internal" line into other docs.
**Pass 2 then finished for real: 705 labels, zero `Label_<addr>` left.** Naming
rule that was applied: `jsr`/`jmp` targets and logical block heads get PascalCase
semantic names, internal branch targets get `<Block>_<Role>` (`_Loop`, `_Next`,
`_Done`, `_Store`, `_Return`, …), and `jsr`/`jmp` operands stay raw numeric
(this file's convention, as in `Joypad Test Cartridge (U)`) while branch operands
are symbolic.
- Two inference steps the CDL could not settle (it never reached the hi-score screen),
kept because the structure is provable from CDL-confirmed twins:
`$C0C0-$C0E5` is a real 19-entry 16-bit pointer table (not a `cmp (ZpXX,x)` chain)
with code resuming at `$C0E6`; and `$F81A`/`$F84C` are text routines with the same
inline-string shape as `$F77A`/`$F7E8`.
- Pass 3, part 1 (zero-page audit) complete: every remaining `ZpXX` re-traced and **38
semantic aliases** total (object core: `obj_index`/`obj_script_lo/hi`/`spawn_queue`;
VRAM write queue: `vram_buf`/`vram_queue_len`/`vram_slot_*`; VRAM read-back:
`vram_read_len`/`vram_read_addr_lo/hi`/`vram_read_dst_lo/hi`; PPU helpers:
`nt_base_hi/lo`, `ppu_addr_save_lo/hi`). 66 stay neutral with recorded reasons.
Ten aliases were **deleted** by the reclassification because 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. Full evidence + residue in `locals.inc`.
- Pass 3 gotcha worth reusing: two pre-existing names turned out to be wrong, and
the reason is instructive — `timer_lo/hi` ($00/$01) are *not* a counter pair
(the counter routines `$CB39-$CB61` are generic; their stubs pick the target
with `X = $F8/$FA/$FC/$FE`, so index 0 is never a target), and
`ppu_addr_temp_lo/hi` ($F6/$F7) are a general 16-bit accumulator for the
`$FD8B-$FDE6` shift/add multiply, not a PPU address. Not renamed again (churn),
recorded in `locals.inc` instead.
- **Pass 3, part 2 (tables) complete: 33 data structures named.** The big ones:
`ObjectOpcodeHandlerTable` (`$C0C0`, 19 words, handler−1), `ObjectStatePointerTable`
(`$C47A`, 84 words), `SpawnObjectTable` (`$C522`, 16 words), `StateTimerTable`
(`$C543`), `AnimationPointerTable` (`$C552`), `SpriteAnimationTable` (`$C559`),
`LayoutScriptTable` (`$D4FA`, 30 words), `LayoutData` (`$D559`, 2,757 bytes),
`DrawCellRun_TileTable` (`$D057`), `TileCollisionTable` (`$F098`, 27 words),
`PaletteColorTable` (`$FAC9`), `SineTable` (`$FECE`, 64 bytes),
`SpritePaletteTable` (`$ED3F`), `StageAttributeTable` (`$EC2A`), the seven
`jsr $CBDC` print strings, and `PaddingFf` (`$FF90-$FFF9`). Five `.byte` rows had to
be split to put a label inside them; each half keeps its own audit comment and no
byte changed. The `$FFFA-$FFFF` vectors are `.word NmiHandler, Reset, IrqHandler`.
Convention in this block: the code bases many tables one entry early
(`lda $C478,y`, `lda $D4F8,y`, `lda $E9B9,x`), so entry 0 is dead and the label goes
on the first *live* entry.
- **Residue the CDL could not settle — recorded, not guessed.** Everything below is
still `.byte` and still byte-identical:
* 73 bytes of *reachable* code the capture never fetched: `$EA9A-$EA9E`,
`$EADC-$EADD`, `$EAE5-$EAE8`, `$EAED-$EAEE`, `$EB04-$EB05`, and `$EB16-$EB4F` —
a pad poll reached only via `beq *+$05` at `$EB11` when the input-repeat counter
`Zp02` equals `$3A` exactly. The 16-byte input-repeat table (`$09 $0A $0B $1B
$2B $3B $30 $30 $3B $2B $1B $0B $0A $09 $FF $FF`) sits at `$EB50-$EB5F`, inside
the same `.byte` run.
* 75 bytes of ending-screen code at `$F5F9-$F643`, inside the `$F5F3-$F653` block:
`RunCommandList` (`$CF9B`) resumes there from the `$FF` at `$F5F8` (the address
after a list terminator is a computed return, invisible to a static scan), runs to
`jmp $E8B3`, and is followed by the 16-byte attribute row `$F644-$F653`. Needs a
completed game, so natural play never reached it.
* ~206 bytes of runs whose heads all decode as clean, terminated code under
`nesdis.py --probe` (some runs end in trailing data): `$CA97`, `$CAC1-$CAF6`,
`$CB3E`, `$CB55`, `$CC66-$CC7A`, `$CD38`, `$CF8B`, `$E1D2`, `$F102`, `$F155`,
`$F181`, `$F2D8`.
* Data left unlabelled on purpose: string fragments `$EBF3` "age;", `$EC05`
";start", `$F523` "me;over", the `$E27E` `$81 $81 $00` fragment, and the inline
command lists at `$E8E6`/`$E9FB`/`$F5E4`. Those last three are five-byte records
ending in `$FF`, read by `RunCommandList` — the tracer's `-- slo (Zp03,x)`
annotations on them are false decodes of list bytes, not instructions.
* `$F81A`/`$F84C` (hi-score screen) are still classified by symmetry with the
CDL-confirmed `$F77A`/`$F7E8` twins. Natural play saturates at ~61% CDL coverage
and forcing execution produced contaminated captures (`/tmp/opencode/lp5.cdl`,
`lp6.cdl` — **do not use**). Beating `HI. 0005000` or sinking a "perfect" shot
would settle them.
- `_tools/cdl_reclass.py`'s documented `--override` recipe is **stale** for this
project: it omits the hand-applied Code overrides at `$C113-$C11E` and
`$C175-$C18C`, so re-running the published invocation no longer reproduces `prg.s`.
- Three undocumented opcodes this project forced into `nesdis.py`: `$72` (RRA abs),
`$42` (NOP #imm), `$FA` (NOP abs,X). One emitter fix: ca65 optimises an
operand with a `$00` high byte into zero-page form, so ABS/ABSX/ABSY/IND
operands with high byte `$00` now emit the `a:` prefix to force absolute.
### 3. Audit comments for Excitebike and Pac-Man
### 3. Audit comments for Excitebike
Both are hand-written sources with semantic labels but **no byte-identity audit
comment**, so a source line cannot be mapped back to ROM bytes without
re-assembling. Every other project has one.
Excitebike (JU) is a hand-written source with semantic labels, and **1,013 of its
5,376 instruction lines carry no byte-identity audit comment**, so those lines cannot
be mapped back to ROM bytes without re-assembling. (An earlier revision of this item
named Pac-Man as well. That was wrong on both counts: Pac-Man (USA) (Namco) is fully
commented, 4,695/4,695, and the two `_wip/Pac-Man` folders are 12-line `.incbin`
Pass-0 skeletons with no disassembly at all. Corrected 2026-10-18.)
- `ca65 -l` gives offset + bytes, and the offset indexes `prg.bin` directly.
Byte *values* must come from `prg.bin`, not the listing — ca65 prints `rr rr`
@@ -192,6 +296,29 @@ These cost real time this session. All are recorded so they don't cost it twice.
- **Input goes on joy1 = `$4016`** with a real latch/shift register. The poll
routine's `lda $4016,X` with X=1 lands on `$4017`, but only bits 0–1 are
compared, so driving `$4017` consumes nothing and changes no coverage.
- **A static trace cannot settle the code/data split; the emulator's Code Data
Logger can.** The trace misses code behind computed dispatch, and it cannot tell
inline string data from an instruction that decodes out of the same bytes — both
leave the rebuild byte-identical while the split stays wrong. Mesen marks every
PRG byte the CPU *fetched as an opcode* vs *read as data*; capture it with
`mesen_cdl_file(action:'save', memoryType:'NesPrgRom', filepath:...)` (cumulative —
it does not reset) and feed it to `_tools/cdl_reclass.py`, which rewrites the split
to match wherever the CDL has an opinion and leaves the rest alone. Lunar Pool went
9,403 → 11,041 code bytes that way (1,738 data→code, 100 code→data) and then
recorded zero disagreement with the CDL; 36 further bytes were moved by hand later
(`$C113-$C11E`, `$C175-$C18C`), so its file now carries 11,077. Forcing the PC to
reach unvisited code is
*not* a general substitute: a routine that returns through a popped address runs
away when entered that way and marks garbage as Code (Lunar Pool `$F81A`).
- **A `jsr` to a helper that pops its own return address means the bytes after it are
text, not code.** Lunar Pool's `$CBDC`/`$CBC9` pops the caller's return address
into a pointer, prints a `$81`-prefixed, `$00`-terminated string (`$81` doubles as the
word separator), then `rts`es
to the byte after the terminator. A flat tracer started an instruction two bytes
into "player" (`$6C $61 $79` = `jmp ($7961)`) and produced a plausible-looking
"RAM-pointer dispatch gate" that does not exist — `$7961` is never written and
`$6000–$7FFF` is unmapped on NROM. Suspect this shape whenever a `jmp ($xxxx)`
operand spells ASCII.
- **Odd vector targets are not bugs.** Burger Time's `$FF7F` NMI and CIRCUS's
`$4CB7` IRQ point into unused space. That is common in this set — verify before
assuming a handler is broken.
@@ -238,4 +365,7 @@ These cost real time this session. All are recorded so they don't cost it twice.
- `_tools/nesdis.py` — Pass 1 tracer (`--undocumented`, `--auto-seed`,
`--extra-seed`, `--table`, `--data`, `--probe`).
- `_tools/verify_pass1.py` — faithfulness + completeness check across all projects.
- `_tools/cdl_reclass.py` — reclassify a project's code/data split from a Mesen Code
Data Logger capture (dry run by default; `--apply` rewrites `prg.s`, then `make`
must still print BYTE-IDENTICAL). Its docstring covers capture and the safety rules.
- `_tools/bytecode_dis.py`, `pass2.py`, `trace_stage_payload.py` — per-pass helpers.