Pass 3 named the 10 data regions in prg.s, then the project was promoted
out of _wip/ so `make` now proves header + PRG + CHR against the original
ROM instead of only the PRG bank.
Data regions named:
LevelPtrTable $C000-$C03B (60 B) level header data
ObjectPtrTable $C03C-$C043 (8 B) 4 pointer pairs
LevelDataPtrTable $C044-$C79D (1882 B) level data pointers
SpriteInitPtrTable labelled at $C4B0 see residue note
UnreachedCode_D650 $D650-$D696 (71 B) unreached code
UnreachedCode_E144 $E144-$E165 (34 B) unreached code
MusicDataTable $F55B-$FA47 (1261 B) music/sound data
APUDataTable $FAE6-$FB74 (143 B) APU pulse/noise tables
SfxDataTable $FE59-$FFEF (407 B) sound-effect data
VectorTable $FFF6-$FFFF (10 B) padding + NMI/RESET/IRQ
Promotion (per the AGENTS.md checklist):
- git mv "_wip/Donkey Kong (JU)" "Donkey Kong (JU)"
- add copilot/rom.nes -> ../Donkey Kong (JU).nes symlink
- add header.s / chr.s (one-line .incbin of header.bin / chr.bin)
- linker.cfg gains HEADER ($0000/16 B) and CHR ($0000/8 KB)
- Makefile switched from `cmp reconstructed_prg.bin prg.bin` to
`cmp reconstructed.nes rom.nes`
- no _tools path change (this Makefile never referenced _tools/)
and no `make regen` pipeline exists for it
Doc corrections:
- project OVERVIEW.md said Mirroring Vertical; header byte 6 = $00
means Horizontal (the root index was right) — fixed
- root OVERVIEW.md: row 18 now BYTE-IDENTICAL, tier paragraph and
summary table refreshed (they had drifted around the Lunar Pool and
Urban Champion promotions), new per-project detail entry
- counts corrected to 19 top-level / 17 _wip in OVERVIEW.md,
TODO.md and AGENTS.md
Residue recorded rather than guessed:
- Pass 2 is partial: 919 Label_ definitions remain (214 semantic
labels applied); noted in TODO.md so nobody reads the promotion as
full semantic naming
- SpriteInitPtrTable's label sits at $C4B0: the routine at $C807 reads
$C4A7,x/$C4A8,x, but that table starts mid-row and a label there
would split the canonical 16-byte audit-comment line
Verification:
make clean && make -> "Byte-identical ROM verified."
verify_pass1.py -> 19/19 top-level build, Donkey Kong (JU) OK
(6014 instr / 12505 B), all comments canonical
Doc-drift sweep: build tiers, mirroring, and stale status tails
Reconciled the docs against verified reality after the Lunar Pool / Urban
Champion / Donkey Kong (JU) promotions and the 2026-10-19 audit-comment
standardization. No source behaviour changed except the two build fixes below.
Build tier (whole-ROM vs PRG-only)
- AGENTS.md: the 17 `_wip/` projects are real disassemblies at the PRG-build
tier, not "skeletons"; header.s/chr.s/rom.nes are promotion additions;
verify_pass1.py covers the top-level projects.
- Root OVERVIEW.md: `_wip` prose, audit-comment narrative and summary table
refreshed (37.5% figure, SEEDED vs OK wording, re-trace false alarm).
- Promoted projects whose build blocks still claimed `make` emits
`reconstructed_prg.bin` compared to `prg.bin` (Balloon Fight, Baseball,
CIRCUS, Clu Clu Land, Excitebike, NES PowerPad, Pac-Man (USA), Tennis):
now describe the whole-ROM `reconstructed.nes` vs `rom.nes`.
- Lunar Pool / Urban Champion / CIRCUS also had stale `cd "_wip/..."`
build paths and "Promotion" items; retargeted to their top-level folders.
Mirroring (header byte 6 bit 0: 0 = Horizontal, 1 = Vertical)
- Corrected DK Jr. prose "horizontal mirroring (flag 6 = $01)" -> vertical,
and both `_wip/Pac-Man` variant notes "Both use vertical mirroring" ->
horizontal (both are byte-6 = $00).
- Re-audited every project OVERVIEW: 0 rows and 0 prose claims now disagree.
Stale status tails
- Retitled/restatused the `_wip` OVERVIEWs to their real `prg.s` (Golf,
Ice Climber (U), Lode Runner, Millipede, Pac-Man Tengen, Pac-Man (U),
Popeye 0/1, Raid, Raid a2, Hogan's Alley, Mario Bros); Golf's
skeleton-era contents rows and Next Steps corrected.
- Dropped the skeleton-era "## Next Steps (per `PROMPTS.md`)" heading and
the "(from the skeleton)" wording across all project OVERVIEWs.
- Rewrote Lunar Pool / Urban Champion Next Steps (both already Pass 1-3
complete and promoted; their tails still described pending Pass 2 and
promotion); fixed root row 13 (Wild Gunman) and row 20 (Duck Hunt).
Also lands two code fixes carried with this sweep:
- `_wip/Hogan's Alley (JU)`: $E9F1-$E9FD and $EA13-$EA14 reclassified as
data with canonical audit comments (the seeded build had decoded a data
table base as code, breaking the build since 2ae09f7). `make` ->
BYTE-IDENTICAL again; reconstructed_prg.bin/.lbl refreshed.
- `_wip/Pyramid (U)`: `Label_D72E` -> `NMI:` (byte-identical).
Verification:
make (Hogan's Alley, Pyramid) -> BYTE-IDENTICAL, BYTE-IDENTICAL
_tools/verify_pass1.py -> 19/19 build, all canonical,
"No truncated or unfaithful Pass 1"
mirroring audit (rows + prose) -> 0 mismatches
Donkey Kong (JU): complete Pass 2 — all 919 Label_ definitions named
The last non-zero Label_ residue in the repo is now gone. Every one of the
919 address-derived definitions in copilot/prg.s is replaced:
* 319 routine entry points (jsr/jmp targets):
- 102 named from observed behaviour (LoadSpriteInitPointerToZp00 $C807,
CopyVramDataToBuffer $CBDF, CheckSpriteListCollision $D8AD,
UpdateRandomSeed $F4ED, ReadControllers $F50E, UpdateSoundEngine $FA48, ...)
- 217 left as SubC<addr> — honest residue where the body is too
context-dependent to name without guessing (Golf: 108, Mario Bros: 223)
- the 77 already-named entries untouched
* 600 internal branch targets named <Routine>_Block / <Routine>_Block_n in
address order within their owning routine (DK Jr./Balloon Fight convention).
Ownership is control-flow containment, so a loop that physically precedes
its own entry lands correctly (FlushPpuBuffer_Block $F1EC under $F228).
* Two proposal collisions disambiguated by indexing zero-page slot:
UpdateObjectListZp5D/DispatchObjectStateZp5E ($DA16/$DA4C) vs
UpdateObjectListZpAE/DispatchObjectStateZpAF ($E1E9/$E250).
jsr/jmp operands stay numeric (Wild Gunman style); only branch operands are
symbolic. Applied with _tools/pass2.py apply (919 defs + 2,622 refs rewritten,
comments re-aligned); the ROM still rebuilds byte-identically and
verify_pass1.py reports 19/19 canonical with no truncated split.
Docs synced: project OVERVIEW.md gains a Pass 2 Details section, root
OVERVIEW.md rows + conventions, TODO.md section 4 closed. dk_full_map.tsv now
records the complete applied map (its zp rows remain unapplied proposals).
_wip status correction: five false "Pass 2 complete" claims + map audit
The `_wip` tier's "Pass 2 complete" headers counted their draft maps' rows, not
the names actually present in `prg.s`, and described the count of *neutral*
`ZpXX` slots in `locals.inc` as "zero-page semantic aliases". Verified against
the sources and corrected in the root inventory rows, the project OVERVIEWs and
TODO:
project named/total Label_ left drafted map
Pinball (JU) 180 / 681 501 210 rows, 179 applied
Ice Climber (JE) 76 / 973 897 255 rows, 76 applied
Donkey Kong Jr. Math 64 / 1035 971 243 rows, 62 applied
Duck Hunt (JUE) 31 / 472 441 272 rows, 31 applied
Ice Climber (U) 2 / 973 971 none - Pass 2 not started
No `_wip` project is Pass 2 complete. Ice Climber (U) was the worst case: the
2026-10-19 sweep itself restated it "Pass 2 complete" while describing its 971
`Label_` definitions in the same paragraph - a copy-paste from (JE); (U) has no
map file at all.
Drafted maps audited row by row against the committed split: they are not
applicable. 100/243 (DK Jr. Math), 96/255 (Ice Climber (JE)) and 17/210
(Pinball) rows point at addresses that are not instruction boundaries - they
were built against an older code/data split - while most of the rest are
unlabelled boundaries that no operand references, or (Duck Hunt) competing
candidate names for one label (`Label_C2BF` alone carries 205). Rows that are
both insertable and corroborated by control flow: 0 in every map. Recorded as
stale and kept for provenance; Pass 2 here needs a fresh map derived from the
committed split.
Also corrected while verifying:
- Mario Bros entry counts (docs said 252 / 90 + 165 / 477 sites / 156 neutral
slots; the file holds 262 distinct `jsr`/`jmp` targets = 39 semantic + 223
`SubC<addr>`, 478 sites, 151 slots).
- root `_wip` prose line/label ranges (4,482-17,180 lines, 192-971 `Label_`,
11,126 total) and the Ice Climber code figure (74.6% / 6,112 instr, not
74.7% / 6,113).
- TODO §4 re-scoped to the promoted tier, with the `_wip` residue table added.
Verification: `make` across all 17 `_wip` projects -> BYTE-IDENTICAL (17/17);
`_tools/verify_pass1.py` -> 19/19 build, all canonical, "No truncated or
unfaithful Pass 1 found". No source bytes changed.
Balloon Fight (JU): apply locals.inc/symbols.inc names to prg.s (Pass 3)
Substitutes symbol names for 922 numeric operands: 125 hardware registers
(symbols.inc) and 797 of 813 zero-page/RAM operands (locals.inc). The 15
multi-alias addresses were resolved per routine; 16 operands stay numeric
because $13 is used there as a plain high-byte scratch beside Temp12 and
none of its three aliases fits.
- _tools/apply_symbols.py: the substitution tool (dry run by default;
--sites supplies per-routine choices for multi-alias addresses, --report
lists the residue). Only the operand token is rewritten, so the
`; CPU:$.... PRG:.... XX XX` audit comments survive byte for byte.
- prg.s: operand tokens only. make prints "Byte-identical ROM verified."
- OVERVIEW.md (project): instruction quotes now match prg.s, new
alias-reuse table, and section 10 rewritten from the stale "497 Label_"
prose to the real naming state and the 16-site residue.
- OVERVIEW.md (root), TODO.md, AGENTS.md: status, counts, tool list.
verify_pass1.py: 19/19 OK, all audit comments canonical.
Bomberman (U): apply locals.inc/symbols.inc names to prg.s (Pass 3)
Substitutes 362 zero-page operands in copilot/prg.s with the names already
declared in copilot/locals.inc: 354 from single-name addresses and 8 from the
two multi-alias addresses, resolved per site:
$E0/$E1 IndirectPtrLo/Hi in the RAM-clear loop
(InitializeSystemAndRunMainLoop -> Main_WriteSoundDispatchIndex_C317,
which writes $00 across $0100-$07FF via (ptr),y),
SoundDelay0 (the base of the per-channel delay table, indexed by X)
in the sound engine;
No symbols.inc substitution was needed: copilot/prg.s contains no numeric
hardware operands ($2000-$4017) at all. All 362 operands were already written
with two hex digits, so no ca65 `a:` prefix was required.
make -> Byte-identical ROM verified. Operand token only; mnemonics, spacing and
the trailing `; CPU:$.... PRG:.... XX XX` audit comments are untouched.
Remaining numeric operands (758) address bytes that no .inc file names:
505 zero-page (133 addresses), 171 RAM $0100-$07FF (46 addresses) and 82
table reads in $8000+ (54 addresses).
Bomberman (U): substitute 24 in-file table labels at ROM operand sites
Uses the new `--labels` option of `apply_symbols.py` with the
ld65 `-Ln` output (`reconstructed.lbl`). Only labels that prg.s
actually defines and that live in the PRG (+) are considered,
so CHR/header segment labels cannot leak in.
24 numeric operands (`lda ,x`, `adc ,y`, etc.) now read
`lda SystemPaletteData,x`, `adc GameplayObjectClassTable,y`, etc.
All 24 point at exact label addresses; the remaining 58 ROM-space
numeric operands point at interior bytes or unlabelled tables and
stay numeric.
make -> Byte-identical ROM verified.
Bomberman (U): update docs for Pass 3 symbol application
- Project OVERVIEW.md: corrected source file from main.s to prg.s/header.s/chr.s;
output name from reconstructed_bomberman.nes to reconstructed.nes;
rewritten §10 Zero-Page/RAM Symbols table to match locals.inc (with aliases
and unnamed residue documented);
fixed §4 step 1/4 to use IndirectPtrLo/Hi and SoundDelay0 names;
fixed §11 InesHeader location (now header.s);
added §2 Pass 3 summary table;
added §13 notes on legacy main.s and Main_WriteSoundDispatchIndex_C317 misnomer.
- Root OVERVIEW.md §3: updated source line count (7,553), file list, and added
Pass 3 summary.
Both builds verified byte-identical.
Pass 3 symbol application to all 19 top-level projects (2026-10-19)
Ran _tools/apply_symbols.py --write on all 19 promoted projects to substitute
symbolic names from symbols.inc (hardware registers) and locals.inc (zero-page
RAM aliases, forward references) into prg.s operands. The tool preserves the
canonical audit comments (; CPU:$XXXX PRG:XXXX AA BB) and preserves byte
identity — all projects still rebuild byte-identical to their original ROMs.
Per-project operand substitutions:
Baseball (UE) 121 (85 single + 36 hardware)
Tennis (JU) 18 (18 single)
Pac-Man (USA) (Namco) 524 (524 single; 727 multi-alias residues)
CIRCUS 118 (38 single + 80 hardware; 23 multi-alias residues)
Wild Gunman (U) 76 (11 single + 65 hardware)
Joypad Test Cartridge (U) 47 (47 hardware)
NES PowerPad Test Card (U) 89 (89 hardware)
MUSCLE (U) 220 (91 single + 129 hardware)
Lunar Pool (U) 45 (18 single + 27 hardware)
Donkey Kong (JU) 75 (17 single + 58 hardware)
Urban Champion (JU) 70 (4 single + 66 hardware)
Already had symbols applied: Balloon Fight, Bomberman, Burger Time,
Clu Clu Land, Defender 2, Donkey Kong 3, Donkey Kong Jr.
Skipped: Excitebike (JU) — inline prg.s definitions conflict with locals.inc
values and source operands don't match ROM byte encodings; automatic
substitution breaks byte identity.
Documentation updated:
OVERVIEW.md — added Pass 3 symbol notes to all 19 project entries,
added Urban Champion as section #36
TODO.md — current state now reflects complete symbol application across
the promoted tier
Verification: python3 _tools/verify_pass1.py reports all 19 projects OK/SEEDED,
all canonical comments, no truncated or unfaithful Pass 1.
502 lines
32 KiB
Markdown
502 lines
32 KiB
Markdown
# TODO / Handoff
|
||
|
||
Written so a fresh session can pick this up cold. Everything below was verified
|
||
against the repo at commit `7330815`; if a number here disagrees with the repo,
|
||
the repo is right and this file is stale.
|
||
|
||
> **Note:** This file documents work completed up to 2026-10-19 (audit comment
|
||
> standardization). The actual repo state is always the source of truth.
|
||
|
||
## The contract
|
||
|
||
Byte-identical rebuild is the hard requirement. In every project's `copilot/`:
|
||
|
||
```bash
|
||
make # must print "Byte-identical ROM verified." (or "BYTE-IDENTICAL" in _wip/)
|
||
```
|
||
|
||
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 (19 projects)** — `make` compares the whole ROM against `copilot/rom.nes`.
|
||
- **`_wip/` (17 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. A promoted project also
|
||
needs `header.s`, `chr.s` and a HEADER/PRG/CHR `linker.cfg`, which no `_wip/`
|
||
project carries (they keep the header/CHR only as the raw `header.bin`/`chr.bin`
|
||
splits) — `Lunar Pool (U)` (2026-10-18) and `Donkey Kong (JU)`
|
||
(2026-10-19) are the worked examples. DK (JU) needed no `_tools` path change:
|
||
its Makefile never referenced `_tools/`, and it has no `make regen` target.
|
||
|
||
## Current state
|
||
|
||
- 19 top-level projects, all byte-identical (Lunar Pool promoted 2026-10-18,
|
||
Donkey Kong (JU) promoted 2026-10-19 after Pass 3).
|
||
- 17 `_wip/` projects, all PRG-identical and all at Pass 1 or beyond. `Pyramid (U)`
|
||
is *not* the only one with Pass 1 done — every one has it. Pass 2 is complete for
|
||
Donkey Kong Jr. Math, Duck Hunt, Ice Climber (JE), Ice Climber (U) and Pinball;
|
||
Golf (JU) and Mario Bros (JU) have substantial Pass 2 work. The inventory rows in
|
||
`OVERVIEW.md` are authoritative.
|
||
- Local `master` is ahead of `origin/master`; nothing pushed.
|
||
- `_tools/verify_pass1.py` covers the **19 top-level projects** (it selects folders
|
||
with `copilot/rom.nes`), not all 36 — the `_wip/` tier is checked by plain `make`.
|
||
Current run (2026-10-19): 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 19 top-level projects
|
||
python3 _tools/verify_pass1.py "MUSCLE (U)" # one project (top-level only)
|
||
```
|
||
|
||
- **Audit comment standardization complete (2026-10-19):** All 19 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 19 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)
|
||
- Excitebike (JU): skipped — inline defs in `prg.s` conflict with `locals.inc`
|
||
|
||
---
|
||
|
||
## 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 project OVERVIEW.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
|
||
|
||
`_wip/Mario Bros (JU)/`. 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
|
||
|
||
`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:** no `_wip/` project has `header.s`, `chr.s` or a
|
||
HEADER/PRG/CHR `linker.cfg` (the header and CHR stay as the raw `header.bin`/`chr.bin`
|
||
splits), 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.
|
||
- **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 — **COMPLETE** (2026-10-19)
|
||
|
||
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.
|
||
|
||
- `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.
|
||
|
||
### 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 |
|
||
| **All 19 promoted** | **0** | **0** | **All Pass 3 complete, symbols applied 2026-10-19** |
|
||
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).** `copilot/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.
|
||
|
||
**All 19 top-level projects now have symbols applied (2026-10-19).** See the Current state section above for the per-project breakdown. Excitebike (JU) was skipped due to inline `prg.s` definitions conflicting with `locals.inc` values.
|
||
|
||
The `_wip` tier is still open. All 17 `_wip/` projects carry address-based
|
||
`Label_` definitions (**192–971 each, 11,126 in total**) and none has semantic
|
||
zero-page aliases. 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 `_wip` OVERVIEWs 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`) |
|
||
| Ice Climber (JE) | 76 / 973 | 897 | 255 / 76 / 96 / **0** |
|
||
| Donkey Kong Jr. Math (U) | 64 / 1,035 | 971 | 243 / 62 / 100 / **0** |
|
||
| Duck Hunt (JUE) | 31 / 472 | 441 | 272 / 31 / 210 conflicts / **0** |
|
||
| Ice Climber (U) | 2 / 973 | 971 | none — Pass 2 not started |
|
||
|
||
**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`.
|
||
|
||
### 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)` 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 19 top-level projects now also
|
||
have Pass 3 symbols applied (2026-10-19).** What the sweep did **not** do:
|
||
|
||
- **The 11 stale `_wip/` project `OVERVIEW.md` files** (Hogan's Alley, Ice Climber (U),
|
||
Lode Runner, Mario Bros, Millipede, both Pac-Man, both Popeye, both Raid) were
|
||
retitled and restatused to match their real sources, but still lack the power-on
|
||
flow analysis the top-level `OVERVIEW.md`s carry — that is Pass-1 analysis work per
|
||
project, not a doc edit.
|
||
- **`Donkey Kong 3 (JUE)` has no top-level `OVERVIEW.md` at all** (root line 3 now
|
||
says so); writing it needs the same flow analysis.
|
||
- **`Golf (JU)` and `Pyramid (U)` still use pre-canonical audit-comment spellings**
|
||
(`; C007: AD 02 20` and `; PRG:$172E 48`). `audit_comment_tool.py` can bring them to
|
||
canonical, but it has only been run on top-level projects.
|
||
- **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. Settle with `cdl_reclass.py`.
|
||
- **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 `OVERVIEW.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.
|
||
- **Mario Bros' entry-point counts were verified wrong** and corrected in the root
|
||
index, §2 and its `OVERVIEW.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 `OVERVIEW.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.** `copilot/rom.nes` is a
|
||
relative symlink to a space-free name. 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 19 top-level
|
||
projects now use this canonical format** (standardized 2026-10-19).
|
||
- 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 `OVERVIEW.md` — inventory of all 36 projects, per-project detail, the Pass 1
|
||
methodology note, and the audit-comment TODO with its full analysis.
|
||
- `<Project>/OVERVIEW.md` — per-project architecture and pass status. The most
|
||
detailed ones: `MUSCLE (U)` (Pass 3b/3c/3d), `Joypad Test Cartridge (U)`,
|
||
`_wip/Mario Bros (JU)`.
|
||
- `_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/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)`.
|