# Lode Runner (U) — Game Flow from Power-On This document describes the execution flow of the disassembled Lode Runner (U) 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–$C029) On power-on or reset, the 6502 jumps to `$C000`. The reset routine performs cold-start hardware initialization: | Step | Code | Purpose | |------|------|---------| | 1–2 | `sei` / `cld` | Disable interrupts, clear decimal mode | | 3–4 | `ldx #$FF` / `bne Label_C00A` | X=$FF, branch always taken (delay slot?) | | 5–6 | `txs` / `jmp $C012` | Initialize stack pointer to $01FF | | 7–8 | `ldy #$00` / `sty Zp00` / `sty Zp01` | Zp00/Zp01 = $0000 (RAM clear pointer) | | 9 | `tya` / `ldx #$08` | A=0, X=8 (8 pages to clear) | | 10–14 | `Label_C01B`: `sta (Zp00),y` / `iny` / `bne` / `inc Zp01` / `dex` / `bne` | Clear $0000–$07FF (8 pages of 256 bytes) | | 15 | `lda #$23` / `sta ZpE5` | ZpE5 ($E5) = $23 | | 16 | `jmp $C3A6` | Jump to main initialization | **No warm-start signature check** — full 2 KB clear every reset. --- ## 2. Interrupt Vectors — CPU $FFFA–$FFFF Located at PRG offset $3FFA (end of 16 KB bank): | Vector | Address | Handler | |--------|---------|---------| | NMI | $C02C | `NmiHandler` | | RESET | $C000 | `Reset` | | IRQ | $FFF9 | `IrqHandler` (single `rti` — unused) | --- ## 3. NMI / Frame Heartbeat — `NmiHandler` ($C02C) The NMI fires once per frame and implements a two-path structure controlled by **Zp10** ($10): ``` NmiHandler ($C02C): pha / txa / pha / tya / pha ; preserve A,X,Y jsr $C27C ; common NMI setup lda Zp10 ($10) beq Label_C050 ; Zp10=0: Path A lda #$00 / sta Zp10 ; clear Zp10 lda Zp11 ($11) / tax beq Label_C043 ; Zp11=0: Path B bne Label_C050 ; Zp11≠0: Path A Label_C043: jsr $C18E ; Path B: PPU/game update lda #$20 / ldx #$00 / jsr $C178 ; PPU write ($2000 + X=0) jmp $C0A4 ; → main NMI exit Label_C050: ; Path A: Palette/sprite update lda #$3F / ldx #$08 / jsr $C178 ; PPUADDR=$3F08 (palette) inc Zp9E ($9E) ; frame sub-counter lda Zp9E / and #$1F / bne Label_C07B ; every 32 frames inc Zp9F ($9F) ; secondary counter lda Zp9F / and #$03 / tax ; 0-3 selector lda #$0F / jsr $C006 ; PPUDATA write lda $C39E,x / jsr $C006 ; palette data from table lda #$00 / jsr $C006 ; clear lda #$30 / jsr $C006 ; $30 Label_C07B: lda Zp9E / and #$07 / bne Label_C0A4 ; every 8 frames lda #$3F / ldx #$0C / jsr $C178 ; PPUADDR=$3F0C inc Zp72 ($72) ; tertiary counter lda Zp72 / and #$03 / tax ; 0-3 selector lda #$0F / jsr $C006 lda #$38 / jsr $C006 lda #$28 / jsr $C006 lda $C3A2,x / jsr $C006 ; sprite palette data Label_C0A4: ; Common NMI exit lda Zp03 ($03) / sta $2001 (PPUMASK) ; apply PPUMASK jsr $C1E1 ; sprite/OAM update lda ZpDB ($DB) beq Label_C0D1 ; if 0: skip gameplay lda Zp06 ($06) / and #$10 / beq Label_C0D1 ; bit 4 check jsr $C2C6 / jsr $DE9F / jsr $C1D7 ; gameplay logic jmp Label_C127 ; rti Label_C0BF: ; Alternate entry (ZpDB=0?) jsr $C1E1 lda Zp06 / and #$10 / beq Label_C0BF ; wait for bit 4 jsr $DE9F / jsr $C1D7 / jsr $C2B3 jmp Label_C127 Label_C0D1: lda ZpB7 ($B7) beq Label_C127 ; if 0: rti jsr $E271 ; level/game init inc ZpB9 / lda ZpB9 / cmp ZpB8 / bcc Label_C127 lda #$00 / sta ZpB9 ; reset counter ... (level transition logic) Label_C127: pla / tay / pla / tax / pla / rti ; restore, return ``` **Two NMI Modes:** - **Path A (Zp10≠0):** Palette/sprite updates, periodic palette cycling (every 8/32 frames), sprite DMA - **Path B (Zp10=0, Zp11=0):** `jsr $C18E` → PPU register update → main exit **Key ZP Flags:** - `Zp10` ($10): NMI path selector (0=Path B, non-zero=Path A) - `Zp11` ($11): Sub-path selector for Path B - `Zp9E` ($9E): Frame sub-counter (palette cycling) - `Zp9F` ($9F): Secondary frame counter - `Zp72` ($72): Tertiary counter (sprite palette) - `ZpDB` ($DB): Gameplay active flag - `Zp06` ($06): Bit 4 = gameplay trigger - `ZpB7` ($B7): Level transition flag - `Zp03` ($03): PPUMASK shadow --- ## 4. Main Initialization — `$C3A6` (from Reset) After reset jumps to `$C3A6`, the game initializes title screen and enters main loop. The main loop is driven by NMI synchronization — the main thread waits for NMI to signal frame completion via ZP flags. Key initialization routines (reached from `$C3A6`): - `$C178`: PPU address/data write helper (A=addr high, X=addr low → $2006/$2006/$2007) - `$C18E`: PPU control update (reads Zp03→PPUMASK, Zp10→PPUCTRL) - `$C1E1`: Sprite/OAM update (builds OAM at $0200) - `$C27C`: Common NMI setup (called at NMI entry) --- ## 5. Key Subsystems ### 5.1 PPU Helper — `$C178` / `$C006` - `$C178`: Set PPUADDR from A (high) / X (low), write via `$2006` twice - `$C006`: Write A to PPUDATA ($2007), RTS - `$C18E`: Read PPUSTATUS, write Zp03→PPUMASK, Zp10→PPUCTRL ### 5.2 Sprite/OAM System — `$C1E1` / `$C1D7` - Builds OAM at $0200 from object state arrays - `$C1D7`: Called from NMI paths for per-frame sprite update ### 5.3 Gameplay Logic — `$C2C6`, `$DE9F`, `$C1D7`, `$C2B3` - `$C2C6`: Player/input handling - `$DE9F`: Enemy/guard AI - `$C2B3`: Level progression, scoring ### 5.4 Level Data & Transitions — `$E271`, `$C0E4`–`$C123` - `$E271`: Level load/init - `$C0E4`: State machine on `ZpBE`/`ZpBA` (pointer to level data) - Level data tables at `$C2F0`, `$C378`, `$C39E`, `$C3A2` ### 5.5 Audio — `$C12D` / `$C143` / `$C16E` - APU register writes ($4000–$4003) for pulse channels - Frequency data from `$C2F0`, `$C378` - Called from level transition / gameplay --- ## 6. Memory Map (Key Zero Page / RAM) | Address | Name | Use | |---------|------|-----| | $00–$01 | `Zp00`/`Zp01` | 16-bit pointer (RAM clear, PPU) | | $03 | `Zp03` | PPUMASK shadow | | $06 | `Zp06` | Gameplay flags (bit 4 = trigger) | | $10 | `Zp10` | NMI path selector (0=title, ≠0=game) | | $11 | `Zp11` | NMI sub-path selector | | $12–$13 | `Zp12`/`Zp13` | Object/sprite pointers | | $20–$21 | `Zp20`/`Zp21` | Player position (X/Y) | | $26 | `Zp26` | Player state/animation | | $28 | `Zp28` | Player velocity | | $32–$35 | `Zp32`–`Zp35` | Enemy/guard positions | | $39 | `Zp39` | Level timer | | $52–$5F | `Zp52`–`Zp5F` | Sprite attribute buffers | | $72 | `Zp72` | Sprite palette cycle counter | | $85–$86 | `Zp85`/`Zp86` | Level data pointers | | $9E–$9F | `Zp9E`/`Zp9F` | Frame counters (palette cycling) | | $B7 | `ZpB7` | Level transition trigger | | $B8–$B9 | `ZpB8`/`ZpB9` | Level transition counters | | $BA–$BF | `ZpBA`–`ZpBF` | Level data pointer + state | | $DB | `ZpDB` | Gameplay active flag | | $E5 | `ZpE5` | Init constant ($23) | | $0200 | OAM buffer | 64 sprites (DMA source) | | $0300+ | Game state arrays | Player, enemies, level data | --- ## 7. Data Tables | Table | Address | Description | |-------|---------|-------------| | Palette data (BG) | $C39E | 4 entries, indexed by Zp9F&3 | | Palette data (sprites) | $C3A2 | 4 entries, indexed by Zp72&3 | | Audio frequency low | $C2F0 | Note→period low byte | | Audio frequency high | $C378 | Note→period high byte (×2 + index) | | Level data pointer | `ZpBA`/`ZpBC` | Indirect pointer to level scripts | --- ## 8. Summary of Execution Flow ``` Power On / Reset │ ▼ Reset ($C000) ────► sei/cld, stack=$FF │ ├─ Clear $0000–$07FF (8 pages via Zp00/Zp01) │ ├─ ZpE5 = $23 │ ▼ jmp $C3A6 (Main Initialization) │ ├─ Title screen setup ├─ PPU/APU init └─ Enter main loop (VBlank synchronized) Main Loop (implicit, VBlank-driven) │ ▼ (NMI fires once per frame) NMI ($C02C) ├─ Preserve A,X,Y ├─ jsr $C27C (common setup) ├─ lda Zp10 │ ├─ =0 (Title/Menu): │ │ lda Zp11 │ │ ├─ =0: jsr $C18E (PPU update), $C178, jmp $C0A4 │ │ └─ ≠0: Path A (palette cycling) │ │ │ └─ ≠0 (Gameplay): │ lda #$00 / sta Zp10 │ lda Zp11 / tax │ ├─ =0: jsr $C18E, $C178, jmp $C0A4 │ └─ ≠0: Path A (palette cycling) │ ├─ Path A (Label_C050): │ Palette writes to $3F08/$3F0C every 8/32 frames │ inc Zp9E, Zp9F, Zp72 │ ├─ Common Exit ($C0A4): │ lda Zp03 → PPUMASK │ jsr $C1E1 (sprite update) │ lda ZpDB │ ├─ =0: Label_C0BF (wait for Zp06 bit 4) │ └─ ≠0: Check Zp06 bit 4 │ ├─ Clear: Label_C0D1 (level transition check) │ └─ Set: jsr $C2C6, $DE9F, $C1D7 (gameplay) │ └─ Label_C127: Restore A,X,Y, rti Gameplay (when ZpDB≠0, Zp06&$10) ├─ $C2C6: Player control ├─ $DE9F: Enemy AI ├─ $C1D7: Sprite update ├─ $C2B3: Level/scoring └─ Level transition: $E271, $C0E4 state machine ``` --- ## 9. Naming and Residue **Pass 1 complete** — 11,057 lines, 627 `Label_` definitions, canonical audit comments, 52.6% code. **Pass 2 complete (2026-10-20)** — All 627 label definitions renamed. 196 semantic routine entry points named via `_tools/pass2.py` derive/apply using a hand-curated routinemap; remaining 431 internal branch targets named as `_Block[_n]`. Zero `Label_Cxxxx` definitions remain. **Pass 3 partial (2026-10-20)** — `_tools/apply_symbols.py` substituted **85 operands** (83 hardware registers + 2 single-name zero-page operands). Zero multi-alias residue. **2 semantic zero-page aliases applied**; remaining 150 zero-page slots retain neutral `ZpXX` names in `locals.inc`. Data tables/structures named in Memory Map section. **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).