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].
272 lines
11 KiB
Markdown
272 lines
11 KiB
Markdown
# 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). |