Files
disasm/Lode Runner (U)/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

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