Pass 3 _wip: Slalom (U), Kung Fu (U), Othello (U) semantic naming complete Pass 3 _wip: apply symbols (0 residue) for Slalom (U), Kung Fu (U), Othello (U) Pass 2/3 _wip: 10 NROM projects (Elevator Action, 1942, Tag Team Wrestling, Galaga, Kung Fu a1, Stack-Up, Magmax, Ms Pac-Man Namco, Soccer, Sqoon) Promote 13 _wip/NROM projects to NROM/ (Slalom, Kung Fu, Othello, Elevator Action, 1942, Tag Team Wrestling, Galaga, Kung Fu a1, Stack-Up, Magmax, Ms Pac-Man Namco, Soccer, Sqoon) Pass 2/3 _wip: 5 CNROM games (Tiger-Heli, Paperboy, Last Starfighter, Mighty Bomb Jack, Kung-Fu Heroes) Docs: document CDL capture + re-trace workflow in TODO.md Adds more code for - Elevator Action (U) - Kung Fu (U) - Othello (U) - Slalom (U) - Sqoon (U)
858 lines
63 KiB
Markdown
858 lines
63 KiB
Markdown
# TODO / Handoff
|
||
|
||
Written so a fresh session can pick this up cold. Everything below was verified
|
||
against the repo at commit `9e2b50a`; if a number here disagrees with the repo,
|
||
the repo is right and this file is stale.
|
||
|
||
> **Counts refreshed 2026-10-21.** The repo now holds **41 promoted projects**
|
||
> (38 under `NROM/`, plus `CNROM/World Class Track Meet (U)`,
|
||
> `UNROM/Zanac (U)` and `MAPPER8/Mermaids of Atlantis (U)`) and **851 `_wip/`
|
||
> projects**. Both the last two disassembled `_wip` projects — `Golf (JU)` and
|
||
> `World Class Track Meet (U)` — were promoted on 2026-10-21, so **every**
|
||
> `_wip/` project is a Pass 0 skeleton. Many sentences below still describe the
|
||
> earlier 30/39-promoted state; treat the numbers in this note as current.
|
||
|
||
> **Note:** This file documents work completed up to 2026-10-20 (promotion of Ice Climber,
|
||
> Lode Runner and Pinball; `_wip` evaluation). The actual repo state is always the source of truth.
|
||
>
|
||
> **Layout note (2026-10-20):** projects now live under their **mapper directory**
|
||
> (`NROM/`, `AOROM/`, `CNROM/`, `MMC1/`, `MMC3/`, `MMC5/`, `UNROM/`), project folders
|
||
> are **flat** (the `copilot/` subfolder is gone — strip the prefix when reading older
|
||
> references below), every project's `OVERVIEW.md` is now its `README.md`, and a root
|
||
> `Makefile` builds, verifies and cleans every project (`make`, `make clean`,
|
||
> `make <MAPPER>`); the pre-commit hook drives it. Both tiers sit two levels deep, so
|
||
> `../../_tools/` is correct from any project folder.
|
||
|
||
## The contract
|
||
|
||
Byte-identical rebuild is the hard requirement. In every project folder:
|
||
|
||
```bash
|
||
make # must print "Byte-identical ROM verified."
|
||
```
|
||
|
||
A build that assembles but does not print that message is a failure. Requires
|
||
`ca65`/`ld65` on PATH. Never edit bytes to make a build pass — fix the source.
|
||
|
||
Two tiers exist, and the difference is real:
|
||
|
||
- **Top level — promoted projects under their mapper dir (`NROM/` etc.) — `make` compares the whole ROM against `rom.nes` in the project folder.
|
||
- **`_wip/<MAPPER>/` (851 projects)** — as of 2026-10-20 these also compare the
|
||
whole ROM against a `rom.nes` copy in the project folder, so the header and
|
||
CHR **are** proven. The tier was moved off the old
|
||
`cmp reconstructed_prg.bin prg.bin` form; promotion is now `git mv` plus the
|
||
disassembly work, with no build surgery. See **`WIP.md`** for the tier's
|
||
layout, its quirks and its open work. Promotion still requires what
|
||
promotion always required: a real disassembly (Pass 1 or beyond) with the
|
||
canonical audit comments, and the Pass 2/3 naming work.
|
||
|
||
## Current state
|
||
|
||
- 38 promoted projects under `NROM/` (41 promoted in all, with `World Class Track Meet (U)` under `CNROM/`, `Zanac (U)` under `UNROM/` and `Mermaids of Atlantis (U)` under `MAPPER8/`), all whole-ROM byte-identical (Lunar Pool promoted 2026-10-18,
|
||
Donkey Kong (JU) promoted 2026-10-19 after Pass 3, **Donkey Kong Jr. Math (U) promoted 2026-10-19 after Pass 3 — re-promoted into `NROM/` 2026-10-20** after the 2026-10-20 mapper-folder reorganization initially parked it in `_wip/NROM/` over a `jsr $F218`-mislabeled-as-`SubCF218_Block` mismatch; the fix repointed four `prg.s` call sites to the true `SubCF218` entry, see `NROM/Donkey Kong Jr. Math (U)/README.md`), **Duck Hunt (JUE) promoted 2026-10-19 after Pass 3**, **Ice Climber (JE) and (U) promoted 2026-10-20 after Pass 3**, **Lode Runner (U) promoted 2026-10-20 after Pass 2/3**, **Pinball (JU) promoted 2026-10-20 after Pass 2/3**, **Popeye (JU) (PRG 1) promoted 2026-10-20 after Pass 2/3** (`header.s`/`chr.s` + HEADER `linker.cfg` added, whole-ROM `cmp` passes on the first promoted build; SHA-256 `00160651…78bd8b4`; see `NROM/Popeye (JU) (PRG 1)/README.md`), **`Excitebike (JU)` promoted 2026-10-20** — the byte-596 mismatch was two `PpuMaskShadow` (`$0A`) operands where the ROM uses `$11` (NMI's second mask shadow, aliased as `NmiMaskShadow` in `locals.inc`); the 4,401 damaged audit comments (doubled `??` forms plus 6,339 comments shifted against their mnemonics) were re-anchored from the ca65 listing via the new `_tools/repair_audit_comments.py`, and all 58 raw `jsr/jmp $XXXX` operands were substituted with routine labels/equates. See `NROM/Excitebike (JU)/README.md`. **`Raid on Bungeling Bay (U)` promoted 2026-10-20** — `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg` added, whole-ROM `cmp` passes on the first promoted build (MD5 `77285503…885000ac`); one leftover neutral indirect operand `jmp (Zp01)` at $E2EC became `jmp (IndDispatch01)` (alias already present in `locals.inc`); `apply_symbols.py` reports 0 substitutions pending. **`Pac-Man (U)` promoted 2026-10-20** — Pass 2 completed first (all 194 `Label_XXXX` definitions renamed from a fresh map derived from the committed split, not from a drafted map; all 49 `jsr`/`jmp` call sites resolved; zero `Label_` and zero raw `$XXXX` call operands left), then Pass 3: **119 operands substituted** (101 hardware + 11 single-name + 7 multi-alias resolved per site through a new `sites.tsv`), `apply_symbols.py` now reports **0 residue**. The 7 site resolutions are all proven from context, not guessed — four `jmp ($0010)` immediately preceded by `sta DispatchPtrLo`/`sta DispatchPtrHi`, one `jmp ($00FE)` preceded by `sta AudioHandlerPtrLo`/`sta AudioHandlerPtrHi`, and two `sta a:$0000,y` zero-page clears that take the neutral `Zp00`. `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg` added, whole-ROM `cmp` passes on the first promoted build (MD5 `a69d310f8cae9397fbd39f204672b5c7`); see `NROM/Pac-Man (U)/README.md`. **`Pac-Man (Tengen) (U)` promoted 2026-10-20** — a *different build* of the same game as the row-32 dump: the PRGs differ in 56 bytes in 8 runs, all of them code-versus-licensee-text ($C3E3-$C408 is 26 spaces here against `LICENSED BY NAMCO AMERICA INC` there; $FF00-$FF16 is `$FF` padding here against the boot code there), and the RESET vector follows ($C033 against $FF00) — the same boot sequence, inline. Disassembled **from scratch**: the map was derived from this dump's own committed split, with no name, map or file carried over from the row-32 dump. Pass 2 named all 194 `Label_XXXX` definitions and resolved all 48 raw `$XXXX` call operands (map in `routinemap.tsv`); Pass 3 added 30 semantic zero-page aliases applied only at the proven sites (`sites.tsv`) and named 28 data tables, including `CharacterDataTable` ($C66A) / `CharacterNameStrings` ($C67E), where the ROM itself names Blinky, Pinky, Inky and Clyde. **A Pass 1 correction came with it:** six command handlers were recovered — `ScriptCommandTable` ($EFDD) is read by the traced instruction at $EFCC and six of its entries point at $F00A, $F022, $F03A, $F052, $F062 and $F072, all of which the tracer had left as data, two of them mis-split across the `jmp $EF61` at $F01F and $F04F. Those 120 bytes are code here. **The same gap exists in the promoted row-32 dump, which has not been corrected.** `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg` added, whole-ROM `cmp` passes on the first promoted build (MD5 `d34e7a74…28ad0`); see `NROM/Pac-Man (Tengen) (U)/README.md`. ****`Pyramid (U) (Sachen)` promoted 2026-10-20** — an unlicensed Sachen release with a mapper-0 header whose boot code pokes a mapper unlock at `$4100` and checks the RAM bytes `$0120`/`$0121` against "A"/"V". **A Pass 1 correction came with it:** the committed split was a strict subset of the correct one — **828 bytes** of code were still marked as data, because the tracer cannot follow a `jmp` that sits inside a data region, and this game dispatches through four such tables ($C01E, $C02A, $C042, $C054) plus ten more at $C000-$C01B. Re-tracing with those sixteen targets, the `$C800` boot mainline and six further code starts seeded by hand recovers them (and a five-byte stub at $DF89 in a hole between two routines); the re-trace loses **no** committed code byte and takes the split from 34.5% to 39.6%. `make regen` reproduces it, so the split stays verifiable. Pass 2 named 431 routine entries and the 32 data blocks from this dump's own code (428 `Label_XXXX` gone, 320 raw `$XXXX` call operands resolved; the internal branch-target names are derived mechanically by the new `_tools/derive_branch_names.py`); Pass 3 added 24 semantic zero-page aliases, applied only where their role is proven, and named 32 data tables. `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg` added, whole-ROM `cmp` passes on the first promoted build (MD5 `aa285ff1…6c59e2`); see `NROM/Pyramid (U) (Sachen)/README.md`. **`Raid on Bungeling Bay (U) [a2]` promoted 2026-10-20** — a *different build* of the same game as the row-34 dump: 150 of the 16,384 PRG bytes differ in 7 runs, all of them code-versus-copyright-text at $C67D-$C6D8 and $C73B-$C779, the CHR differs too, and header byte 6 is $00 (Horizontal) against $01 (Vertical). Its Pass 3 added **15 semantic zero-page aliases** (every one applied only at the sites where its role is proven), and finishing it **corrected a naming error in the already-promoted sibling**: `$EA` had been named `VBlankWaitCounter`, but the routine at $C412 is the game's PRNG (the classic `eor`/`asl`/`php`/`lsr`/`plp`/`rol` scrambler, called from thirteen gameplay sites and twice in a row at $CDF4/$CDF9 with the second result masked `#$07`), so `$EA` is the scrambler's phase counter — renamed `PrngPeriodCounter` in **both** dumps, and `$EB`/`$EC` were named `PrngSeedB`/`PrngState` in both. The sibling's two `sta a:IndDispatch17,y` operands were also corrected: they are absolute stores to RAM $0017, not the dispatch pointer. `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg` added, whole-ROM `cmp` passes on the first promoted build (MD5 `659d340a1e9d420eee28df13319b0094`); see `NROM/Raid on Bungeling Bay (U) [a2]/README.md`. **`Mario Bros (JU)` promoted 2026-10-20** — a two-player light-gun-less platformer whose handler tables are **inline**: the pointers sit in the middle of the code stream, right after the `jsr` that reads them, so the static tracer could never follow them. **A Pass 1 correction came with it:** a Mesen Code Data Logger capture of this ROM (10,915 code bytes) showed **1,010 bytes** the tracer had left as data were code; two branch targets the capture never reached (`$E4CB` and `$E639`) were forced with `--override` for 57 more, so **1,067 bytes** moved from data to code and 22 moved the other way, taking the split from 72.0% to 78.4%. Pass 2 named every label — **zero `Label_` and zero `SubC` residue**: the 185 entries a previous session had left as address-derived residue were all named from the code. Pass 3 added 25 semantic zero-page aliases, applied only where the role is proven, and named 35 data tables. `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg` added, whole-ROM `cmp` passes on the first promoted build; see `NROM/Mario Bros (JU)/README.md`.
|
||
**`Hogan's Alley (JU)` promoted 2026-10-20** — a light-gun game whose engine dispatches through **inline** tables: `IndirectCall` at $C34D pops the return address of the `jsr` that called it and reads a 16-bit handler pointer from the word that follows, so all six handler tables ($C124 19 entries, $C219 4, $C89C 14, $CDD1 1, $D234 2, $D3D6 14) live *inside* the code stream and the static tracer cannot follow them. **A Pass 1 correction came with it:** the committed split was a strict subset of the correct one — a merged Mesen Code Data Logger capture (five captures OR-ed, 6,544 code bytes) showed **1,216 bytes** that the tracer had left as data were code, taking the split from 41.8% to 49.1%; five bytes moved the other way. The four branch targets the reclassifier could not prove were turned into labels by the new `_tools/resolve_raw_branches.py`. Pass 2 named 206 routine entries and derived 413 internal branch targets from this dump's own code (630 `Label_XXXX` gone, 484 raw `$XXXX` call operands resolved, 15 unlabelled call targets given a label); Pass 3 added 60 semantic zero-page aliases, applied only where the role is proven, and named all 34 data tables. `apply_symbols.py` reports 1 residue (`sta a:$00B4,y` at $C203 — the same address as `Zp40`/`ActorPalette` but forced to absolute addressing, so it must stay numeric). `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg` added, whole-ROM `cmp` passes on the first promoted build; see `NROM/Hogan's Alley (JU)/README.md`.
|
||
- 851 projects under `_wip/` (35 in `_wip/NROM/`, 221 in `_wip/MMC1/`, 239 in
|
||
`_wip/MMC3/`, 97 in `_wip/UNROM/`, 57 in `_wip/CNROM/`, 10 in `_wip/MMC5/`
|
||
and 192 across 23 more mapper folders): all whole-ROM byte-identical.
|
||
**All 851 of them are Pass 0 skeletons** — the folder shape and the build are
|
||
proven, but `prg.s` is still a single `.incbin "prg.bin"`, so they owe
|
||
Pass 1 from scratch. Exactly one carries a real disassembly: `Golf (JU)`
|
||
(Pass 1 + substantial Pass 2, still carrying 349 internal `Label_` branch
|
||
targets plus pre-canonical audit comments). `Volleyball (U)` was the last
|
||
project to leave the tier with a disassembly (Pass 1 + Pass 2 + Pass 3,
|
||
promoted to `NROM/` on 2026-10-21 — 11,395 instructions / 23,926 code
|
||
bytes, split with a Mesen CDL capture, all 1,989 labels named).
|
||
Hogan's Alley (JU), Mario Bros (JU), Pinball (JU),
|
||
Lode Runner (U), Popeye (JU) (PRG 1), Excitebike (JU), Raid on Bungeling Bay (U),
|
||
Raid on Bungeling Bay (U) [a2] and Pac-Man (U) were promoted after
|
||
their fix/Pass work. See **`WIP.md`** for the tier's detail.
|
||
- Local `master` is ahead of `origin/master`; nothing pushed.
|
||
- `_tools/verify_pass1.py` covers the **promoted projects** (it selects project
|
||
folders under the mapper dirs carrying a `Makefile` + `prg.bin`/`rom.nes`; after
|
||
the 2026-10-20 promotions this includes all 35 of `NROM/` — all canonical,
|
||
OK verdict), not the `_wip/` tier, which is checked by `make wip`.
|
||
Current run (2026-10-20): every one builds, all canonical, **no truncated or
|
||
unfaithful Pass 1**. The earlier-reported re-trace crash on `MUSCLE (U)` /
|
||
`Donkey Kong Jr. (JU)` was a false alarm — `nesdis.operand_value()` already handles
|
||
the `a:` prefix via `token.removeprefix("a:").lstrip("$")`. The `make` half was
|
||
always working.
|
||
|
||
```bash
|
||
python3 _tools/verify_pass1.py # all 30 promoted projects
|
||
python3 _tools/verify_pass1.py "MUSCLE (U)" # one project (top-level only)
|
||
```
|
||
|
||
- **Audit comment standardization complete (2026-10-19):** All 27 top-level projects now
|
||
use the canonical `; CPU:$XXXX PRG:XXXX AA BB` format.
|
||
- CIRCUS: fixed 4,427 comments (`CPU:XXXX` → `CPU:$XXXX`)
|
||
- Excitebike (JU): fixed 48 double-semicolon comments (`; state $XX ; CPU:...`)
|
||
- 9 projects received canonical comments via `_tools/audit_comment_tool.py`:
|
||
Balloon Fight, Baseball, Bomberman, Burger Time, Clu Clu Land, Defender 2,
|
||
Donkey Kong 3, Donkey Kong Jr., Tennis
|
||
- **Pass 3 symbol application complete (2026-10-19):** All 27 top-level projects now
|
||
have `_tools/apply_symbols.py` run on them, substituting `symbols.inc`/`locals.inc`
|
||
names into `prg.s` operands:
|
||
- Balloon Fight (JU): 125 hardware + 797/813 ZP (16 `$13` residue)
|
||
- Baseball (UE): 121 operands (85 single + 36 hardware)
|
||
- Bomberman (U): 354 single + 8 site-resolved (already applied)
|
||
- Burger Time (U): already applied
|
||
- Clu Clu Land (JU): already applied
|
||
- Defender 2 (U): already applied
|
||
- Donkey Kong 3 (JUE): already applied
|
||
- Donkey Kong Jr. (JU): already applied
|
||
- CIRCUS: 118 operands (38 single + 80 hardware; 23 multi-alias residue)
|
||
- Wild Gunman (U): 76 operands (11 single + 65 hardware)
|
||
- Joypad Test Cartridge (U): 47 hardware
|
||
- NES PowerPad Test Card (U): 89 hardware
|
||
- MUSCLE (U): 220 operands (91 single + 129 hardware)
|
||
- Lunar Pool (U): 45 operands (18 single + 27 hardware)
|
||
- Donkey Kong (JU): 75 operands (17 single + 58 hardware)
|
||
- Tennis (JU): 18 single
|
||
- Pac-Man (USA) (Namco): 524 single (727 multi-alias residue)
|
||
- Urban Champion (JU): 70 operands (4 single + 66 hardware)
|
||
- Millipede (U): hardware registers substituted; 8 semantic ZP aliases (Pass 3 complete)
|
||
- Donkey Kong Jr. Math (U): 69 hardware + 13 multi-alias ZP substituted directly
|
||
- Duck Hunt (JUE): 68 hardware + 18 multi-alias ZP substituted directly
|
||
- Ice Climber (JE): 91 operands (23 single + 68 hardware) + 3 multi-alias via sites.tsv
|
||
- Ice Climber (U): 91 operands (23 single + 68 hardware) + 3 multi-alias via sites.tsv
|
||
- Lode Runner (U): 85 operands (83 hardware + 2 single); 2 semantic ZP aliases (Pass 3 partial)
|
||
- Pinball (JU): hardware registers substituted; no semantic ZP aliases (Pass 3 partial)
|
||
- Popeye (JU) (PRG 0): 79 operands (15 single + 64 hardware); 4 semantic ZP aliases (Pass 3 complete)
|
||
- Popeye (JU) (PRG 1): 80 operands (15 single + 65 hardware); 4 semantic ZP aliases (Pass 3 complete)
|
||
- Excitebike (JU): skipped — inline defs in `prg.s` conflict with `locals.inc`.
|
||
The naming gap this left was closed by hand on 2026-10-20: all 58 raw
|
||
`jsr/jmp $XXXX` operands now carry routine labels or 15 sub-entry equates
|
||
added to `locals.inc` (sub-entries are equates rather than mid-routine
|
||
labels so no ca65 local-label scope gets split — adding `Name:` lines
|
||
mid-routine re-scopes every `@local` after the split and breaks forward
|
||
`@refs`); zero-page operands remain numeric (Pass 3 residue)
|
||
|
||
---
|
||
|
||
## Open work, highest value first
|
||
|
||
### 1. Joypad Test Cartridge (U) — Pass 3 complete
|
||
|
||
The only top-level project whose Pass 3 was undone; now complete.
|
||
|
||
- **Pass 1** redone (6.6x: 282 → 1,773 instructions, 580 → 3,836 bytes / 3.5% → 23.4%).
|
||
- **Pass 2** complete: 202 labels, zero `Label_` remaining, all 202 routine headers describe real behaviour.
|
||
- **Pass 3** complete: the single data region `$CF06–$FFFF` (12,538 bytes) has been
|
||
broken into 20 named sub-blocks (section 8 of the project README.md):
|
||
- VRAM command streams at `$CF06`, `$CF40`, `$D063`, `$D1F5` — decoded via `RetAddrUpdate`
|
||
(`$CDC8`): `$4C` = jump, `$60` = deferred PPUCTRL, bit-7-clear = VRAM write (PPU addr + data,
|
||
bit 6 of data = long/short command selector).
|
||
- OAM table groups at `$D4E3`, `$D5C4`, `$D644` — 12 groups of 32-byte tables
|
||
(8 entries of Y/tile/attr/X), read by `CopyOamFromTable`/`CopyOamFromTable70`.
|
||
- Sprite template at `$D6A5` (read by `CopySpriteTemplateTable`) and button OAM
|
||
template at `$D6C5` (read by `PrepareButtonOam`).
|
||
- `$FF` ROM padding (`$D6E5–$FFF9`) and vectors (`$FFFA–$FFFF`).
|
||
- Build verified byte-identical.
|
||
|
||
**Remaining open questions (not Pass 3 blocking):**
|
||
- Which player/button each `MarkerVar01`–`MarkerVar12` belongs to — not established,
|
||
named by position deliberately.
|
||
- `HandleButtonPress1`–`8` write OAM tiles in order `$0202, $0206, $020E, $020A, ...`
|
||
— sprites 1, 2, 4, 3, 5, 6, 7, 8. The out-of-order third/fourth entries are unexplained.
|
||
|
||
### 2. Mario Bros (JU) — Pass 2 substantial, reach-limited
|
||
|
||
`NROM/Mario Bros (JU)/` (promoted 2026-10-20). Pass 1 complete: **72.2% code**
|
||
(5,735 instructions), byte accounting exact, BYTE-IDENTICAL.
|
||
|
||
**Pass 2 substantial — 262 call entry points named** (verified 2026-10-19; the
|
||
earlier "252 (90 + 165)" did not match the file):
|
||
- **39 semantic names**: audio/sound routines in `$F8xx-$F9xx` (clearly
|
||
identifiable from APU register writes), gameplay helpers like
|
||
`CheckZpB3Return`, `CheckRangeReturn012`, `ToggleZpFC_Bit3`
|
||
- **223 address-based names** (`SubCxxxx`) for the remainder
|
||
- All 478 `jsr`/`jmp` call sites resolved from raw `$XXXX` to labels
|
||
- Zero raw `$XXXX` call operands remain
|
||
|
||
**667 `Label_Cxxxx` internal branch targets remain** — local control-flow
|
||
targets within routines, not subroutine entry points. These do not need
|
||
semantic names for the call graph to be readable.
|
||
|
||
**The real blocker is reach, not naming.** 4,555 bytes (27.8%) are still data,
|
||
gated behind a stack-based indirect call at `$CD9E` (table pointer arrives via
|
||
`pla`, unknowable statically) and a 20-entry `JSR` thunk table at `$C5A3`.
|
||
Resolving the pushed table addresses is the key to the remaining 28%.
|
||
- Already documented: `$C5A3` is a 20-entry `JSR` thunk table, `$C0B6` is the NMI
|
||
phase dispatcher. Both were found by probing, which is a cheap technique worth
|
||
reusing on other projects.
|
||
|
||
### 2b. Lunar Pool (U) — Pass 2 and Pass 3 complete, promoted to the top level
|
||
|
||
`NROM/Lunar Pool (U)/` (promoted out of `_wip/` 2026-10-18 by `git mv`; 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:** this promotion needed the `header.s`,
|
||
`chr.s` and HEADER/PRG/CHR `linker.cfg` added by hand (the header and CHR
|
||
stay as the raw `header.bin`/`chr.bin` splits), so promotion was not just a
|
||
symlink and a `cmp` edit — copy the shape from
|
||
`NROM/Burger Time (U)/` (`header.s`/`chr.s` are one-line
|
||
`.incbin`s; the linker needs `HEADER: start = $0000, size = $0010` before PRG).
|
||
**As of 2026-10-20 the `_wip/` tier no longer needs any of that** — every
|
||
project there carries the whole-ROM build already, so promotion is now just
|
||
`git mv` plus the disassembly work. See `WIP.md`.
|
||
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.
|
||
- **Correction to an earlier claim in this file and in the two project source docs:**
|
||
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 — **COMPLETE** (2026-10-19; re-anchored 2026-10-20)
|
||
|
||
All 5,376 instruction lines in Excitebike (JU) now carry canonical
|
||
`; CPU:\$XXXX PRG:XXXX AA BB` audit comments. The `_tools/audit_comment_tool.py`
|
||
uses `nesdis.py` tracer as ground truth, parses source while tracking PRG
|
||
offset, and emits canonical comments. Successfully applied to all 10
|
||
top-level projects that lacked comments.
|
||
|
||
**2026-10-20 discovery + repair:** the old pass had two damage modes it
|
||
did not self-validate against: 4,401 lines carried doubled `??` comment tails
|
||
(directives like `.word`/`.byte` got no bytes from the tracer), and 6,339 of
|
||
6,478 comment groups were systematically shifted against their mnemonics
|
||
(negative drift up to −140 bytes — comments generated against an earlier,
|
||
shorter source layout and never re-anchored). Fixed by the new
|
||
`_tools/repair_audit_comments.py`, which uses the `ca65 -l` listing as ground
|
||
truth for each source line's PRG offset and byte span, validates every batch
|
||
against `prg.bin`, refuses to write on any `??` residue, and preserves
|
||
non-audit semantic notes (`; state $02` etc.). Key listing facts (reusable for
|
||
other damaged trails): ca65 echoes a line's text on the FIRST byte batch only
|
||
(continuations carry bytes but no text), depth-2 records are `.include` echo,
|
||
and listing offsets index `prg.bin` directly.
|
||
|
||
- `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`
|
||
for relocatable words.
|
||
- **Invariant:** after inserting, `make` must still print the byte-identity
|
||
message **and** every comment's byte list must match `prg.bin` at the stated
|
||
offset. A wrong offset that still assembles is the failure mode to watch.
|
||
|
||
### 3a. Audit comments for Balloon Fight (JU) — **COMPLETE** (2026-10-21)
|
||
|
||
The trail was *spelled* canonically but was **wrong by two bytes** from
|
||
`ResetInitializationData` ($C085) onward: **5,944 of the 6,007** comments named a
|
||
CPU address / PRG offset two bytes below the truth, because the 3-byte
|
||
`.byte "HAL"` signature at $C082–$C084 was counted as one byte when the
|
||
comments were first generated. The routine headers (`; PRG offsets:` /
|
||
`; CPU range:`) and the linker labels were right, so the source and its own
|
||
headers disagreed — a good way to spot this class of damage.
|
||
|
||
- Fixed with `_tools/repair_audit_comments.py` (ca65 listing as ground truth,
|
||
validated against `prg.bin`) followed by `_tools/align_audit_comments.py`
|
||
for the canonical column-35 layout. Comment-and-whitespace only: all 8,544
|
||
lines of code text unchanged, `make` still byte-identical.
|
||
- **Off-by-one fixed in `_tools/align_audit_comments.py`:** it tested the
|
||
*indented* code length against 31 instead of against 35, so code texts of
|
||
28–31 characters were never padded into the column. That is the difference
|
||
between the emitter's `f" {text:<31}; …"` and the tool's behaviour; short
|
||
lines were left ragged at column 34/36/37.
|
||
- **Routine headers completed (2026-10-21):** the 61 routine headers that had
|
||
no `; Clobbers:` line now have one, so all 63 carry the full template
|
||
(global + local description, parameters, returns, clobbers). The blocks are
|
||
derived by `_tools/derive_clobbers.py`, never guessed: the opcode comes from
|
||
the audit comment's byte list, the operand is resolved through
|
||
`locals.inc`/`symbols.inc` so a clobber can be named, the control flow is
|
||
followed (a `jsr` to another named entry is a call, a branch or a
|
||
fall-through is not), and register preservation is modelled — so `NmiHandler`
|
||
reports *no register* and `FlushPpuCommandPreserved` reports only A.
|
||
- **Still open:** 20 of the 63 routine headers still carry the Pass-1
|
||
placeholder text ("Reachable game-engine routine identified during recursive
|
||
Pass-1 tracing", "Caller-defined register and global RAM state", …) in
|
||
their `Global:` / `Local:` / `Parameters:` / `Returns:` fields. The other 43
|
||
have been written. This is Pass 3 work on the *meaning* of the routines, not
|
||
on their mechanics, and it is not derivable from the code alone.
|
||
|
||
### 4. `Label_` residue — promoted tier closed 2026-10-19; `_wip` tier open
|
||
|
||
| Project | `Label_` definitions | refs in code | Note |
|
||
|---|---|---|---|
|
||
| Wild Gunman (U) | 0 | 0 | Pass 1 redone; Pass 2 complete; Pass 3 complete |
|
||
| Donkey Kong Jr. (JU) | 0 | 0 | Pass 2 complete (2026-10-19) |
|
||
| Balloon Fight (JU) | 0 | 0 | Pass 2 complete (2026-10-19); Pass 3 symbols applied — 16 `$13` operands left numeric |
|
||
| Donkey Kong (JU) | 0 | 0 | Pass 2 complete (2026-10-19); Pass 3 complete; promoted 2026-10-19 |
|
||
| Pac-Man (U) | 0 | 0 | Pass 2 complete (2026-10-20); Pass 3 partial — 119 operands, 0 residue; promoted 2026-10-20 |
|
||
| Pac-Man (Tengen) (U) | 0 | 0 | Pass 2 complete (2026-10-20); Pass 3 partial — 30 zero-page aliases, 28 named tables, 0 residue; promoted 2026-10-20 |
|
||
| Pyramid (U) (Sachen) | 0 | 0 | Pass 2 complete (2026-10-20); Pass 3 partial — 24 zero-page aliases, 32 named tables, 0 residue; Pass 1 corrected (+828 bytes of code); promoted 2026-10-20 |
|
||
| Raid on Bungeling Bay (U) | 0 | 0 | Pass 2/3 partial (2026-10-20); promoted 2026-10-20 |
|
||
| Raid on Bungeling Bay (U) [a2] | 0 | 0 | Pass 2 complete; Pass 3 partial — 15 zero-page aliases, 0 residue; promoted 2026-10-20 |
|
||
| **All promoted** | **0** | **0** | **All Pass 3 complete, symbols applied** (DK Jr. Math re-promoted 2026-10-20; Popeye (JU) (PRG 1) promoted 2026-10-20; Pac-Man (U) promoted 2026-10-20) |
|
||
Note these counts were address-derived labels only — some were legitimately
|
||
internal branch targets and may never deserve a semantic name.
|
||
|
||
**Balloon Fight (JU) Pass 3 symbol application (2026-10-19).** `prg.s` now uses
|
||
the names from `symbols.inc` and `locals.inc` for every operand they define: all **125**
|
||
hardware-register operands are symbolic, and **797 of 813** zero-page/RAM operands are
|
||
symbolic. `_tools/apply_symbols.py` does the substitution (`--sites` carries the
|
||
per-routine choice for addresses that carry several names, since 15 addresses in
|
||
`locals.inc` have 2–3 aliases). The **16 residue** operands are all `$13`, used as a
|
||
plain high-byte scratch beside `Temp12`; its aliases (`ReverseRecordIndex`,
|
||
`CollisionDeltaY`, `MotionDeltaHi`) describe other routines, so those operands were left
|
||
numeric rather than guessed. A neutral `Temp13 = $13` in `locals.inc` would close them.
|
||
Rebuild stays byte-identical.
|
||
|
||
**The operand-naming sweep covered the then-30 top-level projects (2026-10-19/20).** The promoted tier is now 41 projects; its per-project state is in the Current state section above. Excitebike (JU)'s `apply_symbols.py` run was skipped due to inline `prg.s` definitions conflicting with `locals.inc` values — its 58 raw `jsr/jmp $XXXX` operands were substituted by hand on 2026-10-20 instead (routine labels + `locals.inc` sub-entry equates); its zero-page operands remain numeric as documented Pass 3 residue.
|
||
|
||
The `_wip` tier is still open, but the shape of the work changed on
|
||
2026-10-20: all 851 of its projects are **Pass 0 skeletons** with no
|
||
disassembly at all, so they owe Pass 1 from scratch rather than a label
|
||
cleanup. The last two that carried real disassemblies — `Golf (JU)` and
|
||
`World Class Track Meet (U)` — were promoted on 2026-10-21. The earlier
|
||
Pass 2/3 audits of the projects that have since left the tier (`Golf (JU)`,
|
||
`Mario Bros (JU)`, `Raid on Bungeling Bay (U) [a2]`, `Pac-Man (U)`,
|
||
`Pac-Man (Tengen) (U)`, `Pyramid (U) (Sachen)`, `Hogan's Alley (JU)` and the
|
||
rest) are recorded in the project READMEs. Five project headers and five inventory rows used to read "Pass 2
|
||
complete"; they had counted their *draft map's rows* rather than names actually
|
||
present in `prg.s`, and were corrected 2026-10-19 (the project READMEs have the
|
||
per-project audits):
|
||
|
||
| Project | named / total label defs | `Label_` left | drafted map (rows / applied / stale / **applicable**) |
|
||
|---|---|---|---|
|
||
| Pinball (JU) | 180 / 681 | 501 | 210 / 179 / 17 / **0** |
|
||
| Mario Bros (JU) | 270 / 937 | 667 | none (262 call targets: 39 semantic + 223 `SubC`) |
|
||
| **Lode Runner (U)** | **627 / 627** | **0** | 0 / 0 / 0 / **0** (fresh Pass 2 done 2026-10-20; **Pass 2/3 complete 2026-10-20**) |
|
||
|
||
**The drafted maps are not applicable.** Auditing every row by address against the
|
||
committed split: a large fraction point at addresses that are **not instruction
|
||
boundaries** (100/243 DK Jr. Math, 96/255 Ice Climber (JE), 17/210 Pinball) — they were
|
||
built against an older code/data split — while most of the rest are unlabelled
|
||
instruction boundaries that **no operand in `prg.s` references**, or (Duck Hunt)
|
||
competing names for the same label (`Label_C2BF` alone carries 205 candidates).
|
||
Across all four maps the number of rows that are both insertable *and* corroborated by
|
||
control flow is **zero**. Pass 2 here means deriving a fresh map from the committed
|
||
split (routine entries + `<Routine>_Block[_n]` internal branch targets, as done for
|
||
`Donkey Kong (JU)`), not applying these files. `Pinball (JU)` also has 40 Pass 3
|
||
data-region names drafted in `data_regions.tsv` and a `pinball_zp_map.tsv`, none applied.
|
||
|
||
**Donkey Kong (JU) Pass 2 resolution (2026-10-19):** the last 919 address-derived
|
||
definitions were named — **102** new semantic routine names, **217** `SubC<addr>` entry
|
||
names (honest residue, matching `Golf (JU)`/`Mario Bros (JU)` practice) and **600**
|
||
derived `<Routine>_Block[_n]` internal branch targets. What remains, if anything, is
|
||
behavioral naming rather than address-derived labels: the 217 `SubC<addr>` routines, the
|
||
145 zero-page slots in `locals.inc` (still neutral `ZpXX`), and the 407 unapplied `zp`
|
||
proposals in `dk_full_map.tsv`.
|
||
|
||
**Donkey Kong Jr. Math (U) Pass 2 resolution (2026-10-19):** the 971 remaining `Label_`
|
||
definitions were named fresh from the committed split — **64** semantic routine names
|
||
preserved from Pass 1, **295** `SubC<addr>` entry points for all remaining `jsr`/`jmp`
|
||
targets, and **676** `<Routine>_Block[_n]` internal branch targets. **13 semantic
|
||
zero-page aliases** added to `locals.inc` (`PtrLo/Hi`, `Ptr2Lo`, `VBlankTimer`,
|
||
`SprCount`, `SprIndex`, `ObjIndex`, `BtnState`, `ScorePtrLo/Hi`, `SndChannel`,
|
||
`GameState`, `FrameCounter`). Hardware register operands (69) substituted via
|
||
`apply_symbols.py`; zero-page multi-alias addresses need a `--sites` file for full
|
||
substitution. Build remains **BYTE-IDENTICAL**.
|
||
|
||
**Donkey Kong Jr. Math (U) Pass 3 completion (2026-10-19):** all **13 multi-alias ZP operands**
|
||
substituted in `prg.s` (via direct replacement, since `apply_symbols.py` only handles numeric operands);
|
||
**18 data tables/structures named** across all data regions (see the project README.md for table).
|
||
Build remains **BYTE-IDENTICAL**.
|
||
|
||
**Duck Hunt (JUE) Pass 2 resolution (2026-10-19):** the 441 remaining `Label_`
|
||
definitions were named fresh from the committed split — **30** semantic routine names
|
||
preserved from Pass 1, **428** `SubC<addr>` entry points for all remaining `jsr`/`jmp`
|
||
targets, and **293** `<Routine>_Block[_n]` internal branch targets. **18 semantic
|
||
zero-page aliases** added to `locals.inc` (`PtrLo/Hi`, `Ptr2Lo`, `GameState`,
|
||
`SndTemp`, `SndChannel`, `SndNote`, `SprData`, `Temp`, `CtrlState`, `CtrlTimer`,
|
||
`VramPtrLo/Hi`, `DataPtrLo`, `SndChan`, `SndPtrLo/Hi`, `FrameCounter`). Hardware register operands (68) substituted via
|
||
`apply_symbols.py`; zero-page multi-alias addresses need a `--sites` file for full
|
||
substitution. Build remains **BYTE-IDENTICAL**.
|
||
|
||
**Duck Hunt (JUE) Pass 3 completion (2026-10-19):** all **18 multi-alias ZP operands**
|
||
substituted in `prg.s` (via direct replacement, since `apply_symbols.py` only handles numeric operands);
|
||
**20 data tables/structures named** across all data regions (see the project README.md for table).
|
||
Build remains **BYTE-IDENTICAL**.
|
||
|
||
**Duck Hunt (JUE) promotion to top level (2026-10-19):** added `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg`, switched `make` to whole-ROM `cmp`, moved `_tools` paths. Build verified **Byte-identical ROM verified.**
|
||
|
||
### 5. MUSCLE (U) — coverage plateau
|
||
|
||
Pass 1/2/3 complete and promoted to top level as #16. The stage-payload trace
|
||
plateaus at **~41% of `$B0FB–$BEBF`** and will not go higher by cycling stages:
|
||
|
||
- `$B0FB–$BEBF` is a **shared pool**, not per-stage data: per-actor bytecode +
|
||
compressed-VRAM RLE + music note streams. `$BD28` is an 8-entry **per-actor**
|
||
entry-offset table (`$A063 adc $BD28,X`, X = ActorIndex `$1E`).
|
||
- Forcing all 8 actor indices added **zero** new bytes.
|
||
- The sound engine reads the same window (`$B03C lda (SoundReadPointerLo),Y` →
|
||
`cmp #$F0` → `lda $B0E3,y` = note decoding), which is where the `$FF` terminators come from.
|
||
- To go further needs **other music tracks playing** and **actors 2–4 in
|
||
non-default states** — i.e. scripted match play, not button taps. Out of scope
|
||
for a coverage tracer.
|
||
- Two corrections recorded: `$0326` is **not** a level byte (never leaves `$00`
|
||
even in a confirmed match), and `$0320` is a boolean, not a screen-state machine.
|
||
|
||
### 6. Documentation follow-ups (2026-10-19 sweep)
|
||
|
||
The sweep corrected the false aggregate claims — the `_wip` "Pass-0 skeleton" prose,
|
||
"`Pyramid (U) (Sachen)` is the only one with Pass 1", the Excitebike/Pac-Man audit-comment gap,
|
||
and the "`verify_pass1.py` checks all 36" scope — and normalized every project's
|
||
mirroring label to the header-byte convention. **All 30 top-level projects now also
|
||
have Pass 3 symbols applied (2026-10-19/20).** What the sweep did **not** do:
|
||
|
||
- **The stale `_wip/` project `README.md` files** (Hogan's Alley,
|
||
Mario Bros, `Pac-Man (Tengen) (U)` and Raid [a2] — `Popeye (JU) (PRG 1)` was promoted
|
||
2026-10-20 with its flow
|
||
README intact, and `Raid on Bungeling Bay (U)` left the tier 2026-10-20 with its
|
||
flow README promoted as-is, and `Pac-Man (U)` left it the same day with its flow
|
||
README promoted and updated) were
|
||
retitled and restatused to match their real sources, but still lack the power-on
|
||
flow analysis the promoted projects' `README.md`s carry — that is Pass-1 analysis work per
|
||
project, not a doc edit. (Lode Runner and Ice Climber (JE)/(U) were promoted to
|
||
top level and their `README.md`s now carry the flow analysis.)
|
||
- **`Donkey Kong 3 (JUE)` has no promoted-tier `README.md` at all** — **corrected 2026-10-20:
|
||
it does have one** (`NROM/Donkey Kong 3 (JUE)/README.md`, 318 lines, "Game Flow from
|
||
Power-On"). The claim was stale; the flow analysis exists.
|
||
- **`Golf (JU)` still uses the pre-canonical audit-comment spelling**
|
||
(`; C007: AD 02 20` instead of `; CPU:$C007 PRG:0007 AD 02 20`).
|
||
`audit_comment_tool.py` can bring it to canonical, but it has only been run
|
||
on top-level projects. **As of 2026-10-21 `Golf (JU)` is the only project
|
||
left in the whole tree with pre-canonical comments** — 4,227 of them, 0
|
||
canonical. `Pyramid (U) (Sachen)`, which used to be the second one, was fixed when
|
||
it was promoted. The gap is recorded in the project's own `README.md` and
|
||
in `WIP.md` §8.
|
||
- **Hogan's Alley's Pass 1 seed list is suspect.** Its `prg.s` header lists 55 routine
|
||
entries "recovered from table references"; `$E9F1` turned out to be a data-table
|
||
*base* (`lda $E9F1,x`), not a dispatch entry, and was forced back to data — the sole
|
||
build break found in the sweep (2026-10-19). 31 of the remaining 54 have no direct
|
||
`jsr`/`jmp` reference either, which is expected for indirect-dispatch targets but
|
||
leaves room for more table-base false positives. **Settled 2026-10-20** with
|
||
`cdl_reclass.py` against a merged Mesen CDL capture: 1,216 bytes moved from data to
|
||
code (41.8% → 49.1% code), five the other way. 17 of the 28 seed entries that no
|
||
`jsr`/`jmp` reaches turned out to be entries of the six **inline** dispatch tables
|
||
(the handler table is the data that follows the `jsr IndirectCall` that reads it,
|
||
so the tracer cannot see it); the other 11 are code the CDL never observed. See
|
||
`NROM/Hogan's Alley (JU)/README.md`.
|
||
- **Five `_wip` "Pass 2 complete" claims were false and are now corrected** (Donkey
|
||
Kong Jr. Math, Duck Hunt, Ice Climber (JE), Ice Climber (U), Pinball) in both the
|
||
project `README.md`s and the root inventory rows. Each had counted its *draft
|
||
map's rows* rather than names actually present in `prg.s`, and had described the
|
||
`locals.inc` count of **neutral `ZpXX` slots** as "zero-page semantic aliases".
|
||
Ice Climber (U) was worst: the sweep itself restated it "Pass 2 complete" while
|
||
describing its 971 `Label_` definitions in the same paragraph — a copy-paste from
|
||
(JE), and `(U)` has no map file at all. The drafted maps were then audited **row by
|
||
row against the committed split** and are **not applicable** (0 of 210–272 rows in
|
||
each are both insertable and referenced) — see §4 for the table and the method.
|
||
**Donkey Kong Jr. Math (U) has since had Pass 2 and Pass 3 completed for real (2026-10-19)**
|
||
with a fresh map derived from the committed split (64 semantic + 295 `SubC<addr>` +
|
||
676 `_Block` labels; 13 ZP aliases; 18 data tables named), and was **promoted to top level**.
|
||
**Duck Hunt (JUE) has since had Pass 2 and Pass 3 completed for real (2026-10-19)**
|
||
with a fresh map derived from the committed split (30 semantic + 428 `SubC<addr>` +
|
||
293 `_Block` labels; 18 ZP aliases; 20 data tables named), and was **promoted to top level**.
|
||
**Ice Climber (JE) and (U) have since had Pass 2 and Pass 3 completed for real (2026-10-20)**
|
||
with fresh maps derived from the committed split (196 semantic + 431 `_Block` labels;
|
||
2 ZP aliases; data tables named), and were **promoted to top level**.
|
||
**Lode Runner (U) has since had Pass 2 and Pass 3 completed for real (2026-10-20)**
|
||
with a fresh map derived from the committed split (196 semantic + 431 `_Block` labels;
|
||
2 ZP aliases; data tables named).
|
||
- **Mario Bros' entry-point counts were verified wrong** and corrected in the root
|
||
index, §2 and its `README.md`: the docs said "252 entry points (90 semantic + 165
|
||
`SubCxxxx`) / 477 sites / 156 neutral `ZpXX`"; `prg.s` actually holds **262** distinct
|
||
`jsr`/`jmp` targets (39 semantic + 223 `SubC<addr>`), **478** call sites, and **151**
|
||
neutral slots. Nothing had changed in `prg.s` since the initial commit, so the old
|
||
numbers were never right.
|
||
|
||
---
|
||
|
||
## Gotchas that will bite you
|
||
|
||
These cost real time this session. All are recorded so they don't cost it twice.
|
||
|
||
- **Undocumented opcodes silently truncate a trace.** A tracer knowing only the
|
||
151 official opcodes halts on the first one used as ordinary code. Joypad died
|
||
at `$C09E` on `DCP $3C20` (`$CF`) and lost 3,256 bytes of code.
|
||
`nesdis.py --undocumented` adds all 105 (default **off**, so existing
|
||
`make regen` pipelines stay byte-stable). Mario Bros: 14.6% → 14.9% from this
|
||
alone, but combined with seeding it mattered more.
|
||
- **ca65 V2.19 implements none of the undocumented opcodes.** They cannot be
|
||
written as mnemonics. `nesdis.py` emits them as explicit `.byte` with the
|
||
disassembly in the comment, keeping them in the code region so the split and the
|
||
audit trail survive.
|
||
- **RESET and IRQ are usually the same address.** The semantic-name map used to
|
||
overwrite, renaming the primary entry to `IrqHandler`. Now `setdefault`, in
|
||
RESET/NMI/IRQ priority order.
|
||
- **`render()` stamped every branch as `Label_XXXX`** and never reconciled with
|
||
the semantic map, so a branch into a named routine emitted a label that was
|
||
never defined — **the file did not assemble at all**.
|
||
- **Control-flow targets must win over fall-through.** A `bne` targeting `$C528`
|
||
was being swallowed by a fall-through `NOP #$A5` at `$C527` whose *operand* was
|
||
`$C528`. No assembler branches mid-instruction. Fixed in `nesdis.py`.
|
||
- **Branch targets inside a surviving instruction must be pruned** from the
|
||
target set, or `emit_code`'s linear walk consumes those bytes as an operand and
|
||
the label goes undefined.
|
||
- **NMI must fire once per frame, not per `$2002` read.** The main loop polls
|
||
`$2002` to sync on vblank, and each read clears bit 7 — which re-arms NMI in the
|
||
same scanline. Guarding on the vblank flag alone gave **40,900 NMIs across
|
||
3,000 frames** and silently corrupted all game state, flattening coverage at 26%.
|
||
- **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.
|
||
- **Vectors are not in the iNES header.** They are the three little-endian words
|
||
at the *end* of the 16 KB PRG (`$FFFA–$FFFF` in the `$C000` mirror). Bytes 0–9
|
||
of `rom.nes` carry only sizes, mirroring and mapper. The snippet to regenerate
|
||
the whole column is in the root `README.md`.
|
||
- **py65 is not a build dependency** and must be unpacked on demand:
|
||
`python3 -m pip download --no-deps -d <dir> py65 && unzip -q <dir>/py65-*.whl -d <dir>/py65lib`.
|
||
`/tmp` is volatile — a reboot wiped it once. Anything reusable belongs in `_tools/`.
|
||
- **ROM filenames contain spaces and parentheses.** the project folder's `rom.nes` is a
|
||
space-free copy/symlink kept for the build and the compare. Keep it working; don't rename it.
|
||
- **`.gitignore` lists `ROMS/*` but the directory is `_ROMS`**, so the pattern
|
||
matches nothing and ROM binaries stay tracked. Check before committing large blobs.
|
||
|
||
## Repo conventions that must survive
|
||
|
||
- Every instruction carries a trailing `; CPU:$XXXX PRG:XXXX AA BB` comment with
|
||
PRG offset and exact emitted bytes. **This is the byte-identity audit trail.**
|
||
Preserve it even while renaming labels or restructuring files. **All promoted
|
||
projects now use this canonical format** (standardized 2026-10-19; Popeye (JU)
|
||
(PRG 1) verified canonical at its 2026-10-20 promotion).
|
||
- Unreached or unresolved bytes stay as `.byte`/`.word`/`.incbin` with ROM
|
||
provenance. Never delete or reformat them away.
|
||
- Two separate symbol files: `symbols.inc` = NES hardware constants only;
|
||
`locals.inc` = zero page and forward references. Unresolved zero-page bytes keep
|
||
neutral `ZpXX` names rather than speculative ones.
|
||
- Routine headers follow the template in `prg.s`: global + local description,
|
||
parameters, returns, clobbers.
|
||
- Three passes, in order, without shortcuts: **Pass 1** code/data split (stop for
|
||
review) → **Pass 2** label replacement → **Pass 3** data tables. Do not skip
|
||
straight to guessy rewrites.
|
||
- `make regen` (where it exists) must keep reproducing `prg.s` byte-for-byte on
|
||
the standard library alone. MUSCLE has this pipeline; re-check it after any
|
||
`nesdis.py` change.
|
||
|
||
## Where the detail lives
|
||
|
||
- Root `README.md` (the old root `OVERVIEW.md`) — inventory of the 41 promoted projects, mapper-folder layout, per-project detail, the Pass 1
|
||
methodology note, and the audit-comment TODO with its full analysis.
|
||
- **`WIP.md`** — the pre-promotion tier: the 851 `_wip/` projects, their layout,
|
||
the two linker layouts (CPU address vs .nes file offset), the banked-mapper
|
||
/ no-CHR / trailing-bytes / odd-header cases, the open Pass 1/2/3 work, the
|
||
promotion procedure and the skeleton tooling.
|
||
- `<Project>/README.md` (formerly `<Project>/OVERVIEW.md`) — per-project architecture and pass status. The most
|
||
detailed ones: `MUSCLE (U)` (Pass 3b/3c/3d), `Joypad Test Cartridge (U)`,
|
||
`NROM/Mario Bros (JU)/`.
|
||
- `_tools/nesdis.py` — Pass 1 tracer (`--undocumented`, `--auto-seed`,
|
||
`--extra-seed`, `--table`, `--data`, `--probe`).
|
||
- `_tools/make_skeleton.py` — creates a byte-identical Pass 0 build skeleton
|
||
for a project folder (splits the ROM, writes the ca65 sources, the linker
|
||
config, the Makefile and the project README). It refuses to overwrite an
|
||
existing `prg.s` unless `--force` is given.
|
||
- `_tools/verify_pass1.py` — faithfulness + completeness check across all projects.
|
||
- `_tools/audit_comment_tool.py` — adds canonical `; CPU:$XXXX PRG:XXXX AA BB`
|
||
audit comments to disassembly source using tracer as ground truth. Usage:
|
||
`python3 _tools/audit_comment_tool.py "Project Name"`.
|
||
- `_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.
|
||
- `_tools/apply_symbols.py` — Pass 3 substitution: replaces numeric operands in `prg.s`
|
||
with the names from `symbols.inc`/`locals.inc`. Multi-alias addresses need a
|
||
`--sites line<TAB>name` file (per-routine resolution); unlisted ones are reported as
|
||
residue. Dry run by default, `--write` applies, and the audit comments are untouched.
|
||
Worked example: `Balloon Fight (JU)`.
|
||
- `_tools/align_audit_comments.py` — restores the audit-comment column after a
|
||
substitution. `nesdis.py` pads every instruction to 31 characters so the comment
|
||
starts at column 35; replacing an operand with a longer name pushes the comment
|
||
right and breaks the alignment the repo convention expects ("comment/audit
|
||
alignment is re-done after substitutions"). Whitespace-only, so the byte identity
|
||
cannot change. Dry run by default, `--write` applies. Data directives and lines
|
||
whose code text exceeds 31 characters are left alone. Worked example:
|
||
`NROM/Pac-Man (U)` (405 lines re-aligned after 119 substitutions).
|
||
- `_tools/derive_clobbers.py` — fills in the `; Clobbers:` block of every routine
|
||
header, so the headers carry the full template `AGENTS.md` describes (global
|
||
+ local description, parameters, returns, clobbers). Everything is derived,
|
||
never guessed: the opcode comes from the audit comment's byte list, so the
|
||
addressing mode is exact and a data directive is never mistaken for an
|
||
instruction; the operand is resolved through `locals.inc`/`symbols.inc` and
|
||
the labels in `prg.s`, so a clobber can be named (where several names share
|
||
one address, the first name in `locals.inc` wins); the control flow is
|
||
followed, so a `jsr` to another named entry is a call (the callee's clobbers
|
||
count) while a branch or a fall-through is not; and register preservation is
|
||
modelled with a small abstract interpreter, so a routine that saves a
|
||
register on the stack and puts it back is not reported as clobbering it.
|
||
The flags are named individually. Dry run by default, `--write` applies, and
|
||
it is idempotent. Worked example: `NROM/Balloon Fight (JU)` (63 of 63
|
||
headers).
|
||
- `_tools/apply_pass2_map.py` — applies a Pass 2 name map (`<CPU address><TAB><name>`)
|
||
to a project's `prg.s`: it renames the `Label_<addr>` definitions and their
|
||
references, and resolves a raw `$XXXX` operand on a `jsr`/`jmp` to the name of
|
||
the address it points at. A raw operand on a *data* reference is left alone -
|
||
it points at a table, not a routine. It refuses to write if a mapped address
|
||
has no label definition or if a rename would collide. Dry run by default,
|
||
`--write` applies. Worked example: `NROM/Pac-Man (Tengen) (U)` (192
|
||
definitions, 168 references, 48 raw call operands).
|
||
- `_tools/recode_region.py` — re-classifies a region of `prg.s` from data to
|
||
code by decoding it straight from the ROM. Use it when a dispatch table that
|
||
the traced code *does* read points into a region the tracer left as data -
|
||
the tracer cannot follow an indirect jump, so the handler it points at is
|
||
never reached. The bytes are the ROM's, so the build cannot change.
|
||
`--labels` takes a TSV of `<address><TAB><name>` for the routine entries.
|
||
Worked example: `NROM/Pac-Man (Tengen) (U)` (the six command handlers at
|
||
$F00A-$F081, including two `jmp` operands the tracer had mis-split).
|
||
- `_tools/name_data_region.py` — gives a data block a name, splitting the
|
||
`.byte` row when the name belongs in the middle of it (each half keeps its
|
||
own audit comment and no byte changes). Worked example:
|
||
`NROM/Pac-Man (Tengen) (U)` (28 tables).
|
||
- `_tools/derive_zp_aliases.py` — the script that derived the Pass 3 zero-page
|
||
alias set for `NROM/Pac-Man (Tengen) (U)`: it lists the aliases whose role is
|
||
the same at every reference and the per-site ones, writes the alias block for
|
||
`locals.inc` and the matching `sites.tsv`, and is idempotent. Keep it with
|
||
the tooling because it records how the aliases were chosen.
|
||
- `_tools/derive_branch_names.py` — derives the internal branch-target names of a
|
||
Pass 2 map from the branch that reaches them: the role says what has to be
|
||
true for the branch to be taken (`beq` -> `OnZero`, `bne` -> `OnNonzero`,
|
||
`bcc` -> `OnCarryClear`, `bcs` -> `OnCarrySet`, `bmi` -> `OnNegative`,
|
||
`bpl` -> `OnNonnegative`, `bvc`/`bvs` -> the overflow cases) and a
|
||
backwards branch is a `Loop`. The owner is the routine the target belongs
|
||
to, so the names read `ActorSearch_OnNonzero`. It merges the derived names
|
||
into the hand-written routine names and is idempotent. Worked example:
|
||
`NROM/Pyramid (U) (Sachen)` (42 derived names on top of 388 hand-written ones).
|
||
- `_tools/name_data_region.py` — gives a data block a name, splitting the
|
||
`.byte` row when the name belongs in the middle of it (each half keeps its
|
||
own audit comment and no byte changes). Worked example:
|
||
`NROM/Pyramid (U) (Sachen)` (32 tables).
|
||
- `_tools/apply_zp_aliases.py` — substitutes semantic zero-page aliases into
|
||
`prg.s` at the *specific* use sites. `apply_symbols.py` only rewrites numeric
|
||
operands, so it cannot rename an operand that is already written as a neutral
|
||
`ZpXX` name; this tool does that by direct textual replacement, either
|
||
everywhere (`--alias NAME=OLD`) or at one CPU address (`--at NAME=OLD@CPU`).
|
||
It keeps the ca65 `a:` prefix (dropping it would turn `sta a:Zp17,y` into a
|
||
zero-page store and change the bytes) and only rewrites the operand token, so
|
||
the audit comment survives byte for byte. Dry run by default, `--write`
|
||
applies. Worked example: `NROM/Raid on Bungeling Bay (U) [a2]` (69 lines).
|
||
|
||
---
|
||
|
||
## CDL capture + re-trace workflow (2026-10-21)
|
||
|
||
Goal: raise code coverage of a `_wip/` or promoted project from the
|
||
vector-seeded baseline (typically 3-10%) to 50%+ by feeding a Mesen Code
|
||
Data Logger capture back into the tracer.
|
||
|
||
### Prerequisites
|
||
- `ca65`/`ld65` on PATH, `python3`, Mesen emulator with MCP tools.
|
||
- The project must already be at Pass 1 (byte-identical build).
|
||
|
||
### Step 1 — CDL capture (Mesen)
|
||
1. Load the ROM: `mesen_load_rom(filepath=<project>/rom.nes)`.
|
||
2. Emulation is already running at 60 fps; CDL accumulates automatically.
|
||
Play through the game (menus, levels, game-over) so as many code paths
|
||
as possible execute. Use game-specific inputs (e.g., Slalom: Select
|
||
switches 1P/2P, D-pad changes course).
|
||
3. When done, save the CDL:
|
||
`mesen_cdl_file(action="save", memoryType="NesPrgRom", filepath="/tmp/opencode/<Name>.cdl")`.
|
||
4. Note the code-byte count from `mesen_get_cdl_statistics`.
|
||
|
||
### Step 2 — Extract CDL code-run seeds
|
||
```python
|
||
from pathlib import Path
|
||
raw = Path("/tmp/opencode/<Name>.cdl").read_bytes()
|
||
assert raw[:5] == b"CDLv2"
|
||
cdl = bytearray(raw[9:])[:PRG_SIZE]
|
||
starts = []
|
||
i = 0
|
||
while i < PRG_SIZE:
|
||
if cdl[i] & 1:
|
||
starts.append(BASE + i)
|
||
while i < PRG_SIZE and (cdl[i] & 1):
|
||
i += 1
|
||
else:
|
||
i += 1
|
||
Path("/tmp/opencode/<Name>_seeds.txt").write_text(",".join(hex(a) for a in starts))
|
||
```
|
||
`BASE` is `$8000` for CPU-layout NROM (32 KB) or `$C000` for 16 KB;
|
||
`PRG_SIZE` is the PRG byte count.
|
||
|
||
### Step 3 — Re-trace with CDL seeds
|
||
Back up `prg.s` and `locals.inc`, then:
|
||
```bash
|
||
seeds=$(cat /tmp/opencode/<Name>_seeds.txt)
|
||
python3 _tools/nesdis.py --prg prg.bin --base 0x8000 --project "<Name>" \
|
||
--out prg.s --locals locals.inc --extra-seed "$seeds" --auto-seed --undocumented
|
||
```
|
||
For banked projects (CNROM/MMC/etc.) use `banked_dis.py` with `--cdl` instead.
|
||
|
||
### Step 4 — Fix undefined labels
|
||
The re-trace may introduce `Label_XXXX` references that are undefined
|
||
(branch targets inside surviving instructions). Run:
|
||
```bash
|
||
python3 /tmp/scratchpad/fix_iter.py "<project dir>"
|
||
```
|
||
This iteratively assembles, parses "Symbol 'X' is undefined" errors, and
|
||
converts the offending lines to `.byte` (preserving the audit comment and
|
||
bytes). Repeat until `make` passes.
|
||
|
||
### Step 5 — Pass 2 (label replacement)
|
||
1. Generate a seed routinemap with the vector handlers:
|
||
```python
|
||
# extract Reset/NmiHandler/IrqHandler addresses from prg.s, write routinemap.tsv
|
||
```
|
||
2. Derive branch-target names:
|
||
`python3 _tools/derive_branch_names.py <proj> <proj>/routinemap.tsv "" <proj>/labelmap.tsv`
|
||
3. Apply the map:
|
||
`python3 _tools/apply_pass2_map.py <proj> <proj>/labelmap.tsv --write`
|
||
(Remove any map entries that fail with "no label definition" and re-run.)
|
||
4. Fill routine headers:
|
||
`python3 _tools/describe_routines.py <proj>/prg.s`
|
||
5. Align audit comments:
|
||
`python3 _tools/align_audit_comments.py <proj> --write`
|
||
|
||
### Step 6 — Pass 3 (symbols)
|
||
```bash
|
||
python3 _tools/apply_symbols.py <proj> --write
|
||
python3 _tools/align_audit_comments.py <proj> --write
|
||
```
|
||
|
||
### Step 7 — Verify and update docs
|
||
```bash
|
||
make # must print "Byte-identical ROM verified."
|
||
```
|
||
Update `README.md` Status line and `CHANGELOG.md` Pending entry. Commit.
|
||
|
||
### Coverage achieved so far (CDL-guided)
|
||
| Project | Coverage |
|
||
|---|---|
|
||
| Sqoon (U) | 53.4% |
|
||
| Elevator Action (U) | 57.1% |
|
||
| Slalom (U) | 26.4% |
|
||
| Kung Fu (U) | 37.1% |
|
||
| Othello (U) | 40.1% |
|
||
|
||
Games still pending (no CDL capture yet): 1942 (JU), Tag Team Wrestling (U),
|
||
Galaga (U), Kung Fu (U) (a1), Stack-Up (JU), Magmax (U), Ms Pac-Man (Namco) (U),
|
||
Soccer (JU), Tiger-Heli (U), Paperboy (U), Last Starfighter, The (U),
|
||
Mighty Bomb Jack (U), Kung-Fu Heroes (U).
|
||
|
||
### Key gotchas
|
||
- CNROM (and other flat-layout) projects use file-offset linker layout; Pass 2
|
||
label resolution breaks bytes unless the linker is first converted to CPU
|
||
layout (PRG start = $8000 for 32 KB). See the Tiger-Heli conversion.
|
||
- `apply_pass2_map.py` defaults to dry-run; always pass `--write`.
|
||
- `apply_symbols.py` defaults to dry-run; always pass `--write`.
|
||
- `fix_iter.py` lives in `/tmp/scratchpad/` and is not committed; recreate if
|
||
`/tmp` is wiped.
|
||
- Coverage below 50% is acceptable for the disassembly but the user's stated
|
||
bar for "traced" is 50%+; flag any project below that.
|