Promotes Lunar Pool
This commit is contained in:
1 parent
837007843d
commit
62866563ba
53 files changed
+115746
-4100
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user