# 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. ## 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 (16 projects)** — `make` compares the whole ROM against `copilot/rom.nes`. - **`_wip/` (20 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. ## Current state - 16 top-level projects, all byte-identical. - 20 `_wip/` projects, all PRG-identical. `Pyramid (U)` is the only one with Pass 1. - 9 commits ahead of `origin/master`, nothing pushed. - `_tools/verify_pass1.py` rebuilds and re-traces everything: **all 36 faithful, none truncated.** ```bash python3 _tools/verify_pass1.py # all projects python3 _tools/verify_pass1.py "MUSCLE (U)" # one project ``` --- ## 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 — 252 subroutine entry points named:** - **90 semantic names**: audio/sound routines in `$F8xx-$F9xx` (clearly identifiable from APU register writes), gameplay helpers like `CheckZpB3Return`, `CheckRangeReturn012`, `ToggleZpFC_Bit3` - **165 address-based names** (`SubCxxxx`) for the remainder - All 252 `jsr`/`jmp` targets 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 1 complete, Pass 2/3 outstanding `_wip/Lunar Pool (U)/`. Pass 1 complete: **57.8% code** (4,586 instructions, 9,475 bytes), byte accounting exact, BYTE-IDENTICAL. - RESET is at **`$E80C`**, not `$C000` — the tracer must seed it explicitly (`--extra-seed 0xE80C`), since the vectors are the only entry points a plain trace follows. - Code is gated behind two dispatch mechanisms, both of the Mario Bros shape: `jmp ($7961)` at three sites (`$F788`/`$F7F6`/`$F828`, RAM-pointer dispatch — the data blocks after them hold the screen text "game over" and "hi`score__"), and stack-based dispatch at `$CBDC` (pointer arrives via `pla`). - 6,909 bytes (42.2%) still data. ~585 `Label_Cxxxx` to name in Pass 2. - Three undocumented opcodes this project forced into `nesdis.py`: `$72` (RRA abs), `$42` (NOP #imm), `$FA` (NOP abs,X). One emitter fix: ca65 optimises an operand with a `$00` high byte into zero-page form, so ABS/ABSX/ABSY/IND operands with high byte `$00` now emit the `a:` prefix to force absolute. ### 3. Audit comments for Excitebike and Pac-Man Both are hand-written sources with semantic labels but **no byte-identity audit comment**, so a source line cannot be mapped back to ROM bytes without re-assembling. Every other project has one. - `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. - **Blocker: ca65's listing has no source line numbers.** It emits `r ` with nothing tying entries to lines. - Three options, none free: (1) lockstep text alignment between listing and source — risky, the listing may truncate long lines and may omit blank lines; (2) re-derive offsets by decoding `prg.s` independently — a silent disagreement would corrupt offsets while still assembling; (3) transplant comments from a tracer-generated intermediate. - **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. - No tool was committed for this — the one written does not work. ### 4. Projects with `Label_` residue | Project | `Label_` definitions | refs in code | Note | |---|---|---|---| | Wild Gunman (U) | 718 | 539 | Pass 1 redone; Pass 2/3 pending | | Donkey Kong Jr. (JU) | 688 | 840 | Pass 1 verified; Pass 2/3 pending | | Balloon Fight (JU) | 497 | 711 | | | NES PowerPad Test Card (U) | 46 | 43 | otherwise Pass 3 complete | The other 12 top-level projects have **zero** `Label_` and are Pass 3 complete. Note these counts are address-derived labels only — some are legitimately internal branch targets and may never deserve a semantic name. ### 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. --- ## 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. - **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 py65 && unzip -q /py65-*.whl -d /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. (Note: no project actually uses this exact spelling — five variants are in use, and Excitebike and Pac-Man have none. See item 3.) - 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. - `/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/bytecode_dis.py`, `pass2.py`, `trace_stage_payload.py` — per-pass helpers.