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
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 BZp9E($9E): Frame sub-counter (palette cycling)Zp9F($9F): Secondary frame counterZp72($72): Tertiary counter (sprite palette)ZpDB($DB): Gameplay active flagZp06($06): Bit 4 = gameplay triggerZpB7($B7): Level transition flagZp03($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$2006twice$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 onZpBE/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).