Files
disasm/_wip/Mario Bros (JU)/OVERVIEW.md
T
tmerritt 9eb04ea7ef 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].
2026-10-19 19:58:41 -04:00

11 KiB
Raw Blame History

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:

  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

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

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 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).