Files
disasm/Popeye (JU) (PRG 0)/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

244 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Popeye (JU) (PRG 0) — Game Flow from Power-On
This document describes the execution flow of the disassembled Popeye (JU) (PRG 0) 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.
**Companion note:** `Popeye (JU) (PRG 1)` holds a different PRG image of the same title (different mirroring flag: Horizontal vs Vertical). Neither is currently known to be a superset of the other.
---
## 1. Reset Vector — CPU $FFFC → $C649
**`Reset`** ($C649–$C683)
On power-on or reset, the 6502 jumps to **$C649** (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 #$10` / `sta $2000` | PPUCTRL = $10 (NMI off, sprite $0000, BG $0000, 8×8) |
| 5–6 | `ldx #$FF` / `txs` | Initialize stack pointer to $01FF |
| 7–11 | `Label_C653`: poll `$2002` via `and #$80` / `beq` | Wait for VBlank (bit 7 of PPUSTATUS) |
| 12–13 | `ldy #$07` / `sty Zp01` / `ldy #$00` / `sty Zp00` | Zp00/Zp01 = $0700 (RAM clear pointer) |
| 14 | `tya` | A = 0 |
| 15–19 | `Label_C663`: `sta (Zp00),y` / `dey` / `bne` / `dec Zp01` / `bpl` | Clear $0000–$07FF (8 pages) |
| 20 | `jsr $C686` | Initialize player/object state at $0500–$0504 |
| 21 | `lda #$01` / `sta Zp60` | Zp60 ($60) = 1 (game state) |
| 22 | `lda #$00` / `sta Zp63` | Zp63 ($63) = 0 |
| 23 | `lda Zp10` / `eor #$80` / `sta $2000` / `sta Zp10` | PPUCTRL ^= $80 (toggle NMI enable) + shadow |
| 24 | `Label_C680`: `jsr $EE74` / `jmp $C680` | Infinite loop: waits for first NMI |
**`$C686` (Label_C686)** — Player state init:
```
lda #$11 / sta $0500 / sta $0502
lda #$03 / sta $0501
lda #$01 / sta $0503
ldx #$19 / lda #$24
Label_C69C:
sta $0504,x ; fill $0504–$051C with $24 (25 bytes)
dex / bne Label_C69C
ldx #$00
```
Then falls through to `$C6A4` (main loop entry).
---
## 2. Main Loop — `$C6A4` onward
After reset initialization, the main loop runs at `$C6A4`, synchronized with NMI via Zp10/Zp60/Zp63 flags. The loop calls various gameplay subroutines and waits for NMI to signal frame completion.
---
## 3. Interrupt Vectors — CPU $FFFA–$FFFF
Located at PRG offset $3FFA (end of 16 KB bank):
| Vector | Address | Handler |
|--------|---------|---------|
| NMI | $C701 | `NmiHandler` |
| RESET | $C649 | `Reset` |
| IRQ | $FFF0 | `IrqHandler` (single `sta (Zp16,x)` — undocumented opcode $81, effectively unused) |
---
## 4. NMI / Frame Heartbeat — `NmiHandler` ($C701)
The NMI fires once per frame and implements a state machine dispatching on Zp30/Zp31/Zp60/Zp63:
```
NmiHandler ($C701):
lda Zp10 ($10) / and #$7F / sta $2000 / sta Zp10 ; PPUCTRL &= ~$80 (NMI off during frame)
lda Zp11 ($11) / eor #$18 / sta $2001 ; PPUMASK ^= $18 (toggle bits 3,4)
jsr $ED34 ; sprite/OAM update
jsr $ED20 ; PPU/scroll update
lda $0400 / bne Label_C724 ; check $0400
lda $0401 / bne Label_C724 ; check $0401
jsr $EE5E ; attract mode audio
Label_C724:
jsr $F970 ; gameplay audio
jsr $EE89 ; sound effects
lda Zp43 ($43)
bne Label_C732
lda #$FF / sta Zp43 ; init Zp43 = $FF
Label_C732:
lda Zp30 ($30)
beq Label_C73C
jsr $CA8B / jmp $C75D ; state: Zp30≠0
Label_C73C:
lda Zp31 ($31)
beq Label_C746
jsr $CAD4 / jmp $C75D ; state: Zp31≠0
Label_C746:
lda Zp60 ($60)
bne Label_C754
lda Zp63 ($63)
beq Label_C75A
jsr $CB12 / jmp $C75D ; state: Zp60=0, Zp63≠0
Label_C754:
jsr $C76A / jmp $C75D ; state: Zp60≠0
Label_C75A:
jsr $C89B ; state: Zp60=0, Zp63=0
Label_C75D:
... (common NMI exit → rti)
```
**NMI State Machine:**
| Condition | Handler | Description |
|-----------|---------|-------------|
| $0400≠0 or $0401≠0 | `$EE5E` | Attract mode audio |
| Zp30≠0 | `$CA8B` | Gameplay mode A |
| Zp31≠0 | `$CAD4` | Gameplay mode B |
| Zp60≠0 | `$C76A` | Gameplay mode C |
| Zp60=0, Zp63≠0 | `$CB12` | Bonus/stage transition |
| Zp60=0, Zp63=0 | `$C89B` | Title/attract mode |
**Key NMI Operations:**
1. PPUCTRL &= $7F (disable NMI during frame), PPUMASK ^= $18
2. Sprite/OAM update (`$ED34`), PPU/scroll update (`$ED20`)
3. Audio: attract (`$EE5E`) or gameplay (`$F970` + `$EE89`)
4. State dispatch via Zp30/Zp31/Zp60/Zp63
5. Zp43 init to $FF on first gameplay NMI
---
## 5. Key Subsystems
### 5.1 Sprite/OAM System — `$ED34` / `$ED20`
- `$ED34`: Builds OAM at $0200 from object arrays
- `$ED20`: PPU scroll/register updates
### 5.2 Audio Engine
- **Attract:** `$EE5E` (called when $0400/$0401 set)
- **Gameplay:** `$F970` (music), `$EE89` (sound effects)
- APU registers at $4000–$4013
### 5.3 Gameplay Logic
- `$CA8B` / `$CAD4` / `$C76A` / `$CB12` / `$C89B`: State-specific gameplay updates
- Player (Popeye), enemies (Bluto, Sea Hag), projectiles, collectibles (spinach, hearts, letters)
### 5.4 Level Data — $C000–$C648 region
- 1609 bytes classified as `.byte` data but referenced by 148+ code instructions
- Contains level layouts, enemy paths, platform positions
- Referenced via `lda $Cxxx,x`, `bit $Cxxx` — this is **level/map data**, not code
### 5.5 Indirect Dispatch — `$ECFF` (`jmp ($0002)`)
`jmp ($0002)` at $ECFF — 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 #$18) |
| $30–$31 | `Zp30`/`Zp31` | Gameplay state selectors |
| $43 | `Zp43` | Audio state (init $FF) |
| $60 | `Zp60` | Game state (set 1 at reset) |
| $63 | `Zp63` | Sub-state (cleared at reset) |
| $0200 | OAM buffer | Sprite DMA source |
| $0400–$0401 | Attract flags | Checked in NMI for audio path |
| $0500–$051C | Player state | Initialized at $C686 ($11,$03,$11,$24×25) |
---
## 7. Data Tables
| Table | Address | Description |
|-------|---------|-------------|
| Level/map data | $C000–$C648 | 1609 bytes of level layouts, enemy paths (referenced by code) |
| Audio sequences | $F970+ | Music/SFX data |
---
## 8. Summary of Execution Flow
```
Power On / Reset
│
▼
Reset ($C649) ────► sei/cld, PPUCTRL=$10, stack=$FF
│
├─ Wait VBlank (poll $2002 bit 7)
│
├─ Clear $0000–$07FF (8 pages via Zp00/Zp01)
│
├─ jsr $C686:
│ $0500=$11, $0501=$03, $0502=$11, $0503=$01
│ $0504–$051C = $24 (25 bytes)
│
├─ Zp60=1, Zp63=0
├─ PPUCTRL ^= $80 (enable NMI) → Zp10
│
▼
Label_C680: jsr $EE74 / jmp $C680 (wait for first NMI)
│
▼ (NMI fires)
NMI ($C701) [once per frame]
├─ Zp10 &= $7F → PPUCTRL (NMI off during frame)
├─ Zp11 ^= $18 → PPUMASK (toggle bits 3,4)
├─ jsr $ED34 (sprite/OAM update)
├─ jsr $ED20 (PPU/scroll update)
├─ Check $0400/$0401:
│ ├─ Set: jsr $EE5E (attract audio)
│ └─ Clear:
│ jsr $F970 (gameplay audio)
│ jsr $EE89 (sound effects)
│ Zp43 init ($FF)
│ Dispatch on Zp30/Zp31/Zp60/Zp63:
│ Zp30≠0 → $CA8B
│ Zp31≠0 → $CAD4
│ Zp60≠0 → $C76A
│ Zp60=0,Zp63≠0 → $CB12
│ Zp60=0,Zp63=0 → $C89B
└─ rti
Main Loop ($C6A4+)
├─ Gameplay logic (player, enemies, collectibles)
├─ Collision detection
├─ Level progression
└─ Loop (VBlank synchronized via Zp10/Zp60/Zp63)
```
---
## 9. Naming and Residue
**Pass 1 complete** — 14,875 lines, canonical audit comments, 72.3% code. 6 seeded routine entries from table references.
**Pass 2 complete** — all 878 `Label_` definitions renamed: 2 semantic entry names (`Reset`, `NmiHandler`) + 315 `SubC<addr>` entry points + 561 `<Routine>_Block_<addr>` internal branch targets. No `Label_` remains. Every `jsr`/`jmp` target is a named routine.
**Pass 3 complete** — 4 semantic zero-page aliases (PtrLo/Hi, Ptr2Lo/Hi) added to `locals.inc`; 79 operands substituted via `apply_symbols.py` (15 single + 64 hardware); data tables named: `LevelData` ($C000–$C648), `AudioData` ($F088–$F832), `VectorPageData` ($FCCE–$FFEF). Remaining zero-page slots retain neutral `ZpXX` names.
**Indirect dispatch** (not followed statically): `jmp ($0002)` at $ECFF.
**Large data region** — $C000–$C648 (1609 bytes) classified as data but referenced by 148+ code instructions. This is level/enemy pattern data.
**PRG 0 vs PRG 1** — Same vectors, same structure. PRG 0: Vertical mirroring. PRG 1: Horizontal mirroring. PRG bytes differ slightly (different build).
**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).