Files
disasm/Millipede (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

10 KiB
Raw Blame History

Millipede (U) — Game Flow from Power-On

This document describes the execution flow of the disassembled Millipede (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 → $C735

Reset ($C735–$C7B3)

On power-on or reset, the 6502 jumps to $C735 (non-$C000 entry, Donkey Kong Jr. style). The reset routine performs cold-start hardware initialization:

Step Code Purpose
1–2 sei / cld Disable interrupts, clear decimal mode
3–4 lda #$00 / sta $2000 PPUCTRL = 0 (NMI off, sprite $0000, BG $0000)
5–6 ldx #$FF / txs Initialize stack pointer to $01FF
7–11 Label_C73F: poll $2002 via and #$80 / beq Wait for VBlank (bit 7 of PPUSTATUS)
12–13 ldy #$06 / sty Zp01 / ldy #$00 / sty Zp00 Zp00/Zp01 = $0600 (RAM clear pointer)
14 tya A = 0
15–19 Label_C74F: sta (Zp00),y / dey / bne / dec Zp01 / bpl Clear $0000–$05FF (6 pages)
20 jsr $C76A Initialize object state at $03D0–$03F8
21 lda Zp10 / ora #$80 / sta $2000 / sta Zp10 PPUCTRL
22 Label_C764: jsr $F09B / jmp $C764 Infinite loop: audio init? (waits for NMI)

$C76A (Label_C76A) — Object initialization:

    ldx #$29 / lda #$24
Label_C76E:
    sta $03D0,x   ; clear $03D0–$03F9 (42 bytes)
    dex / bne Label_C76E
    ldx #$00
Label_C776:
    lda $C271,x / sta $03DF,x   ; copy 6 bytes from $C271 to $03DF
    inx / cpx #$06 / bne Label_C776
    lda #$2C / sta $03D0
    lda #$2D / sta $03E6
    lda #$24 / sta $03E9
    lda #$16 / sta $03F2
    lda #$D0 / sta $03F5–$03F8
    lda #$00 / sta $4011 (APUDMCDIRECT)
    lda #$06 / sta $2001 (PPUMASK: show BG)
    jsr $C7E6 / jsr $C7CA
    jmp $C7B6

$C7B6 (Label_C7B6) — Clear score/timer:

    lda #$00 / sta $0330 / sta $0331

Then enters main loop at $C7BE.


2. Main Loop — $C7BE onward

Label_C7BE:
    jsr $C7E0
    lda Zp10 / and #$E7 / sta Zp10     ; clear bits 3,4,5 of PPUCTRL shadow
    ... (main game loop continues)

The main loop runs continuously, synchronized with NMI via Zp10/Zp11 flags.


3. Interrupt Vectors — CPU $FFFA–$FFFF

Located at PRG offset $3FFA (end of 16 KB bank):

Vector Address Handler
NMI $C7F1 NmiHandler
RESET $C735 Reset
IRQ $FFF0 Unused (lands in vector-page padding)

4. NMI / Frame Heartbeat — NmiHandler ($C7F1)

The NMI fires once per frame and drives the frame pacing:

NmiHandler ($C7F1):
    lda #$00 / sta $4016                   ; controller strobe
    lda Zp10 ($10) / and #$7F / sta $2000 / sta Zp10  ; PPUCTRL &= ~$80 (NMI off during frame)
    lda Zp11 ($11) / eor #$1A / sta $2001           ; PPUMASK ^= $1A (toggle bits)
    jsr $EF4A                              ; sprite/OAM update
    jsr $EF36                              ; PPU/scroll update
    lda $0396 / bne Label_C819             ; check state
    lda $0397 / bne Label_C819
    jsr $F085                              ; attract mode audio
Label_C819:
    jsr $F9A0                              ; gameplay audio
    jsr $F0BC                              ; sound effects
    lda Zp43 ($43)
    bne Label_C827
    lda #$FF / sta Zp43                    ; init Zp43 = $FF
Label_C827:
    lda Zp60 ($60)                         ; game state
    beq Label_C843                         ; 0: title/attract
    cmp #$01 / beq Label_C849              ; 1: gameplay
    cmp #$02 / beq Label_C83D              ; 2: bonus/stage clear
    jsr $D3F1 / lda #$00 / sta Zp77 / jmp $C84C  ; 3+: other
Label_C83D:
    jsr $CCD7 / jmp $C84C                  ; state 2
Label_C843:
    jsr $C865 / jmp $C84C                  ; state 0
Label_C849:
    jsr $CA31                              ; state 1
Label_C84C:
    ... (common NMI exit)

NMI State Machine (Zp60 / $60):

  • 0 (Title/Attract): $C865 — attract mode logic
  • 1 (Gameplay): $CA31 — main gameplay update
  • 2 (Bonus/Transition): $CCD7 — stage clear, bonus
  • 3+ (Other): $D3F1 — game over, high score, etc.

Key NMI Operations:

  1. Controller strobe ($4016)
  2. PPUCTRL/PPUMASK update (Zp10/Zp11 shadows)
  3. Sprite/OAM update ($EF4A)
  4. PPU/Scroll update ($EF36)
  5. Audio engine ($F9A0 gameplay, $F085 attract, $F0BC SFX)
  6. Game state dispatch via Zp60

5. Key Subsystems

5.1 Sprite/OAM System — $EF4A / $EF36

  • $EF4A: Builds OAM at $0200 from object arrays
  • $EF36: PPU scroll/register updates

5.2 Audio Engine

  • Attract: $F085 (called when $0396/$0397 set)
  • Gameplay: $F9A0 (music), $F0BC (sound effects)
  • APU registers at $4000–$4013

5.3 Gameplay Logic — $CA31 / $CCD7 / $D3F1 / $C865

  • $CA31: Main gameplay (player, millipede, enemies, collision)
  • $CCD7: Stage clear / bonus round
  • $D3F1: Game over / high score
  • $C865: Attract mode demo

5.4 Object State — $03D0+ region

  • $03D0–$03F8: Initialized at reset from $C271 table
  • $0330–$0331: Score/timer
  • $0396–$0397: Attract mode flags
  • $03F5–$03F8: Player position/state

5.5 Indirect Dispatch — $EF15 (jmp ($0002))

jmp ($0002) at $EF15 — target address in ZP $0002/$0003, set by caller. Not followed statically.


6. Memory Map (Key Zero Page / RAM)

Address Name Use
$00–$01 Zp00/Zp01 16-bit pointer (RAM clear)
$02–$03 Zp02/Zp03 Indirect dispatch pointer (jmp ($0002))
$10 Zp10 PPUCTRL shadow (bit 7 = NMI enable)
$11 Zp11 PPUMASK shadow (toggled via EOR #$1A)
$43 Zp43 Audio state (init $FF)
$60 Zp60 Game state (0=attract, 1=play, 2=bonus, 3+=other)
$77 Zp77 Gameplay sub-state
$0330–$0331 Score/Timer Cleared at $C7B6
$0396–$0397 Attract flags Checked in NMI for audio path
$03D0–$03F8 Object init state Copied from $C271 at reset
$03DF–$03E4 Object params 6 bytes from $C271
$03F5–$03F8 Player state Initialized to $D0
$4011 APUDMCDIRECT DMC direct load (silenced at reset)
$4016 JOY1 Controller strobe (NMI)

7. Data Tables

Table Address Description
Object init data $C271 (ObjectInitData) 6 bytes copied to $03DF at reset
Level data $C000 (LevelData) 1845 bytes of level/enemy pattern data
Audio data $F24F (AudioData) audio sequences
Vector page $FE61 (VectorPageData) padding/vectors
Level/millipede data $C000–$C734 1845 bytes of level layouts, enemy paths (classified as data, referenced by code)
Audio sequences $F000+ Music/SFX data

Note: The region $C000–$C734 (1845 bytes) is classified as .byte data but is heavily referenced by code (93+ references from lda $Cxxx,x, bit $Cxxx instructions). This is level/map data for the millipede and enemy movement patterns.


8. Summary of Execution Flow

Power On / Reset
    │
    ▼
Reset ($C735) ────► sei/cld, PPUCTRL=0, stack=$FF
    │
    ├─ Wait VBlank (poll $2002 bit 7)
    │
    ├─ Clear $0000–$05FF (6 pages via Zp00/Zp01)
    │
    ├─ jsr $C76A:
    │     Clear $03D0–$03F9
    │     Copy $C271→$03DF (6 bytes)
    │     Init $03D0=$2C, $03E6=$2D, $03E9=$24, $03F2=$16
    │     $03F5–$03F8 = $D0
    │     APUDMCDIRECT=0, PPUMASK=$06
    │     jsr $C7E6, $C7CA
    │
    ├─ PPUCTRL |= $80 (NMI on) → Zp10
    │
    ▼
Label_C764: jsr $F09B / jmp $C764  (wait for first NMI)
    │
    ▼ (NMI fires)

NMI ($C7F1) [once per frame]
    ├─ Controller strobe ($4016)
    ├─ Zp10 &= $7F → PPUCTRL (NMI off during frame)
    ├─ Zp11 ^= $1A → PPUMASK (toggle bits 1,3,4)
    ├─ jsr $EF4A (sprite/OAM update)
    ├─ jsr $EF36 (PPU/scroll update)
    ├─ Check $0396/$0397 → attract audio ($F085) if set
    ├─ jsr $F9A0 (gameplay audio)
    ├─ jsr $F0BC (sound effects)
    ├─ Zp43 init ($FF) if zero
    ├─ Dispatch on Zp60 ($60):
    │     0: jsr $C865 (attract) → $C84C
    │     1: jsr $CA31 (gameplay) → $C84C
    │     2: jsr $CCD7 (bonus) → $C84C
    │     3+: jsr $D3F1 → $C84C
    └─ $C84C: common exit → rti

Main Loop ($C7BE+)
    ├─ jsr $C7E0
    ├─ Zp10 &= $E7 (clear bits 3,4,5)
    ├─ Game logic (player, millipede, enemies)
    ├─ Collision detection
    ├─ Level progression
    └─ Loop (VBlank synchronized via Zp10/Zp11)

9. Naming and Residue

Pass 1 complete — 14,744 lines, canonical audit comments, 74.6% code. All 839 address-derived Label_ definitions have been renamed: 12 semantic routine names (Reset, NmiHandler, MainLoop, GameplayUpdate, AttractMode, AttractAudio, GameOver, StageClear, InitObjects, ClearScore, SoundEffects, GameplayAudio) + 264 SubC<addr> entry points + 577 Sub_8764_* internal branch targets. No Label_ remains.

Pass 2 complete — every jsr/jmp target is a named routine (semantic or SubC<addr>); internal branch targets use <Routine>_Block/JoinBranchPaths style names.

Pass 3 complete — 8 semantic zero-page aliases (PtrLo/Hi, Ptr2Lo/Hi, PpuCtrlShadow, PpuMaskShadow, AudioState, GameState, GameplaySubState); hardware register operands substituted via apply_symbols.py; data tables named: LevelData ($C000–$C734), ObjectInitData ($C271), AudioData ($F24F–$F984), VectorPageData ($FE61–$FFFF). Remaining zero-page slots retain neutral ZpXX names where role is not unique.

Indirect dispatch (not followed statically): jmp ($0002) at $EF15.

Large data region — $C000–$C734 (1845 bytes) classified as data but referenced by 93+ code instructions (lda $Cxxx,x, bit $Cxxx). This is level/enemy pattern data, not code.

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