28 KiB
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 | Vertical |
| PRG bank start | CPU $C000 |
| Source file | copilot/prg.s (14,332 lines) |
| Output | reconstructed_prg.bin (16,384 bytes) |
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
DrawMazeNametableas 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_prg.bin matches prg.bin exactly (all 16,384 bytes), and prg.bin matches the PRG bank of rom.nes (skipping the 16-byte iNES header).
3. Reset — CPU $FFFC → $C033
Reset ($C033)
On power-on or reset:
ldx #$00 / stx PPUCTRL— disable PPU rendering/NMI.sei / cld— disable maskable interrupts, clear decimal mode.dex / txs— initialize stack pointer to $FF.nop x3— three NOP padding bytes.- Wait for two VBlanks —
@WaitForPpuloop: set PPUMASK=$06, poll PPUSTATUS bit 7 twice (X counts down from 2). This is the PPU warm-up delay. - Clear low RAM —
@ClearLowRam: zero $0000–$003D. - 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). PPUMASK=$06, scroll=$00 — enable background + sprites, reset scroll.- Warm-start signature check —
@CheckRamSignature: compare RAM at $0052 against the 15-byte string"HIROKI AOYAGI"plus$AA,$55(stored atRamSignature, $C354–$C366):- If all 15 bytes match → warm start: skip full RAM clear, go to
@SignatureAlreadyPresent. - If any byte differs → cold start:
@InitializeRamSignatureclears all of zero page, then copies the signature string to $0052–$0060.
- If all 15 bytes match → warm start: skip full RAM clear, go to
- Set $64=$01 (some flag), set gameState=$00.
- 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. APUCTRL=$1F,APUFRAMECNT=$C0— enable all APU channels, set frame counter mode.jsr InitializeAudioEngine($E1F8 region) — clear audio request/channel state.jsr $EEB4— audio/sound engine init.- Set PPUCTRL=$88,
ppuCtrlShadow=$88 — enable NMI, $2000 nametable base. - Set $48=$FF (gameplay suspended flag), clear $023F and $024B (PPU update buffers).
- Set $36=$01 (some state flag).
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:
- Save registers (A, X, Y on stack).
- PPUMASK=$1E — enable background + sprites with specific emphasis settings.
- 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. jsr FlushGameplayPpuUpdates($DE3B region) — flush pending PPU gameplay update blocks at $023F and $0245.jsr ReadNearbyNametableTiles— read nametable tile data around Pac-Man (likely for collision/tunnel logic).jsr UpdatePalettes— upload palette data from RAM buffers $0299 and $02A9 to PPU palette $3F00/$3F10 when those buffers are marked active ($0F marker).- Restore scroll: write $41 and $42 to PPUSCROLL (two writes — horizontal then vertical).
- Construct PPUCTRL:
lda $47 / and $46 / and #$01 / asl / ora $43— builds the PPUCTRL value from shadow/flag bits, writes to PPUCTRL. - Controller strobe: write $01 then $00 to JOY1 ($4016).
- 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.
- Controller 1: read JOY1, mask with #$03, compare with #$01, rotate into $4D (
- Select active controller: X =
currentInput($4D) by default; ifcocktailModeFlag($46) &activePlayerFlag($47) both nonzero, X =newlyPressedInput($4E). Store X topreviousInput($4F). inc frameCounter($4B) — increment per-frame counter.- $48 gate: if $48 (
gameplaySuspended) != 0, skip to@NmiDone. jsr InitializeAudioChannels— scan and activate audio channel slots.@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:
$40=$01— set the NMI sync flag (NMI will clear it each frame).@WaitForInitialNmi— spin until NMI clears $40 to $00 (synchronize to first VBlank).- PPUCTRL=$88 / ppuCtrlShadow=$88 — enable NMI.
- PPUMASK=$00 — disable rendering.
- Clear OAM page — fill $0700–$07FF with 0.
cocktailModeFlag=$00, then call:jsr ClearGameplayNametablesjsr InitializeNametableAttributesjsr InitializeNametableDatajsr $E419— additional init (address-named).
- Initialize palette buffers — copy 32 bytes from
TitleScreenPaletteto RAM $0299–$02B8. - Set
actorStatusDisplay($6C–$6F) = $2D, set $023F=$FF,gameplaySuspended=$FF. - Clear $47, $46, $42, $41,
stateTimer($4C), $87, $88. - PPUCTRL selection: if
gameState($3F) != 0, use $8A; else use $8A as well (both paths end up with $8A; thebneis effectively a no-op in current code). Write to PPUCTRL andppuCtrlShadow. MainDispatchLoop($C645):$40=$01, spin until NMI clears it (@WaitForNmi).Y = gameState($3F), load 16-bit handler address fromMainStateHandlerTable($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
$0Fcountdown. If nonzero, return toMainDispatchLoop. - When zero: set
gameState=$02, return toMainDispatchLoop. - This is a timed transitional state: another state places a duration in
$0Fand 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:
TimedStateTransitionis at $C27AInitialStateHandleris 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 intopacmanRequestedDirection($50) andpacmanDirection($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 aspacmanRequestedDirection, reset counter, set $87=$01, toggleactivePlayerFlag($47) between 0/1, update display marker tiles at $024D/$0251 fromPlayerCountSelectionMarkerTiles($C50F–$C511).@CheckConfirmInput: if controller bit $08 changed → store aspacmanDirection. If bit $08 now active → cleargameplaySuspended,jsr $EEB4, jumpGameModeEntry.
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 fromState04SubstateTable($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 toGameModeEntry.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:
- PPUCTRL=$88, ppuCtrlShadow=$88.
- Wait for VBlank.
- PPUMASK=$00, gameState=$00, cocktailModeFlag=$00, $69=$00, $4A=$00.
jsr ClearGameplayNametables,jsr DrawGameplayHudLabels,jsr DrawGameplayScoreFields.- If
gameplaySuspended!= 0 → roundNumber=$01 (continue existing game). - Else → clear 32 bytes starting at roundNumber ($67–$86), set roundNumber=$03, $77=$03, clear $0600/$0601.
- Common init: roundDisplayValue=$FF, $78=$FF, $0245=$FF, ppuCtrlShadow=$88, PPUCTRL=$88.
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 fromGameModeStateTable($CAD7) into $10/$11, indirect jump.
- If
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
MazeDataPointerBytesat $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:
makeincopilot/producesreconstructed_prg.binthat is byte-identical toprg.binand to the PRG bank ofrom.nes. - Vectors confirm NMI at $C10E, RESET at $C033, IRQ at $C17E (a bare
rti, unused). - The foreground main loop (
MainDispatchLoopat $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 (GameModeStateTableat $CAD7), leading into gameplay initialization.- State $04 contains a nested secondary state machine (
State04SubstateTableat $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 ofprg.s) provides NES hardware register names, controller masks, and extensive zero-page symbol definitions.