Standardizes build process and adds OVERVIEW.md for each

This commit is contained in:
tmerritt committed 2026-10-15 18:50:41 -04:00
1 parent 25423d3828
commit a29b03b00b
135 files changed
+26562 -420

No files matched your search

+467
View File
@@ -0,0 +1,467 @@
# 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 `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_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:
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_prg.bin` that is **byte-identical** to `prg.bin` and to the PRG bank of `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.