# Pac-Man (USA) (Namco) — Game Flow from Power-On This document describes the execution flow of the disassembled Pac-Man (USA) (Namco) 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`. --- ## 1. ROM and Memory Layout | Property | Value | |----------|-------| | Format | iNES / NES 2.0 | | PRG | 1 × 16 KB (mapped at CPU $C000–$FFFF) | | CHR | 1 × 8 KB | | Mirroring | Horizontal | | PRG bank start | CPU $C000 | | Source file | `copilot/prg.s` (14,332 lines) | | Output | `reconstructed.nes` (whole ROM: 16-byte header + 16,384-byte PRG + 8,192-byte CHR) | ### Interrupt vectors (CPU $FFFA–$FFFF) | Vector | Address | Handler | |--------|---------|---------| | NMI | $C10E | `NmiHandler` | | RESET | $C033 | `Reset` | | IRQ / BRK | $C17E | `IrqHandler` (just `rti` — unused) | Additional ROM data at $FFF6–$FFF9: - $FFF6/$FFF7 = $ECEC (little-endian), used by `DrawMazeNametable` as a source pointer ($00/$01) - $FFF8/$FFF9 = $C10E (little-endian) --- ## 2. Build Status `make` in `copilot/` assembles `prg.s` with `ca65`, links with `ld65`, and verifies against `prg.bin`. **Verification result: BYTE-IDENTICAL** — `reconstructed.nes` matches `rom.nes` exactly (header + PRG + CHR). --- ## 3. Reset — CPU $FFFC → $C033 **`Reset`** ($C033) On power-on or reset: 1. `ldx #$00 / stx PPUCTRL` — disable PPU rendering/NMI. 2. `sei / cld` — disable maskable interrupts, clear decimal mode. 3. `dex / txs` — initialize stack pointer to $FF. 4. `nop x3` — three NOP padding bytes. 5. **Wait for two VBlanks** — `@WaitForPpu` loop: set PPUMASK=$06, poll PPUSTATUS bit 7 twice (X counts down from 2). This is the PPU warm-up delay. 6. **Clear low RAM** — `@ClearLowRam`: zero $0000–$003D. 7. **Clear RAM pages** — `@ClearRamPages`: using indirect pointer at $00/$01, clear pages starting at $0100 upward until high byte reaches $08 (effectively clearing $0100–$07FF). 8. `PPUMASK=$06`, scroll=$00 — enable background + sprites, reset scroll. 9. **Warm-start signature check** — `@CheckRamSignature`: compare RAM at $0052 against the 15-byte string `"HIROKI AOYAGI"` plus `$AA,$55` (stored at `RamSignature`, $C354–$C366): - If all 15 bytes match → warm start: skip full RAM clear, go to `@SignatureAlreadyPresent`. - If any byte differs → cold start: `@InitializeRamSignature` clears all of zero page, then copies the signature string to $0052–$0060. 10. Set $64=$01 (some flag), set gameState=$00. 11. Set $45=$07, then check cocktail/active-player flags at $46/$47. If both nonzero, perform a 16-byte swap loop (`@SwapLoop`) swapping bytes between $67,y and $77,y for Y=0..15. 12. `APUCTRL=$1F`, `APUFRAMECNT=$C0` — enable all APU channels, set frame counter mode. 13. `jsr InitializeAudioEngine` ($E1F8 region) — clear audio request/channel state. 14. `jsr $EEB4` — audio/sound engine init. 15. Set PPUCTRL=$88, `ppuCtrlShadow`=$88 — enable NMI, $2000 nametable base. 16. Set $48=$FF (gameplay suspended flag), clear $023F and $024B (PPU update buffers). 17. Set $36=$01 (some state flag). 18. `jmp MainEntry` — transfer control to the main program. **`RamSignature`** ($C354): `.byte "HIROKI AOYAGI", $AA, $55` — developer credit / warm-start validation signature. --- ## 4. NMI / Frame Heartbeat — `NmiHandler` ($C10E) The NMI fires once per frame and is the central real-time driver: 1. **Save registers** (A, X, Y on stack). 2. **PPUMASK=$1E** — enable background + sprites with specific emphasis settings. 3. **OAM DMA**: write $00 to OAMADDR ($2003), $00 to $40, then `lda $45 / sta OAMDMA` ($4014) — DMA from page $45 (i.e., $4500–$45FF) to sprite RAM. 4. **`jsr FlushGameplayPpuUpdates`** ($DE3B region) — flush pending PPU gameplay update blocks at $023F and $0245. 5. **`jsr ReadNearbyNametableTiles`** — read nametable tile data around Pac-Man (likely for collision/tunnel logic). 6. **`jsr UpdatePalettes`** — upload palette data from RAM buffers $0299 and $02A9 to PPU palette $3F00/$3F10 when those buffers are marked active ($0F marker). 7. **Restore scroll**: write $41 and $42 to PPUSCROLL (two writes — horizontal then vertical). 8. **Construct PPUCTRL**: `lda $47 / and $46 / and #$01 / asl / ora $43` — builds the PPUCTRL value from shadow/flag bits, writes to PPUCTRL. 9. **Controller strobe**: write $01 then $00 to JOY1 ($4016). 10. **Read both controllers** — 8 iterations each: - Controller 1: read JOY1, mask with #$03, compare with #$01, rotate into $4D (`currentInput`). - Controller 2: read `APUFRAMECNT` ($4017), mask with #$03, compare with #$01, rotate into $4E. 11. **Select active controller**: X = `currentInput` ($4D) by default; if `cocktailModeFlag` ($46) & `activePlayerFlag` ($47) both nonzero, X = `newlyPressedInput` ($4E). Store X to `previousInput` ($4F). 12. **`inc frameCounter`** ($4B) — increment per-frame counter. 13. **$48 gate**: if $48 (`gameplaySuspended`) != 0, skip to `@NmiDone`. 14. **`jsr InitializeAudioChannels`** — scan and activate audio channel slots. 15. **`@NmiDone`**: restore registers (Y, X, A), `rti`. The label `IrqHandler` appears at the `rti` following NMI's register restore — it is the same code address, just an alternate entry label for the IRQ vector (which points to the same `rti`). ### `UpdatePalettes` Conditionally uploads 16-byte palette blocks from RAM $0299 (to PPU $3F00) and $02A9 (to PPU $3F10) when the corresponding buffer marker byte equals $0F. After upload, resets the marker and PPU address to $3000. --- ## 5. Main Entry — `MainEntry` ($C573) After Reset, control transfers to `MainEntry`. This is the foreground state-machine entry: 1. **`$40=$01`** — set the NMI sync flag (NMI will clear it each frame). 2. **`@WaitForInitialNmi`** — spin until NMI clears $40 to $00 (synchronize to first VBlank). 3. **PPUCTRL=$88 / ppuCtrlShadow=$88** — enable NMI. 4. **PPUMASK=$00** — disable rendering. 5. **Clear OAM page** — fill $0700–$07FF with 0. 6. `cocktailModeFlag=$00`, then call: - `jsr ClearGameplayNametables` - `jsr InitializeNametableAttributes` - `jsr InitializeNametableData` - `jsr $E419` — additional init (address-named). 7. **Initialize palette buffers** — copy 32 bytes from `TitleScreenPalette` to RAM $0299–$02B8. 8. Set `actorStatusDisplay` ($6C–$6F) = $2D, set $023F=$FF, `gameplaySuspended`=$FF. 9. Clear $47, $46, $42, $41, `stateTimer` ($4C), $87, $88. 10. **PPUCTRL selection**: if `gameState` ($3F) != 0, use $8A; else use $8A as well (both paths end up with $8A; the `bne` is effectively a no-op in current code). Write to PPUCTRL and `ppuCtrlShadow`. 11. **`MainDispatchLoop`** ($C645): - `$40=$01`, spin until NMI clears it (`@WaitForNmi`). - `Y = gameState` ($3F), load 16-bit handler address from `MainStateHandlerTable` ($C267) into $10/$11. - `jmp ($0010)` — indirect jump to the state handler. **`MainStateHandlerTable`** ($C267–$C26E) — 4 entries, indexed by `gameState` as a byte offset (even values $00, $02, $04, $06): | Offset | Handler | Description | |--------|---------|-------------| | $00 | `InitialStateHandler` | Title/input-wait state | | $02 | `PlayerSelectionState` | 1P/2P selection | | $04 | `State04Handler` | Substate dispatch machine | | $06 | `TimedStateTransition` | Timed state transition | --- ## 6. Main State Handlers ### `TimedStateTransition` ($C27A) — state offset $06 - Decrement `$0F` countdown. If nonzero, return to `MainDispatchLoop`. - When zero: set `gameState`=$02, return to `MainDispatchLoop`. - This is a timed transitional state: another state places a duration in `$0F` and selects state $06; this handler counts down before advancing to state $02. ### `InitialStateHandler` ($C27A region, actually at $C27A? No — at $C27A is TimedStateTransition; InitialStateHandler is at $C27A? Let me re-check) Actually from the listing: - `TimedStateTransition` is at $C27A - `InitialStateHandler` is at $C27A? No. Let me re-read: Line 730: `TimedStateTransition:` is at $C27A (based on comment "$C27A-$C2A5: Timed State Transition Handler" — wait, the comment says $C27A but the code shows `dec $0F` at line 731. Then line 740: `; $C27A-$C2A5: Initial/Input State Handler` and line 766: `InitialStateHandler:`. So there's a discrepancy in comments. The actual code: - `TimedStateTransition` = `dec $0F / bne MainDispatchLoop / lda #$02 / sta gameState / jmp MainDispatchLoop` — this is state $06's handler. - `InitialStateHandler` = checks controller bits $0C; if set → gameState=$06, $0F=$5A, jump MainEntry. Else increment $42; if $42 reaches $F0 → reset $42, ppuCtrlShadow=$88, gameState=$06, $0F=$5A. Otherwise return to MainDispatchLoop. ### `PlayerSelectionState` ($C546 region) — state offset $02 - If $87|$88 != 0 → `@AdvanceTimeout` (16-bit timeout counter). - Else (first entry): copy `PlayerCountSelectionDisplayData` ($C507–$C50E) to RAM $024B until $FF terminator. Capture controller bits $04 and $08 into `pacmanRequestedDirection` ($50) and `pacmanDirection` ($51). - `@AdvanceTimeout`: increment 16-bit counter at $87/$88. If high byte reaches $02 → reset counter, set gameState=$04, jump MainDispatchLoop. - `@CheckSelectionInput`: if controller bit $04 changed → store as `pacmanRequestedDirection`, reset counter, set $87=$01, toggle `activePlayerFlag` ($47) between 0/1, update display marker tiles at $024D/$0251 from `PlayerCountSelectionMarkerTiles` ($C50F–$C511). - `@CheckConfirmInput`: if controller bit $08 changed → store as `pacmanDirection`. If bit $08 now active → clear `gameplaySuspended`, `jsr $EEB4`, jump `GameModeEntry`. **`PlayerCountSelectionDisplayData`** ($C507): `$22,$0A,$5C,$00, $22,$4A,$20,$FF` — copied to $024B on entry. **`PlayerCountSelectionMarkerTiles`** ($C50F): `$5C,$20,$5C` — selection marker toggle data. ### `State04Handler` ($C52E region) — state offset $04 - If controller bits $0C set → gameState=$02, jump MainEntry (restart). - Else: `Y = $87` (substate offset), load 16-bit handler from `State04SubstateTable` ($C52E), jump indirect via $10/$11. **`State04SubstateTable`** ($C52E) — 12 entries indexed by even offsets $00–$16: | Offset | Handler | |--------|---------| | $00 | `State04Initialize` | | $02–$12 | `State04TimedSubstate` (shared, 6 entries) | | $14 | `State04LongDelay` | | $16 | `State04FinalPhase` | - `State04Initialize` ($C573): sync to NMI via $40, PPUCTRL=$08, disable rendering, clear $88, call `$C5D8`, `InitializeState04PpuData` ($C608), `LoadState04DisplayData` ($C5A6), advance $87 by 2, PPUCTRL=$88, return to loop. - `State04TimedSubstate` ($C589): increment $88; at $30 frames → clear $88, call `$C5A6`, advance $87 by 2. - `State04LongDelay` ($C5A6): increment $88; at $80 frames → advance $87 by 2 to substate $16. - `State04FinalPhase` ($C5D7): sets $47=$01, tail-jumps to `GameModeEntry`. - `SetSelectionFlagAndContinue` ($C5D7): `activePlayerFlag=$01`, `jmp GameModeEntry`. ### `LoadState04DisplayData` ($C5A6) Uses $87 as offset into `CharacterIntroDisplayDataTable` ($C687), copies $FF-terminated stream to RAM $024B. Optional secondary copy of 16 bytes from `CharacterIntroSpriteData` based on substate offset. **`CharacterIntroDisplayDataTable`** ($C687) — 10 pointers to display streams for characters/nicknames (SHADOW, BLINKY, SPEEDY, PINKY, etc.) plus scoring legend. --- ## 7. Game Mode Entry — `GameModeEntry` ($C328) Reached from `SetSelectionFlagAndContinue` (state $04 final phase). This transitions from the attract/selection sequence into gameplay: 1. PPUCTRL=$88, ppuCtrlShadow=$88. 2. Wait for VBlank. 3. PPUMASK=$00, gameState=$00, cocktailModeFlag=$00, $69=$00, $4A=$00. 4. `jsr ClearGameplayNametables`, `jsr DrawGameplayHudLabels`, `jsr DrawGameplayScoreFields`. 5. If `gameplaySuspended` != 0 → roundNumber=$01 (continue existing game). 6. Else → clear 32 bytes starting at roundNumber ($67–$86), set roundNumber=$03, $77=$03, clear $0600/$0601. 7. Common init: roundDisplayValue=$FF, $78=$FF, $0245=$FF, ppuCtrlShadow=$88, PPUCTRL=$88. 8. **`GameModeFrameSync`** ($C360): `$40=$01`, wait for NMI clear ($@WaitForNmi`), then: - If `gameplaySuspended` != 0 AND controller bits $0C set → gameState=$02, jump MainEntry (restart). - If `stateTimer` ($4C) != 0 → decrement, return to `@WaitForNextFrame`. - Else `@DispatchState`: `Y = gameState`, load handler from `GameModeStateTable` ($CAD7) into $10/$11, indirect jump. **`GameModeStateTable`** ($CAD7) — 8 entries indexed by even offsets $00–$0E: | Offset | Handler | |--------|---------| | $00 | `GameModeState00` | | $02 | `GameModeState02` | | $04 | `GameModeState04` | | $06 | `GameModeState06` | | $08 | `GameModeState08` | | $0A | `GameModeState0A` | | $0C | `GameModeState0C` | | $0E | `InitializeGameplayTransitionState` | ### `GameModeState04` ($CAE9) — pause/per-frame handler - If $060F != 0 → return to `GameModeFrameSync`. - Else: edge-detect controller bit $08 using $49 as previous state. On new press → increment $4A, install pause display commands at $024B, toggle pause display via bit 0 of $4A. - When not paused and bit 0 of $4A clear → call gameplay helper chain before returning to `GameModeFrameSync`. - Adjacent data at $CB5B contains tile values for "PAUSE" text — confirms this is the pause-state handler. --- ## 8. PPU / Rendering ### `DrawMazeNametable` ($E2E4) Draws the maze nametable using compressed data: - PPU destination selected based on cocktail mode ($47 & $46): $2000 or $2080. - Source pointer from `MazeDataPointerBytes` at $FFF6–$FFF7 ($ECEC → points to ROM data). - Each row: set PPU address, decode 2-bit-run-length-encoded bytes from the source (2 bits = repeat count-1, 6 bits = tile value), write $16 (22) bytes per row. - Advance PPU destination by $20 per row. ### `FlushGameplayPpuUpdates` ($DE3B) - If $48 (`gameplaySuspended`) == 0 → process update block at $023F (6 bytes + terminator to PPU). - Else → jump to `UpdateAnimatedGameplayPpuFields`. - Block at $0245 processed similarly if nonzero. --- ## 9. Audio System **`InitializeAudioEngine`** ($E1F8): - Clear audio request pointer ($F0/$F1 = $0006), channel table pointer ($F2/$F3 = $0620), sequence table pointer from ROM $F100/$F101. - audioWorkValue=$40, APUSTATUS=$1F, APUFRAMECNT=$C0. - Clear 16 bytes through ($F0), clear 16 entries at 8-byte stride through ($F2). **`InitializeAudioChannels`** ($EED0): - Clear audio channel flags and work values, scan 16 eight-byte slots through ($F2). - Slots with values $01–$04 activate a channel: copy 4-byte parameters through indirect pointer $F6. - Falls through to runtime command processing at $EF2D. **`UpdateAudioChannels`** ($EF2D): - For each active slot: decrement countdown, fetch command byte from sequence table, decode command ($F0+ = special, $C0+ = delay, else = note/frequency), update APU registers ($4000–$400F). **`UpdateAudio`** ($F8DE): - APUFRAMECNT=$C0, update Pulse 1, Pulse 2, Noise, Sound Effect channels. - Clear $FF–$FB; if $07FF != 0 → APUDMALOAD, decrement $07FF. Audio data tables at $FF00–$FFF5 (period/mask tables), $FF64–$FFEA (sequences/instruments), $F450–$F49A (runtime command data). --- ## 10. Zero-Page / RAM Symbols | Address | Symbol | Use | |---------|--------|-----| | $00–$01 | workPtr0/1 | General indirect pointers | | $02–$09 | workValue0–5 | General scratch values | | $0A | pathCompareValue | Maze pathfinding | | $0B | pathFallbackValue | Maze pathfinding | | $0C | legalDirections | Maze pathfinding | | $10 | dispatchVector | State dispatch indirect pointer | | $12–$15 | arithmeticValue/Operand | 16-bit arithmetic | | $1A–$2D | pacmanMotion–ghost4Motion | Actor motion records (4 bytes each) | | $2E–$2F | pacmanTargetRow/Column | Pac-Man maze target | | $32 | pacmanSpriteFrame | Pac-Man sprite frame | | $33 | ghostSpriteFrames | Ghost sprite frames | | $39 | ghostDirectionFlags | Ghost direction/control state | | $3F | gameState | Main gameplay state | | $40 | — | NMI sync flag ($01=pending, NMI clears to $00) | | $41 | ppuCtrlShadow | PPUCTRL mirror | | $42 | — | Second PPUSCROLL write | | $46 | cocktailModeFlag | Cocktail cabinet mode | | $47 | activePlayerFlag | Active player (0/1) | | $48 | gameplaySuspended | Gameplay suspended flag | | $4B | frameCounter | Per-frame counter | | $4C | stateTimer | State countdown timer | | $4D | currentInput | Controller 1 input | | $4E | newlyPressedInput | Newly pressed input | | $4F | previousInput | Previous input state | | $50 | pacmanRequestedDirection | Requested direction | | $51 | pacmanDirection | Current direction | | $61 | highScoreDigits | High score digits | | $67 | roundNumber | Round number | | $68 | roundDisplayValue | Round display value | | $6A | pelletsRemaining | Pellets remaining | | $6B | extraLifeAwarded | Extra life flag | | $6C–$6F | actorStatusDisplay | Actor status display | | $70 | playerScoreDigits | Player score digits | | $F0–$F7 | audioRequestPtr…audioWorkValue | Audio engine pointers/values | | $F8 | audioChannelFlags | Audio channel flags | | $FC | audioRequestIndex | Audio request index | | $FD | audioChannelOffset | Audio channel offset | | $FE–$FF | audioIndirectPtr | Audio indirect pointer | --- ## 11. Data Tables | Label | Address | Description | |-------|---------|-------------| | `CopyrightText` | $C004 | "COPYRIGHT 1980 1993 NAMCO LTD. ALL RIGHTS RESERVED" | | `RamSignature` | $C354 | `"HIROKI AOYAGI" $AA $55` — warm-start signature | | `MainStateHandlerTable` | $C267 | 4 main state handler pointers | | `State04SubstateTable` | $C52E | 12 state-$04 substate handler pointers | | `GameModeStateTable` | $CAD7 | 8 game-mode state handler pointers | | `CharacterIntroDisplayDataTable` | $C687 | 10 display data pointers for character intro | | `TitleScreenTileBlock` | $C326 | 6 rows × $17 bytes of title tile data | | `TitleScreenPpuRecords` | $C3B0 | 9 PPU update records for title screen | | `TitleScreenPalette` | $C43F | 32-byte initial palette | | `TitleScreenAttributeData` | $C45F | $18 attribute bytes | | `PlayerCountSelectionDisplayData` | $C507 | 8 bytes for player-count display | | `PlayerCountSelectionMarkerTiles` | $C50F | $5C,$20,$5C marker toggle | | `CharacterIntroPalette` | $C667 | 32-byte character intro palette | | `CharacterIntroDisplayDataTable` | $C687 | Character intro display pointers | | `CharacterNicknameHeaderData` | $C69B | "CHARACTER ; NICKNAME" display data | | `MazeDataPointerBytes` | $FFF6 | $EC,$EC,$0E,$C1 — maze data pointer ($C10E) | | Audio tables | $FF00–$FFEA | Period/mask tables, sequences, instruments | --- ## 12. Summary of Execution Flow ``` Power On / Reset │ ▼ Reset ($C033) ───────────────────────────────────────────────┐ │ sei, cld, disable PPU, wait 2× VBlank │ │ clear RAM $0000–$003D, clear pages $0100–$07FF │ │ PPUMASK=$06, scroll=$00 │ │ check warm-start signature "HIROKI AOYAGI" + $AA,$55 │ │ valid → skip full clear; invalid → clear + save sig │ │ set flags, optional 16-byte swap ($67↔$77) │ │ APUCTRL=$1F, APUFRAMECNT=$C0 │ │ jsr InitializeAudioEngine, jsr $EEB4 │ │ PPUCTRL=$88 (NMI enabled), $48=$FF (suspended) │ │ jmp MainEntry │ │ │ MainEntry ($C573) │ │ $40=$1, wait for NMI to clear it │ │ PPUCTRL=$88, PPUMASK=$00, clear OAM │ │ clear cocktail/player flags, $41/$42/$4C/$87/$88 │ │ init nametables, attributes, palette buffers │ │ select PPUCTRL ($8A), set ppuCtrlShadow │ │ │ MainDispatchLoop ($C645) ─────────────────────────────────────┤ │ $40=$1, wait for NMI │ │ Y = gameState ($3F) │ │ load handler from MainStateHandlerTable ($C267) │ │ jmp ($0010) → state handler │ │ │ ├─ State $00: InitialStateHandler │ │ ├─ controller bits $0C → gameState=$06, $0F=$5A │ │ │ → jump MainEntry (restart sync) │ │ └─ else inc $42; if $42==$F0 → reset, gameState=$06 │ │ │ ├─ State $02: PlayerSelectionState │ │ ├─ 1P/2P selection via controller bits $04/$08 │ │ ├─ timeout ($87/$88 reaches $0200) → gameState=$04 │ │ └─ confirm (bit $08) → clear suspended, jsr $EEB4, │ │ jmp GameModeEntry │ │ │ ├─ State $04: State04Handler │ │ ├─ controller bits $0C → gameState=$02, restart │ │ └─ Y=$87 → dispatch via State04SubstateTable ($C52E) │ │ ├─ $00: State04Initialize (PPU setup, $87+=2) │ │ ├─ $02–$12: State04TimedSubstate ($30 frames) │ │ ├─ $14: State04LongDelay ($80 frames) │ │ └─ $16: State04FinalPhase → $47=$1, │ │ jmp GameModeEntry │ │ │ └─ State $06: TimedStateTransition │ │ dec $0F; if nonzero → MainDispatchLoop │ │ else gameState=$02 → MainDispatchLoop │ │ │ └─ loop back to MainDispatchLoop │ │ │ GameModeEntry ($C328) [from SetSelectionFlagAndContinue] │ │ │ ├─ PPUCTRL=$88, wait VBlank, PPUMASK=$00 │ ├─ clear gameplay state, HUD labels, score fields │ ├─ if not suspended: clear player state, roundNumber=$3 │ │ else: roundNumber=$1 │ ├─ common init: roundDisplayValue=$FF, PPUCTRL=$88 │ │ │ GameModeFrameSync ($C360) ────────────────────────────────────┤ │ $40=$1, wait for NMI │ │ │ ├─ if suspended && controller bits $0C → gameState=$02, │ │ jmp MainEntry (restart) │ │ │ ├─ if stateTimer != 0 → dec, return to @WaitForNextFrame │ │ │ └─ Y = gameState → dispatch via GameModeStateTable ($CAD7)│ │ │ ├─ $00: GameModeState00 │ ├─ $02: GameModeState02 │ ├─ $04: GameModeState04 (pause/input handler) │ │ ├─ $060F != 0 → return to frame sync │ │ ├─ edge-detect controller bit $08 ($49) │ │ │ → new press: inc $4A, install pause cmds │ │ │ → bit 0 of $4A toggles pause display │ │ └─ not paused, bit0 of $4A clear → gameplay │ │ helper chain → return to frame sync │ ├─ $06–$0C: GameModeState06/08/0A/0C │ └─ $0E: InitializeGameplayTransitionState │ │ │ └─ loop back to GameModeFrameSync │ │ │ └─ (eventually enters gameplay via $0E handler) │ │ NMI ($C10E) [once per frame] │ │ │ ├─ save A, X, Y │ ├─ PPUMASK=$1E │ ├─ OAM DMA ($4500–$45FF → $0200–$02FF) │ ├─ jsr FlushGameplayPpuUpdates ($DE3B) │ ├─ jsr ReadNearbyNametableTiles │ ├─ jsr UpdatePalettes ($3F00/$3F10 from $0299/$02A9) │ ├─ restore scroll ($41,$42 → PPUSCROLL) │ ├─ construct PPUCTRL from $47,$46,$43 │ ├─ strobe JOY1, read both controllers (8 bits each) │ │ → $4D (current), $4E (newly pressed) │ ├─ select active controller → $4F (previousInput) │ ├─ inc frameCounter ($4B) │ ├─ if $48==0: jsr InitializeAudioChannels │ ├─ restore registers, rti │ │ │ IrqHandler ($C17E): rti (unused) │ │ Audio Engine (from NMI via InitializeAudioChannels/$EEB4) │ ├─ 16 eight-byte channel slots scanned │ │ │ values $01–$04 activate a channel │ │ │ 4-byte parameters copied via $F6 pointer │ ├─ UpdateAudioChannels ($EF2D): │ │ │ decrement countdowns, fetch command bytes, │ │ │ decode ($F0+=special, $C0+=delay, else=note), │ │ │ write APU registers $4000–$400F │ └─ UpdateAudio ($F8DE): 4 channels + SFX + DMA trigger │ ``` --- ## 13. Notes - **Build verification:** `make` in `copilot/` produces `reconstructed.nes`, compared whole-ROM against `rom.nes`. - Vectors confirm NMI at $C10E, RESET at $C033, IRQ at $C17E (a bare `rti`, unused). - The foreground main loop (`MainDispatchLoop` at $C645) is strictly NMI-synchronized via `$40`: NMI clears it each frame, and the main code spins waiting for it to clear before dispatching to the next state handler. - `GameModeEntry` ($C328) is a second, separate state machine entered from the state-$04 final phase. It has its own frame sync (`$40`) and its own 8-state dispatch table (`GameModeStateTable` at $CAD7), leading into gameplay initialization. - State $04 contains a nested secondary state machine (`State04SubstateTable` at $C52E) with 12 substate offsets, implementing a timed character-introduction sequence (GHOST names, nicknames, scoring legend). - The warm-start signature `"HIROKI AOYAGI"` at $0052 allows the game to preserve RAM across a soft reset. - Many routine labels are address-based (`State04TimedSubstate`, `LoadState04DisplayData`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional. - `symbols.inc` (embedded at the top of `prg.s`) provides NES hardware register names, controller masks, and extensive zero-page symbol definitions.