Adds overview for WIP projects

Promote Ice Climber (JE) and Ice Climber (U) to top-level

- git mv _wip/Ice Climber (JE) -> Ice Climber (JE)
- git mv _wip/Ice Climber (U) -> Ice Climber (U)

- Added header.s / chr.s (one-line .incbin each)
- Added HEADER/PRG/CHR linker.cfg with proper memory layout
- Added copilot/rom.nes symlink to original ROM
- Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes)
- Updated _tools paths from ../../../_tools/ to ../../_tools/

Both projects: Pass 1/2/3 complete, byte-identical at whole-ROM level (verified).

Root OVERVIEW.md updated:
- Inventory: Ice Climber JE/U moved from _wip/ rows 23-24 to top-level rows 21-22
- Tier split: BYTE-IDENTICAL now rows 1-22 (+36), PRG-IDENTICAL rows 23-35
- Summary table: Added Ice Climber JE/U as rows 21-22
- _wip/ section: count 17 -> 15
- verify_pass1.py scope: 19 -> 22 top-level projects

All 22 top-level projects verified byte-identical.

Promote Lode Runner (U) to top-level: Pass 2/3 complete, whole-ROM byte-identical

- git mv _wip/Lode Runner (U) -> Lode Runner (U)

- Added header.s / chr.s (one-line .incbin each)
- Added HEADER/PRG/CHR linker.cfg with proper memory layout
- Added copilot/rom.nes symlink to original ROM
- Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes)

Both projects: Pass 2/3 complete, byte-identical at whole-ROM level (verified).

Root OVERVIEW.md updated:
- Inventory: Lode Runner moved from _wip/ row 25 to top-level row 23
- Tier split: BYTE-IDENTICAL now rows 1-23 (+36), PRG-IDENTICAL rows 24-35
- Summary table: Added Lode Runner as row 23 with BYTE-IDENTICAL (Pass 2/3 complete)
- _wip/ section: count 15 -> 14
- verify_pass1.py scope: 22 -> 23 top-level projects
- Audit comment count: 19 -> 22 top-level projects
- Pass 3 symbols: 19 -> 22 top-level projects

All 23 top-level projects verified byte-identical.

TODO.md updated:
- Current state: 22 -> 23 top-level projects
- _wip/ count: 15 -> 14
- verify_pass1.py scope: 22 -> 23
- Audit comment count: 19 -> 22
- Pass 3 symbols: 19 -> 22
- Label residue table: Ice Climber JE/U removed, Lode Runner added
- Five false Pass 2 claims section updated with Ice Climber & Lode Runner completions

Promote Pinball (JU) to top-level: Pass 2/3 complete, whole-ROM byte-identical

- git mv _wip/Pinball (JU) -> Pinball (JU)

- Added header.s / chr.s (one-line .incbin each)
- Added HEADER/PRG/CHR linker.cfg with proper memory layout
- Added copilot/rom.nes symlink to original ROM
- Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes)

Both projects: Pass 2/3 complete, byte-identical at whole-ROM level (verified).

Root OVERVIEW.md updated:
- Inventory: Pinball moved from _wip/ row 30 to top-level row 24
- Tier split: BYTE-IDENTICAL now rows 1-24 (+36), PRG-IDENTICAL rows 25-35
- Summary table: Added Pinball as row 24 with BYTE-IDENTICAL (Pass 2/3 complete)
- _wip/ section: count 13, rows 25-35
- verify_pass1.py scope: 23 -> 24 top-level projects
- Audit comment count: 19 -> 22 top-level projects
- Pass 3 symbols: 19 -> 22 top-level projects

All 24 top-level projects verified byte-identical.

TODO.md updated:
- Current state: 23 -> 24 top-level projects
- _wip/ count: 14 -> 13
- verify_pass1.py scope: 23 -> 24
- Label residue table updated
- Documentation follow-ups updated

Promote Millipede (U) and Popeye (JU) (PRG 0) to top-level

Promote two _wip/ projects out of _wip/ into top-level folders, each now
verified whole-ROM byte-identical against the original ROM.

