# 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 `_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).