Files
disasm/Donkey Kong 3 (JUE)/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

319 lines
14 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.
# Donkey Kong 3 (JUE) — Game Flow from Power-On
This document describes the execution flow of the disassembled Donkey Kong 3 (JUE) 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 ROM (whole-ROM compare).
---
## 1. Reset Vector — CPU $FFFC → $C000
**`ResetHandler`** ($C000–$C08C)
On power-on or reset, the 6502 jumps to `$C000`. The reset routine performs cold-start hardware initialization and enters the main loop dispatcher:
| Step | Code | Purpose |
|------|------|---------|
| 1–2 | `cld` / `sei` | Clear decimal mode, disable interrupts |
| 3–6 | Poll `$2002` twice (`bpl *-3`) | Wait for two full VBlank periods (PPU warm-up) |
| 7–8 | `ldx #$00` / `txs` | Initialize stack pointer to $01FF |
| 9–11 | `ldx FrameCounter` / `ldy #$07` / `sty WorkPointerOrValueHi` | FrameCounter ($27) → X; Y=$07 → WorkPointerOrValueHi ($02) |
| 12 | `ldy #$00` / `sty WorkPointerOrValueLo` | Y=$00 → WorkPointerOrValueLo ($01) |
| 13–21 | Clear RAM $0000–$07FF | 8 pages via (WorkPointerOrValueLo),Y; dec WorkPointerOrValueHi loops pages |
| 22 | `txa` / `bne FinishResetInitialization` | If FrameCounter was non-zero, skip full clear |
| 23 | `ldx #$5F` | Alternate init value for warm start |
| 24 | `stx Ram_06F0` | Store signature at $06F0 |
| 25 | `jsr ClearNametable2400WithTileFF` ($C1A2) | Fill nametable $2400 with $FF |
| 26 | `jsr HideAllOamSprites` ($C1EA) | Hide all 64 sprites (Y=$F4) |
| 27 | `sty $2005` ×2 | PPUSCROLL = 0,0 |
| 28 | `iny` / `sty GameMode` | Y=$01 → GameMode ($3A) = title mode |
| 29 | `lda #$0F` / `sta $4015` | Enable all APU channels |
| 30 | `lda #$90` / `sta $2000` / `sta PPUCTRL` | PPUCTRL: NMI on, sprite $0000, BG $1000, 8×8 sprites |
**Warm-start signature:** `FrameCounter` ($27) and `Ram_06F0` survive the RAM clear. If `FrameCounter` ≠ 0 on reset, the game skips the full 2 KB clear and uses X=$5F.
---
## 2. Main Loop Dispatcher — `SelectMainLoopAudioPath` ($C047)
After reset initialization, control flows to **`SelectMainLoopAudioPath`** ($C047), the main loop dispatcher:
```
SelectMainLoopAudioPath ($C047):
lda GameMode ; $3A
beq RunAudioOnlyFrame ; 0 = title/attract: audio only
lda PlayerModeOrScreenPhase ; $3C
cmp #$01
bne RunMainGameFrame ; !=1: gameplay frame
RunAudioOnlyFrame ($C051):
jsr UpdateAudioEngine ; $F820 (banked audio)
RunMainGameFrame ($C054):
jsr DecrementGameplayTimers ; $C3B0
jsr UpdateTitleAndGameplayState ; $C0BA
jsr RequestNmiProcessing ; $C05F
beq SelectMainLoopAudioPath ; loop until NMI signals frame ready
```
**`RequestNmiProcessing`** ($C05F): Sets `NmiFrameDelay` ($32) = 1, signaling NMI to do work.
**`WaitForFrameReady`** ($C063): Called after `RequestNmiProcessing` returns (via `beq` loop):
```
jsr AdvancePseudoRandomState ; $C42F (PRNG)
lda FrameReadyFlag ; $30
beq WaitForFrameReady ; wait for NMI to set it
inc FrameCounter ; $27
dec NmiFrameDelay ; $32
lda #$00 / sta FrameReadyFlag ; clear for next frame
rts
```
---
## 3. Interrupt Vectors — CPU $FFFA–$FFFF
Located at PRG offset $3FFA (end of 16 KB bank):
| Vector | Address | Handler |
|--------|---------|---------|
| NMI | $C08D | `NmiHandler` |
| RESET | $C000 | `ResetHandler` |
| IRQ | $C000 | `ResetHandler` (reused — unused placeholder) |
---
## 4. NMI / Frame Heartbeat — `NmiHandler` ($C08D)
The NMI fires once per frame and performs PPU updates, controller reading, and signals frame completion:
```
NmiHandler ($C08D):
pha / txa / pha / tya / pha ; preserve A,X,Y
lda #$00 / sta $2003 ; OAMADDR = 0
lda #$02 / sta $4014 ; OAM DMA from $0200
lda NmiFrameDelay ($32)
beq CompleteNmiAndRestoreRegisters ; if 0: minimal NMI (title screen)
jsr RunQueuedPpuTransfer ; $C1F5 — process queued PPU writes
jsr UpdatePpuTransferQueue ; $C370 — update queue pointers
jsr ResetPpuScroll ; $C424 — reset scroll registers
jsr UpdateControllerState_C328 ; $C328 — read controllers
ldy #$01 / sty FrameReadyFlag ; $30 = 1 (signal main loop)
CompleteNmiAndRestoreRegisters ($C0B0):
lda #$01 / sta NmiWorkMode ; $33 = 1 (signal NMI work done)
pla / tay / pla / tax / pla / rti
```
**Two NMI modes:**
- **NmiFrameDelay = 0** (title/attract): Only OAM DMA, no game logic.
- **NmiFrameDelay = 1** (gameplay): Full PPU queue flush, controller read, scroll reset, sets `FrameReadyFlag`.
---
## 5. Title / Gameplay State Machine — `UpdateTitleAndGameplayState` ($C0BA)
```
UpdateTitleAndGameplayState ($C0BA):
lda GameMode ($3A)
bne HandleStartAndPlayerSelection ; !=0: player select
lda ControllerButtons ($1A)
and #$10 ; START button
beq ClearScreenSubstate
ldy ScreenSubstate ($36)
bne DispatchCurrentScreenState
iny / sty ScreenSubstate ; first START press: advance substate
ldy CurrentScreenState ($3B)
cpy #$0D / beq StartGameplayCountdown
cpy #$06 / bne DispatchCurrentScreenState
ldx #$07
jsr CopyPlayerStateDefaults ; init player timers
; ... sets CurrentScreenState=$0D, PPUMASK=$0E, PPUCTRL=$81
jsr ClearAudioRequestFlags
beq SelectAlternateAudioMode ; audio request $02
ClearScreenSubstate ($C104):
lda #$00 / sta ScreenSubstate
DispatchCurrentScreenState ($C108):
lda CurrentScreenState ($3B)
jsr DispatchScreenStateHandler ($C3C6)
ScreenStateHandlerTable ($C10D): 14 entries (2 bytes each)
.word Handler_$C663, $C6AB, $C6D4, $C6EB, $C755, $C7C2, $C7DD, $C8C6, $C8C8
.word $C93E, $C94B, $C998, $C9AF, $C9C7, $C07C
```
**GameMode = 0 (Title):**
- Waits for START press (`ControllerButtons & $10`)
- `ScreenSubstate` tracks first press vs. subsequent
- `CurrentScreenState` selects which title screen handler (14 states)
- State $0D = start gameplay countdown (sets `GameplayTimer`=$14, `CurrentScreenState`=$0C)
- State $06 = player select (copies defaults, sets up audio)
**GameMode ≠ 0 (Player Select / Gameplay):**
- `HandleStartAndPlayerSelection` ($C129): reads START/SELECT, sets `GameMode`/`CurrentScreenState`
- `DispatchGameModePhase` ($C142): dispatches on `PlayerModeOrScreenPhase` ($3C)
- Screen state handlers at $C18D via second table ($C192): 15 entries for gameplay screens
---
## 6. Key Subsystems
### 6.1 PPU Transfer Queue — `$C1F5` / `$C370`
- `QueuedPpuTransferCount` ($34) tracks pending transfers
- Source pages from `QueuedPpuTransferSourcePageTable` ($C20A)
- Destination offsets from `QueuedPpuTransferDestinationOffsetTable` ($C206)
- `RunQueuedPpuTransfer` processes one per NMI; `UpdatePpuTransferQueue` refills
### 6.2 Controller Input — `UpdateControllerState_C328` ($C328)
- Reads `$4016` (strobe) + shift register
- Stores buttons in `ControllerButtons` ($1A), pressed edges in `ControllerPressedOrScriptInput` ($1B)
- Second controller in `SecondControllerPressed` ($1D)
### 6.3 Audio Engine — `UpdateAudioEngine` ($F820)
- Banked at $F800+ (called via trampoline)
- 4-channel sequencer: `AudioChannelStateArray` ($F0–$F3), sequence pointers ($F9/$FA)
- Note/period tables at $F970, $FAB3
- Sound effects via `StartSoundEffect` ($FAE6)
### 6.4 Screen State Handlers — `DispatchScreenStateHandler` ($C3C6)
- Title: 14 handlers (attract, copyright, high score, player select, etc.)
- Gameplay: 15 handlers (level intro, gameplay, bonus, intermission, game over, etc.)
- Each handler manages its own objects, timers, and transitions
### 6.5 Object / Actor System
- `ObjectState` ($0300+): 16+ slots, fields for type, position, timer, animation
- `ActorFlags` ($84), `ActorX` ($85), `ActorY` ($86), `ActorStateArray` ($70+)
- Collision detection via `ScreenCollisionState` ($44), `CollisionMask` ($BF)
### 6.6 PRNG — `AdvancePseudoRandomState` ($C42F)
- Called once per frame in `WaitForFrameReady`
- Used for enemy spawn patterns, bonus timing
---
## 7. Memory Map (Key Zero Page / RAM)
| Address | Name (from `locals.inc`) | Use |
|---------|--------------------------|-----|
| $00 | `ControllerSerialBits` | Controller shift register bits |
| $01–$02 | `WorkPointerOrValueLo/Hi` | General 16-bit pointer (RAM clear, PPU) |
| $03 | `SourceRecordPointerLo` | PPU transfer source pointer |
| $1A | `ControllerButtons` | Current button state |
| $1B | `ControllerPressedOrScriptInput` | Edge-detected presses |
| $20–$27 | `GameplayTimerArray` | 8 gameplay timers |
| $21 | `GameplayTimer` | Main gameplay countdown |
| $25 | `ScreenSelectionPositionOrDelay` | Menu cursor / delay |
| $27 | `FrameCounter` | Global frame counter |
| $28–$2F | `SavedGameplayTimerArray` | Backup timers |
| $30 | `FrameReadyFlag` | Set by NMI, cleared by main loop |
| $31 | `CollisionResultFlag` | Collision detection result |
| $32 | `NmiFrameDelay` | NMI work trigger (0=skip, 1=do work) |
| $33 | `NmiWorkMode` | NMI completion signal |
| $34 | `QueuedPpuTransferCount` | Pending PPU transfers |
| $36 | `ScreenSubstate` | Title screen sub-state |
| $37 | `StartInputLatch` | START button latch |
| $38 | `SelectedPlayerAndScreenFlags` | Player/screen selection |
| $3A | `GameMode` | 0=title, 1=player select, 2+=gameplay |
| $3B | `CurrentScreenState` | Active screen handler index |
| $3C | `PlayerModeOrScreenPhase` | Player mode / screen phase |
| $3D | `ReturnScreenState` | Return destination |
| $40–$41 | `SelectedPlayerIndex`, `SelectedScreenVariant` | Menu selections |
| $50+ | `GameplayObjectState`… | Object pool ($0300+) fields mirrored in ZP |
| $F0–$FF | Audio engine ZP | Channel state, sequences, envelopes |
| $0200 | `OamBuffer` | OAM DMA source (64 sprites) |
| $0300+ | `ObjectState`… | Main object/actor array |
| $06F0 | `Ram_06F0` | Warm-start signature |
| $0700+ | Extended RAM | Actor timers, collision, audio buffers |
---
## 8. Data Tables
| Table | Address | Description |
|-------|---------|-------------|
| `ScreenStateHandlerTable` | $C10D | 14 title screen handlers |
| `ScreenStateHandlerTable` (gameplay) | $C192 | 15 gameplay screen handlers |
| `QueuedPpuTransferSourcePageTable` | $C20A | PPU transfer source banks |
| `QueuedPpuTransferDestinationOffsetTable` | $C206 | PPU dest offsets (alias at $C206) |
| `PlayerStartXTable` | $8C | Player start X positions |
| `PlayerStartYTable` | $8D | Player start Y positions |
| `AudioPitchOrPeriodTable_F970` | $F970 | Note→period lookup |
| `AudioSequenceIndexTable_FAB3` | $FAB3 | Sequence index table |
| `IndexedGameplayLookupTable_FFC3` | $FFC3 | Gameplay bitmask/offset table |
| `GameplayConstantTable_FF4C` | $FF4C | Level/gameplay constants |
---
## 9. Summary of Execution Flow
```
Power On / Reset
│
▼
ResetHandler ($C000) ────► cld/sei, wait 2 VBlanks, init stack
│
├─ Clear $0000–$07FF (8 pages via WorkPointerOrValueLo/Hi)
│ └─ Warm-start: if FrameCounter≠0, skip clear, X=$5F
│
├─ stx Ram_06F0 (signature)
├─ jsr ClearNametable2400WithTileFF ($C1A2)
├─ jsr HideAllOamSprites ($C1EA)
├─ PPUSCROLL = 0,0
├─ GameMode = 1 (title)
├─ APU enable ($4015=$0F)
├─ PPUCTRL = $90 (NMI on)
│
▼
SelectMainLoopAudioPath ($C047) ─────────────────────────────┐
│ │
├─ GameMode=0: jsr UpdateAudioEngine ($F820), loop │
│ │
└─ GameMode≠0: │
jsr DecrementGameplayTimers ($C3B0) │
jsr UpdateTitleAndGameplayState ($C0BA) │
jsr RequestNmiProcessing ($C05F) → NmiFrameDelay=1 │
jsr WaitForFrameReady ($C063) ◄── wait for NMI ────┘
│
├─ jsr AdvancePseudoRandomState ($C42F)
├─ wait for FrameReadyFlag ($30)
├─ inc FrameCounter ($27), dec NmiFrameDelay
└─ clear FrameReadyFlag, rts
beq SelectMainLoopAudioPath (loop)
NMI ($C08D) [once per frame]
├─ Preserve A,X,Y
├─ OAM DMA ($4014 from $0200)
├─ lda NmiFrameDelay ($32)
│ ├─ 0: CompleteNmiAndRestoreRegisters (title)
│ └─ 1: RunQueuedPpuTransfer ($C1F5)
│ UpdatePpuTransferQueue ($C370)
│ ResetPpuScroll ($C424)
│ UpdateControllerState_C328 ($C328)
│ ldy #$01 / sty FrameReadyFlag ($30)
├─ lda #$01 / sta NmiWorkMode ($33)
└─ Restore A,X,Y, rti
Title Screen ($C0BA, GameMode=0)
├─ Wait for START (ControllerButtons & $10)
├─ ScreenSubstate tracks first/second press
├─ CurrentScreenState selects handler (14 states at $C10D)
├─ State $0D: StartGameplayCountdown → GameplayTimer=$14, State=$0C
└─ State $06: CopyPlayerStateDefaults → init timers, audio
Player Select / Gameplay ($C129, GameMode≠0)
├─ HandleStartAndPlayerSelection: START/SELECT → GameMode, CurrentScreenState
├─ DispatchGameModePhase ($C142): PlayerModeOrScreenPhase ($3C) → handler
├─ DispatchScreenState ($C18D): 15 gameplay handlers ($C192 table)
└─ Gameplay loop: objects, collision, scoring, level progression
```
---
## 10. Naming and Residue
**Pass 1/2/3 complete** — `prg.s` has **zero** `Label_` definitions. All routines carry semantic names from Pass 2 (`ResetHandler`, `NmiHandler`, `UpdateTitleAndGameplayState`, `DispatchScreenStateHandler`, etc.) and internal branch targets use `<Routine>_Block[_n]` convention.
**Pass 3 symbols applied (2026-10-19):** `_tools/apply_symbols.py` substituted **75 operands** (17 single-name + 58 hardware) from `symbols.inc`/`locals.inc`. Zero residue.
**Overlapping table aliases** (expressed as relocatable symbols):
- `QueuedPpuTransferDestinationOffsetTable = RunQueuedPpuTransfer + $11` ($C206)
- `AudioPitchOrPeriodTable_F970 = Flow_F96F + $01` ($F970)
- `AudioSequenceIndexTable_FAB3 = Flow_FA85 + $2E` ($FAB3)
**Vectors** at PRG $3FFA: `.word NmiHandler, ResetHandler, ResetHandler`
Build verification: `make` in `copilot/` produces `reconstructed.nes`, compared whole-ROM against `rom.nes` (byte-identical).