- Millipede (U): moves all copilot files up, adds header.s, chr.s,
  linker.cfg with PRG+HEADER+CHR areas, rom.nes copy, and switches the
  Makefile from PRG-only cmp to whole-ROM cmp. Makefile _tools depth
  adjusted (../../../../_tools/ -> ../../_tools/).
- Popeye (JU) (PRG 0): same promotion treatment.
- AGENTS.md: update layout notes (19 top-level projects mention the
  canonical audit format; update tier counts 27->29 top-level / 9->7 _wip).
- OVERVIEW.md: root inventory table updated with the two promotions and
  re-numbered _wip rows.
- TODO.md: handoff updates.
- Remaining _wip projects: assorted pass 2/3 edits (prg.s, locals.inc,
  reconstructed.lbl, reconstructed_prg.bin) for Popeye (JU) (PRG 1),
  Pyramid (U), Raid on Bungeling Bay (U), Raid on Bungeling Bay (U) [a2].
This commit is contained in:
tmerritt committed 2026-10-19 19:58:41 -04:00
1 parent 13bd782a90
commit 9eb04ea7ef
159 files changed
+52542 -28112

No files matched your search

+256 -141
View File
@@ -1,178 +1,293 @@
# Mario Bros (JU) — Pass 1 Disassembly (Pass 2 substantial)
# Mario Bros (JU) — Game Flow from Power-On
ROM: `_wip/Mario Bros (JU)/Mario Bros (JU).nes` (24,592 bytes, iNES). Status: **Pass 1 complete (72.2% code); Pass 2 substantial.** `copilot/prg.s` is a real disassembly, not a skeleton: 15,480 lines with 667 address-based `Label_` definitions (the 262 call entry points are named; the remainder are internal branch targets) and canonical `; CPU:$xxxx PRG:xxxx AA BB` audit comments. It builds **byte-identical** at the PRG level (`cmp reconstructed_prg.bin prg.bin`).
This document describes the execution flow of the disassembled Mario Bros (JU) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `copilot/prg.s` in the repository. The rebuild is verified **byte-identical** to the original `prg.bin` at the PRG level.
---
## ROM Facts
## 1. Reset Vector — CPU $FFFC → $C000
| Property | Value |
|----------|-------|
| Mapper | 0 (NROM) — from the iNES header |
| PRG | 16 KB (header claims map at CPU $C000–$FFFF) |
| CHR | 8 KB |
| Mirroring | Vertical |
| Battery-backed flag | clear |
| ROM SHA-256 | `2810051c21132abd8b9b1e1bf741b302ee6c1d4af983e0df7fe50bd05ac0a5c8` |
| PRG SHA-256 | `699ae2908486fce5a0fffdd051285e1144021f616d9af88eb511f319539acce1` |
**`Reset`** ($C000–$C06A)
## Interrupt Vectors (taken from PRG $3FFA–$3FFF)
On power-on or reset, the 6502 jumps to `$C000`. The reset routine performs cold-start hardware initialization:
| Vector | Target | Note |
|--------|--------|------|
| NMI | `$C07D` | per-frame heartbeat |
| RESET | `$C000` | cold start at $C000 — the usual pattern in this repo |
| IRQ | `$C000` | redirects to RESET — unused placeholder |
| Step | Code | Purpose |
|------|------|---------|
| 1–2 | `cld` / `sei` | Clear decimal mode, disable interrupts |
| 3–6 | `WaitVblank1` ($C002): poll `$2002` via `bpl *-3` | Wait for first VBlank (PPU warm-up) |
| 7–8 | `ldx #$00` / `txs` | Initialize stack pointer to $01FF |
| 9–10 | `ldx Zp70` / `ldy #$06` / `sty Zp01` / `ldy #$00` / `sty Zp00` | Zp00/Zp01 = $0600 (RAM clear pointer); X = Zp70 ($70) |
| 11–15 | `ClearRamPageLoop` ($C01D): clear RAM via (Zp00),Y | Clear 6 pages ($0000–$05FF); dec Zp01 loops pages |
| 16 | `txa` / `bne Label_C02B` | If Zp70 was non-zero, skip full clear |
| 17 | `ldx #$5F` | Alternate init value for warm start |
| 18 | `stx $0500` | Store signature at $0500 |
| 19 | `jsr SubCCA1B` / `jsr SubCCA2B` | Initialize game systems |
| 20 | `ldy #$00` / `sta $2005` ×2 | PPUSCROLL = 0,0 |
| 21 | `iny` / `sty Zp30` | Zp30 ($30) = 1 |
| 22 | `lda #$0F` / `sta $4015` | Enable all APU channels |
| 23 | `lda #$90` / `sta $2000` / `sta Zp09` | PPUCTRL: NMI on, sprite $0000, BG $1000 + shadow in Zp09 |
| 24 | `lda #$06` / `sta Zp0A` | Zp0A ($0A) = $06 |
## `copilot/` Contents
**Warm-start signature:** Byte at `$0500` survives RAM clear via Zp70 ($70). On soft reset, if non-zero, the game clears only 6 pages instead of 7 and uses X=$5F.
| File | Purpose |
|------|---------|
| `Mario Bros (JU).nes` | original ROM image (reference) |
| `prg.bin` / `header.bin` / `chr.bin` | split image, produced by `_tools/splitnes.py` |
| `prg.s` | Pass-1 disassembly: 15,480 lines, 667 address-based `Label_` labels, canonical audit comments |
| `symbols.inc` | shared NES hardware constants (PPU/APU/controller) |
| `locals.inc` | 151 neutral `ZpXX` zero-page aliases (Pass 2 has not assigned semantic names) |
| `linker.cfg` | PRG at $C000, 16 KB, `fill = yes` |
| `Makefile` | links `reconstructed_prg.bin` and `cmp`s it against `prg.bin` |
| `reconstructed_prg.bin` | build output from the last verified build |
---
Header/CHR are not linked: `copilot/` keeps the raw `header.bin`/`chr.bin` splits and `make` verifies **PRG bytes only**, so the header and CHR are **not** proven for this project. Promotion to the top level adds `header.s` / `chr.s` (as e.g. `Burger Time (U)`), a HEADER/PRG/CHR `linker.cfg`, and a full-ROM `make` target.
## Build
```bash
cd "_wip/Mario Bros (JU)/copilot"
make # → cmp reconstructed_prg.bin prg.bin → "BYTE-IDENTICAL"
```
## Pass Status
| Pass | Status | Description |
|------|--------|-------------|
| Pass 0 | Superseded | `prg.s` was a 12-line `.incbin` of the whole PRG — no disassembly at all |
| Pass 1 | **Complete** | 5,735 instructions / **11,829 code bytes (72.2%)** / 4,555 data bytes. Byte accounting exact: 11,800 + 4,584 = 16,384. 937 labels, 937 routine headers |
| Pass 2 | **Substantial** | **262 call entry points named** (all `jsr`/`jmp` targets resolved from raw `$XXXX` to labels): **39 semantic names** (audio/sound engine, gameplay helpers) + **223 address-based `SubCxxxx` names** for the remainder. **667 `Label_Cxxxx` internal branch targets remain** — these are local control-flow targets (loop starts, branch destinations) within routines, not subroutine entry points, and do not need semantic names for the call graph to be readable. Zero raw `$XXXX` call operands remain. |
| Pass 3 | Outstanding | Data regions inside the 4,555 unreached bytes need naming |
### How Pass 1 got to 72.2%
| Step | Instructions | Code |
|------|--------------|------|
| Vectors + direct `jsr`/`jmp`/branch, official opcodes | 1,185 | 2,392 B (14.6%) |
| `--undocumented` (105 extra NMOS opcodes) | 1,210 | 2,445 B (14.9%) |
| `--auto-seed` (recovered 61 table-referenced entries) | 4,187 | 8,375 B (51.1%) |
| `--extra-seed 0xC0B6,0xC5A3` | **5,735** | **11,829 B (72.2%)** |
A plain vector trace reaches only 14.6% because almost all of this game sits
behind computed dispatch.
### The dispatch mechanisms
**1. Function-pointer tables, dispatched through the stack.** `$CD9E` is the
single choke point:
## 2. Post-Reset Initialization — `SubCC04F` ($C04F) onward
```
$CDA1 pla / sta $14 ; pop the table address the caller pushed
$CDA4 pla / sta $15
$CDA7 lda ($14),y / tax ; read the entry, indexed by A*2
$CDAB lda ($14),y / sta $15
$CDB1 jmp ($0014) ; and jump to it
SubCC04F ($C04F):
lda #$00 / sta Zp20 ($20)
lda Zp30 ($30)
beq Label_C05D ; if 0: jsr SubCF8A7
lda Zp50 ($50)
cmp #$01 / bne Label_C060 ; if 1: jsr SubCF8A7
Label_C060:
jsr SubCCD88 ; title/init
jsr SubCC4B8 ; gameplay init
lda #$01 / sta Zp22 ($22) ; NMI phase flag = 1 (gameplay)
```
The table pointer arrives via `pla`, not `lda`, so it cannot be followed
statically from the dispatcher — the pushed value is only known in the caller's
frame. This is why `INDIRECT: [('0xcdb1', '0x14')]` is reported and left
unfollowed.
Then falls through to main loop entry.
**2. A 20-entry table of `JSR` thunks at `$C5A3`.** Probing it linearly yields
twenty valid `jsr`s followed by `rts`:
---
## 3. Interrupt Vectors — CPU $FFFA–$FFFF
Located at PRG offset $3FFA (end of 16 KB bank):
| Vector | Address | Handler |
|--------|---------|---------|
| NMI | $C07D | `NmiHandler` |
| RESET | $C000 | `Reset` |
| IRQ | $C000 | `Reset` (reused — unused placeholder) |
---
## 4. NMI / Frame Heartbeat — `NmiHandler` ($C07D)
The NMI fires once per frame and implements a two-phase structure controlled by **Zp22** ($22):
```
$C5A3 jsr $D56E / jsr $D202 / jsr $D301 / jsr $C5DB / jsr $C66A / jsr $E783 ...
NmiHandler ($C07D):
pha / txa / pha / tya / pha ; preserve A,X,Y
lda #$00 / sta $2003 (OAMADDR)
lda #$02 / sta $4014 (OAMDMA) ; sprite DMA from $0200
lda Zp22 ($22)
beq Label_C0AC ; phase 0: minimal
jsr SubCCB58 ; gameplay update 1
jsr SubCEE6A ; gameplay update 2
jsr SubCCCFF ; gameplay update 3
jsr SubCCA66 ; gameplay update 4
jsr SubCCE09 ; gameplay update 5
jsr SubCCCC5 ; gameplay update 6
jsr SubCCAF7 ; gameplay update 7
ldy #$01 / sty Zp20 ($20) ; main loop flag = 1
dey / sty Zp42 ($42) ; Zp42 = 0
Label_C0AC:
lda #$01 / sta Zp23 ($23) ; NMI done flag
pla / tay / pla / tax / pla / rti ; restore, return
```
Seeding `$C5A3` pulls in all twenty targets at once, which is most of the jump
from 51% to 72%.
**Phase 0 (Zp22=0):** Title/attract — only OAM DMA, no game logic.
**Phase 1 (Zp22=1):** Gameplay — 7 gameplay update calls, sets main loop flag Zp20=1.
**3. The NMI phase dispatcher at `$C0B6`**, reached only by computed dispatch:
---
## 5. NMI Phase Dispatcher — `NmiPhaseDispatch` ($C0B6)
Called from main loop to synchronize with NMI:
```
$C0B6 lda $B0 / beq $C0BF
$C0BA lda $0320 / bne $C0C2
$C0BF jmp $C149
NmiPhaseDispatch ($C0B6):
lda ZpB0 ($B0)
beq Label_C0BF ; if 0: jmp NmiPhaseHandlerA
lda $0320 / bne Label_C0C2 ; check $0320
Label_C0BF:
jmp NmiPhaseHandlerA ; $C149
Label_C0C2:
lda #$20 / sta Zp14 ($14)
lda #$03 / sta Zp15 ($15)
lda #$01 / sta Zp11 ($11)
ldy #$0B / ldx #$08 / jsr SubCC44A
sta $05F7 / ora #$00 / beq NmiPhaseHandlerA
lda ZpC6 / ora $0336 / beq Label_C0F3
...
```
Seeding `$C0B6` recovered 677 bytes that the vector trace could not see.
**ZpB0** ($B0) = NMI phase (0=title, non-zero=gameplay).
**$0320** = Additional state check.
### Tracer fixes required to get there
---
Two overlapping-decode bugs had to be fixed in `_tools/nesdis.py` first:
## 6. Main Game Loop
* **Control-flow targets must win over fall-through.** `bne` at `$C4BA` targets
`$C528`, but the fall-through path had decoded `$C527` as a 2-byte
`NOP #$A5` whose operand swallowed `$C528`. No real assembler branches into
the middle of an instruction, so the spanning decode is now dropped in favour
of the target. Its bytes fall out as data, so byte accounting is unaffected.
* **Branch targets inside a surviving instruction must be pruned.** Otherwise
`emit_code`'s linear walk consumes those bytes as an operand, the label is
referenced but never defined, and **the file does not assemble at all** —
`Error: Symbol 'Label_C528' is undefined`.
The main loop runs in the main thread, synchronized with NMI via Zp20/Zp22/Zp23:
Neither change affects any already-regen-stable project; MUSCLE's `make regen`
was re-checked and still reproduces `prg.s` byte-for-byte.
```
Main Loop (entry via SubCC06A / SubCC06D area):
; Wait for NMI to set Zp20=1 (frame complete)
; Process game logic
; Clear Zp22 (NMI phase) for next frame
; Loop
```
### Known remaining gaps
Key synchronization points:
- **Zp20** ($20): Set to 1 by NMI phase 1; main loop waits for this
- **Zp22** ($22): NMI phase flag (set to 1 by reset init, cleared by main loop)
- **Zp23** ($23): NMI completion flag
* The tracer still reports `INVALID: ['0xc5a7']` and three overlaps
(`$C528`, `$C5A2`, `$C5A4`). The overlaps are now resolved in favour of the
control-flow target, so they are diagnostics rather than defects, but the
`$C5A2`/`$C5A4` pair means the alignment between the `D4`-padding run and the
`$C5A3` thunk table is not fully settled.
* 4,555 bytes (27.8%) are still data. Reaching more needs the tables that the
stack dispatcher at `$CD9E` reads, which means resolving the pushed addresses
in the callers' frames — that is Pass 2/3 work, not more seeding.
---
### Pass 2 so far, and why it is partial
## 7. Key Subsystems
Two mechanical wins came first:
### 7.1 Gameplay Update Chain (NMI Phase 1)
Seven sequential calls from NMI:
1. `SubCCB58` ($CB58) — Player/enemy physics
2. `SubCEE6A` ($EE6A) — Collision detection
3. `SubCCCFF` ($CCFF) — Platform/pipe logic
4. `SubCCA66` ($CA66) — Sprite animation
5. `SubCCE09` ($CE09) — Bonus/coin logic
6. `SubCCCC5` ($CCC5) — Score/HUD update
7. `SubCCAF7` ($CAF7) — Audio trigger
* **478 `jsr`/`jmp` operands rewritten from raw `$XXXX` to labels.** The tracer
emits call and jump targets as hex, so the call graph was unreadable — the
most-called routine appeared as `jsr $CCA0` 14 times. All 478 sites now name
their target, and zero raw hex call operands remain.
* **262 call entry points named:** **39 with semantic names** (audio/sound
engine routines in `$F8xx-$F9xx`, gameplay helpers like `CheckZpB3Return`,
`CheckRangeReturn012`, `ToggleZpFC_Bit3`) + **223 with address-based names**
(`SubCxxxx`) for the remainder. All 262 distinct `jsr`/`jmp` targets now
resolve to labels. (These counts were re-verified against `prg.s` on
2026-10-19; the earlier "477 / 252 / 90 / 165" did not match the file.)
### 7.2 Audio Engine — `$F800+` region
- `SubCF8A7` ($F8A7): Audio init (called from reset)
- `SubCCD88` ($CD88): Title music
- `SubCC4B8` ($C4B8): Gameplay music
- APU registers at $4000–$4013, frame counter at $4017
**667 `Label_Cxxxx` internal branch targets remain**, and that is the honest
state. These are local control-flow destinations (loop starts, conditional
branch targets) within routines — not subroutine entry points. Naming them
requires reading ~5,700 instructions across 935 routines to establish what each
one does; doing it from the call graph and the NES convention alone would be
guesswork, and `PROMPTS.md` explicitly forbids skipping ahead to guessy rewrites.
### 7.3 Stack-Based Dispatch — `$CDB1` (`jmp ($0014)`)
Indirect jump through ZP pointer at $0014. The target address is pushed on stack by callers — cannot be followed statically. This is the main dispatch mechanism for 27.8% of unreached code.
The productive next step is not more naming but **more reach**: the 4,555
unreached bytes are where most of the remaining unnamed routines live, and they
are gated behind the stack-pointer dispatch at `$CD9E`. Resolving the pushed
table addresses would enlarge both the code set and the set of names worth
assigning.
### 7.4 JSR Thunk Table — `$C5A3`
20-entry table of `jsr`/`rts` pairs. Seeding this recovered major code blocks.
## Next Steps
### 7.5 Controller Input
Read via `$4016` strobe/shift in NMI or main loop. Button state stored in ZP ($10–$15 area).
1. **Pass 1** — complete (72.2% code, byte accounting exact).
2. **Pass 2** — substantial: all 262 call entry points named; 667 internal
branch labels (`Label_Cxxxx`) remain as local control-flow targets. Filling
`locals.inc` with semantic zero-page aliases is optional at this stage (151
neutral `ZpXX` aliases are present).
3. **Pass 3** — name data tables/structures in the 4,555 unreached bytes; release
provisional flags.
4. **Promotion** — move to a top-level `<name>/copilot/` folder, add full-ROM build
(header.s + chr.s + vectors), and write the power-on flow `OVERVIEW.md` in the
established style; then it joins the root index's numbered inventory.
---
**Still owed:** this file was retitled and restatused in the 2026-10-19 doc-drift
sweep to match the actual `prg.s`; the power-on flow analysis the top-level
`OVERVIEW.md`s carry is still to be written here.
## 8. Memory Map (Key Zero Page / RAM)
| Address | Name | Use |
|---------|------|-----|
| $00–$01 | `Zp00`/`Zp01` | 16-bit pointer (RAM clear) |
| $09 | `Zp09` | PPUCTRL shadow ($90) |
| $0A | `Zp0A` | Init constant ($06) |
| $10 | `Zp10` | PPUCTRL working copy |
| $11 | `Zp11` | State flag |
| $14–$15 | `Zp14`/`Zp15` | NMI dispatch parameters |
| $20 | `Zp20` | Main loop sync flag (set by NMI) |
| $22 | `Zp22` | NMI phase (0=title, 1=gameplay) |
| $23 | `Zp23` | NMI done flag |
| $30 | `Zp30` | Reset signature check result |
| $42 | `Zp42` | Cleared by NMI phase 1 |
| $50 | `Zp50` | Game mode (0=title, 1=gameplay) |
| $70 | `Zp70` | Warm-start signature ($0500) |
| $B0 | `ZpB0` | NMI phase dispatch selector |
| $B1 | `ZpB1` | Heavily used (88 refs) — gameplay state |
| $B2–$BF | `ZpB2`–`ZpBF` | Object positions, velocities, timers |
| $C0–$CF | `ZpC0`–`ZpCF` | Enemy/bonus state arrays |
| $F0–$FF | `ZpF0`–`ZpFF` | Audio engine ZP |
| $0200 | OAM buffer | Sprite DMA source |
| $0300+ | Game state | Platforms, pipes, enemies, bonuses |
| $0500 | Warm-start signature | Must match Zp70 for warm start |
---
## 9. Data Tables
| Table | Address | Description |
|-------|---------|-------------|
| JSR Thunk Table | $C5A3 | 20 entries of `jsr target` / `rts` |
| Stack Dispatch Targets | (dynamic) | Pushed on stack, jumped via `jmp ($0014)` at $CDB1 |
| Audio Sequences | $F800+ | Music/sound data |
| Level Layout | $0300+ | Platform positions, pipe connections |
---
## 10. Summary of Execution Flow
```
Power On / Reset
│
▼
Reset ($C000) ────► cld/sei, wait VBlank, stack=$FF
│
├─ Check Zp70 ($70) warm-start signature at $0500
│ ├─ Zero: Clear 7 pages ($0000–$06FF), X=$00
│ └─ Non-zero: Clear 6 pages ($0000–$05FF), X=$5F
│
├─ stx $0500 (signature)
├─ jsr SubCCA1B, SubCCA2B (system init)
├─ PPUSCROLL = 0,0
├─ Zp30 = 1
├─ APU enable ($4015=$0F)
├─ PPUCTRL = $90 (NMI on) → Zp09
├─ Zp0A = $06
│
▼
SubCC04F ($C04F) ──► Zp20=0
│
├─ Zp30=0 or Zp50=1 → jsr SubCF8A7 (audio init)
│
└─ jsr SubCCD88, SubCC4B8 (title/gameplay init)
lda #$01 / sta Zp22 (NMI phase = gameplay)
│
▼ (fall through to main loop)
Main Loop ────────────────────────────────────────────────────┐
│ │
├─ Wait for Zp20=1 (set by NMI phase 1) │
├─ Process frame logic │
├─ Call NmiPhaseDispatch ($C0B6) │
│ ZpB0=0 → NmiPhaseHandlerA ($C149) │
│ ZpB0≠0 → check $0320, setup Zp14/15/11, jsr SubCC44A │
├─ Clear Zp22 (NMI phase) for next frame │
└─ Loop │
NMI ($C07D) [once per frame]
├─ Preserve A,X,Y
├─ OAM DMA ($4014 from $0200)
├─ lda Zp22
│ ├─ =0 (Title): Label_C0AC → set Zp23=1, rti
│ └─ =1 (Gameplay):
│ jsr SubCCB58 (physics)
│ jsr SubCEE6A (collision)
│ jsr SubCCCFF (platforms)
│ jsr SubCCA66 (sprites)
│ jsr SubCCE09 (bonus)
│ jsr SubCCCC5 (HUD)
│ jsr SubCCAF7 (audio trigger)
│ ldy #$01 / sty Zp20 (signal main loop)
│ dey / sty Zp42 (=0)
├─ lda #$01 / sta Zp23 (NMI done)
└─ Restore A,X,Y, rti
```
---
## 11. Naming and Residue
**Pass 1 complete** — 5,735 instructions / 11,829 code bytes (72.2%), byte accounting exact. 937 labels, 937 routine headers. Reached via:
- Vectors + direct `jsr`/`jmp`/branch, official opcodes: 1,185 instr (14.6%)
- `--undocumented` (105 extra NMOS opcodes): 1,210 instr (14.9%)
- `--auto-seed` (61 table-referenced entries): 4,187 instr (51.1%)
- `--extra-seed 0xC0B6,0xC5A3`: 5,735 instr (72.2%)
**Pass 2 substantial** — **262 call entry points named** (verified 2026-10-19):
- **39 semantic names**: Audio routines in `$F8xx-$F9xx`, gameplay helpers (`CheckZpB3Return`, `CheckRangeReturn012`, `ToggleZpFC_Bit3`)
- **223 `SubC<addr>` address-based names** for remaining `jsr`/`jmp` targets
- **667 `Label_Cxxxx` internal branch targets remain** — local control-flow targets within routines
- Zero raw `$XXXX` call operands remain (all 478 `jsr`/`jmp` sites resolved to labels)
**Pass 3 outstanding** — Data regions in 4,555 unreached bytes need naming; 151 neutral `ZpXX` aliases in `locals.inc`.
**Indirect dispatch** (not followed statically): `jmp ($0014)` at $CDB1 — table pointer arrives via `pla`, only knowable in caller's frame.
**Tracer fixes required** (in `_tools/nesdis.py`):
- Control-flow targets win over fall-through (fixed `bne` at $C4BA targeting $C528 vs fall-through `NOP #$A5` at $C527)
- Branch targets inside surviving instruction must be pruned (otherwise `Label_C528` undefined)
**Promotion outstanding** — Add `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg`, switch `make` to whole-ROM `cmp`, move `_tools` paths from `../../../_tools/` to `../../_tools/`.
Build verification: `make` in `copilot/` produces `reconstructed_prg.bin`, compared against `prg.bin` (PRG byte-identical).