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].
11 KiB
Mario Bros (JU) — Game Flow from Power-On
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.
1. Reset Vector — CPU $FFFC → $C000
Reset ($C000–$C06A)
On power-on or reset, the 6502 jumps to $C000. The reset routine performs cold-start hardware initialization:
| 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 |
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.
2. Post-Reset Initialization — SubCC04F ($C04F) onward
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)
Then falls through to main loop entry.
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):
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
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.
5. NMI Phase Dispatcher — NmiPhaseDispatch ($C0B6)
Called from main loop to synchronize with NMI:
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
...
ZpB0 ($B0) = NMI phase (0=title, non-zero=gameplay). $0320 = Additional state check.
6. Main Game Loop
The main loop runs in the main thread, synchronized with NMI via Zp20/Zp22/Zp23:
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
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
7. Key Subsystems
7.1 Gameplay Update Chain (NMI Phase 1)
Seven sequential calls from NMI:
SubCCB58($CB58) — Player/enemy physicsSubCEE6A($EE6A) — Collision detectionSubCCCFF($CCFF) — Platform/pipe logicSubCCA66($CA66) — Sprite animationSubCCE09($CE09) — Bonus/coin logicSubCCCC5($CCC5) — Score/HUD updateSubCCAF7($CAF7) — Audio trigger
7.2 Audio Engine — $F800+ region
SubCF8A7($F8A7): Audio init (called from reset)SubCCD88($CD88): Title musicSubCC4B8($C4B8): Gameplay music- APU registers at $4000–$4013, frame counter at $4017
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.
7.4 JSR Thunk Table — $C5A3
20-entry table of jsr/rts pairs. Seeding this recovered major code blocks.
7.5 Controller Input
Read via $4016 strobe/shift in NMI or main loop. Button state stored in ZP ($10–$15 area).
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 remainingjsr/jmptargets - 667
Label_Cxxxxinternal branch targets remain — local control-flow targets within routines - Zero raw
$XXXXcall operands remain (all 478jsr/jmpsites 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
bneat $C4BA targeting $C528 vs fall-throughNOP #$A5at $C527) - Branch targets inside surviving instruction must be pruned (otherwise
Label_C528undefined)
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).