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