# Bomberman (U) — Game Flow from Power-On This document describes the execution flow of the disassembled Bomberman (U) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `copilot/prg.s` (with `header.s` and `chr.s` for the iNES header and CHR data) in the repository. --- ## 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 files | `copilot/prg.s` (7,553 lines, ~493 KB), `header.s`, `chr.s` | | Output | `reconstructed.nes` (24,592 bytes) | ### Interrupt vectors (CPU $FFFA–$FFFF) | Vector | Address | Handler | |--------|---------|---------| | NMI | $C01A | `NmiHandler` | | RESET | $C000 | `Reset` | | IRQ / BRK | $FFF9 | `IrqHandler` (just `rti` — unused) | --- ## 2. Build Status `make` in `copilot/` assembles and links successfully, producing `reconstructed.nes` (24,592 bytes). **Verification result:** The reconstructed ROM is **byte-identical** to `bomberman.nes` — headers, CHR, and all 16,384 PRG bytes match exactly. ### Naming fixes applied | Change | Location | Reason | |--------|----------|--------| | `RandomState0` kept at `$54`; added `RandomStateAlt = $E4` | `locals.inc` | Zero-page reuse: `$54` is the random-state byte used by most code; `$E4` is a distinct byte decremented at CPU `$E71B` | | `dec RandomState0` → `dec RandomStateAlt` | `prg.s` (was `main.s:6972`) | The original ROM has `dec $E4` at CPU `$E71B`, not `dec $54` | | `ControllerState` → `ControllerState1` | `prg.s` (16 refs) | Clarifies this is controller port 1; added `ControllerState2 = $13` for port 2 | | `Main_HandleControl_C368` → `MainGameLoop` | `prg.s` (3 refs) | Main per-frame game loop entry point | | `Gameplay_HandleControl_C79D` → `GameplayUpdate` | `prg.s` (4 refs) | Gameplay object dispatcher/update | | `Sound_HandleControl_E65F` → `SoundUpdate` | `prg.s` (3 refs) | Sound engine update called from NMI | ### Pass 3 symbol application (2026-10-19) | Pass | Substitutions | Detail | |------|---------------|--------| | locals.inc (single-name) | 354 operands | All zero-page addresses with exactly one name in `locals.inc` | | locals.inc (multi-alias, site-resolved) | 8 operands | `$E0/$E1`: `IndirectPtrLo/Hi` in the RAM-clear loop (`InitializeSystemAndRunMainLoop` → `Main_WriteSoundDispatchIndex_C317`); `SoundDelay0` (indexed, `$E1,x`) in sound engine (`Gameplay_ReadSoundDelay0_E57B`, `InitializeApu`, `Sound_ReadSoundDelay0_E661`) | | symbols.inc (hardware) | 0 operands | `prg.s` already contains no numeric hardware operands ($2000–$4017) | | In-file data-table labels | 24 operands | 24 ROM-space table reads (`lda $C2E7,x` → `lda SystemPaletteData,x`, etc.) substituted via `reconstructed.lbl` | **Residue:** 758 numeric operands remain, addressing bytes not named in either `.inc` file: 505 zero-page (133 distinct addresses), 171 RAM $0100–$07FF (46 addresses), and 82 ROM table reads (54 addresses, 34 of which are interior bytes of labelled tables). --- ## 3. Reset — CPU $FFFC → $C000 **`Reset`** ($C000) On power-on or reset: 1. `sei` — disable maskable interrupts. 2. `lda #$00 / sta PPUMASK / sta PPUCTRL` — disable PPU rendering and NMI. 3. `cld` — clear decimal mode. 4. `ldx #$FF / txs` — initialize stack pointer to $FF. 5. **Wait for two startup VBlanks** — `WaitForFirstStartupVBlank` ($C00D) and `WaitForSecondStartupVBlank` ($C012) each poll `PPUSTATUS` ($2002) for the VBlank bit. 6. `jmp InitializeSystemAndRunMainLoop` ($C30C) — transfer control to the system initializer. --- ## 4. System Initialization — `InitializeSystemAndRunMainLoop` ($C30C) This is the main entry point after reset: 1. **Clear RAM** — writes $00 to zero page via `Main_WriteZp00_C333` ($C333) and to $0100–$07FF via an indirect pointer `IndirectPtrLo`/`IndirectPtrHi` ($E0/$E1) in a 7-page loop (`Main_WriteSoundDispatchIndex_C317` at $C317). 2. `jsr InitializeVideoSystem` ($C1F2) — disables rendering, clears shadow OAM, uploads the system palette, enables NMI. 3. `jsr InitializeApu` ($E642) — silences all APU channels, resets sound delay/state variables (`SoundDelay0`–`SoundDelay2` at $E1–$E3). 4. Writes zero to sound state variables ($E0–$E3). 5. Sets stack pointer to $FF, enables APU ($4015 = $0F). 6. `jsr ComposeStatusScreen` ($DE83) — draws the initial status/title screen. 7. `jsr EnableNmiInPpuControl` ($C2B0) — enables NMI via PPUCTRL shadow. 8. Sets `ScreenState` ($72) = $01. 9. Enters the main loop at `MainGameLoop` ($C368). --- ## 5. NMI / Frame Heartbeat — `NmiHandler` ($C01A) The NMI fires once per frame and is the central real-time driver: 1. **Save registers** (A, X, Y on stack). 2. Set `FrameSyncFlag` ($0B) = $00. 3. Set OAM address to $00. 4. **Sprite DMA path** — if $60 is zero, write OAM entry $0700–$0703 (sprite #0 at Y=$F8) and trigger OAMDMA ($4014 = $07). 5. **VRAM transfer path** — if `VramReadOffset` ($14) ≠ `VramWriteLimit` ($15), transfer VRAM data from a table at $0600–$0607, indexing into pointer data at $D9C6–$D9C9. Handles both write and read packet types. 6. **Screen state / NMI work pending** — based on `ScreenState` ($72) and `NmiWorkPending` ($60), transfers various VRAM data blocks (title screen, gameplay screen, etc.) using `SetPpuAddress` ($C196). 7. **PPU status / audio** — reads PPUSTATUS, commits PPU state and scroll via `CommitPpuStateAndScroll` ($C296), increments `FrameCounter` ($33), handles audio channel timing at $69/$6A. 8. `jsr Ppu_WriteZp10_C19D` ($C19D) — reads and debounces both controller ports, stores results in `ControllerState1` ($12) and `ControllerState2` ($13). 9. `jsr Gameplay_ReadZpB2_E4C7` ($E4C7) — gameplay input handling. 10. `jsr SoundUpdate` ($E65F) — sound engine update. 11. **DMC control** — if $38 and `GameMode` ($59) are zero, starts DMC playback ($4010 = $0E, DMC address $C000, length $FF, enable channels). 12. **PPU scroll wait** — if $60 is set, waits for VBlank (polls PPUSTATUS bit 6), then applies scroll from $0E/$0F. 13. Toggles `FrameParity` ($DB) with `EOR #$05` (frame parity / flip-flop flag). 14. **Restore registers, `rti`.** ### Controller reading (`Ppu_ReadControllerAndReturnA_C1BF` / $C1BF) - Strobes `JOY1` ($4016) with $01 then $00. - Reads 8 bits per controller, shifting into X. - `Ppu_HandleControl_C1EB` masks to the low 2 bits ($03) and increments X if non-zero (button press detection). - Port 1 result → `ControllerState1` ($12); port 2 result → $13 (`ControllerState2`). --- ## 6. Main Loop The main loop starts at **`MainGameLoop`** ($C368): ``` MainGameLoop ($C368): wait until ControllerState1 ($12) is nonzero (button pressed) └─ branch on SELECT ($20) to toggle $5F └─ decrement sub-delay $71 / main delay $70 └─ when delays expire, increment $59 (game mode / screen state) ``` Based on `$59` and `$5F`, the loop dispatches to: - `RunMenuSelectionScreen` ($DA8E) — title/menu screen - Gameplay initialization paths ($C3B6–$C5FF) — sets up object positions, stage, sound - `Main_WriteStageCounter_C613` ($C613) — stage counter display The core per-frame gameplay dispatch uses a **state table** at `MainStateDispatchTable` ($C75D–$C79C), which maps state indices to handler sub-indices ($00–$27). Gameplay object processing walks arrays at $03E6 (state), $0436, $0486 (X/Y position), $04D6, $0526 (timer/flags), with up to 80 ($4F) object slots. Key main-loop subroutines: - `Main_ReadGameMode_C60B` ($C60B) — waits for `FrameDelay` ($B5) to reach zero. - `Main_WriteStageCounter_C613` ($C613) — updates stage counter, handles START button to begin gameplay. - `GameplayUpdate` ($C79D) — main gameplay object dispatcher (80 slots). - `Gameplay_Dispatch_C7A4` / `Gameplay_Dispatch_C835` — object state dispatch. - `Stage_WriteObjectWorkX_D8FC` ($D8FC) — object work initialization. --- ## 7. Gameplay Systems ### Object management - Up to 80 object slots ($00–$4F), tracked by state at $03E6,X. - Position arrays: $0436,X / $0486,X (X/Y), $04D6,X (state/flags), $0526,X (timer/direction). - `Stage_WriteObjectWorkX_D8FC` ($D8FC) initializes object work fields. - Objects are processed in a loop with `dex`/index retreat patterns ($C756, $C8A6). ### Bomb / explosion / player mechanics Referenced throughout the gameplay code: - `Gameplay_HandleControl_C96A`, `Gameplay_HandleControl_CA1B`, `GameplayUpdate` — player/bomb input and state. - `Object_WriteStageProgress_CFB0` ($CFB0) — stage progress tracking. - `Frame_ReadObjectIndex_CE5B` ($CE5B), `Frame_ReadObjectX_CDD4` ($CDD4) — frame-based object reading. ### Stage system - `Stage_ReadZp58_D69A` ($D69A) — stage data lookup. - Stage counter display via `Main_WriteStageCounter_C613`. --- ## 8. Audio / Sound Engine ### `InitializeApu` ($E642) - Clears DMC direct register ($4011), sound delays `SoundDelay0`–`SoundDelay2` ($E1–$E3). - Silences pulse 1, pulse 2, triangle, noise channel controls ($4000, $4004, $4008, $400C). - Enables APU channels ($4015 = $0F). ### `SoundUpdate` ($E65F) - Loops over 3 sound channels (X = 2 down to 0). - Decrements per-channel delay at `SoundDelay0,x` ($E1,X). - When delay expires, reads sound command state from `SoundCommandState` ($DF): - If $DF < 0 → read sound RAM pointer ($E6BC table). - If $DF == 3 → write sound command state. - Otherwise → return or handle based on $E0 (indirect pointer). - Calls into sound dispatch via `SoundDispatchAddressTable` ($E6AD–$E6C8). ### Sound dispatch table (`SoundDispatchAddressTable`, $E6AD) A jump table of 14 sound handler addresses (e.g., $E641, $E6C8, $E6CC, $E6DE, $E727, $E766, $E792, $E7AB, etc.), each handling a specific sound effect or music channel. ### Channel-specific sound paths - `Sound_UpdateAudioChannelPath_E6CF` ($E6CF) — noise channel ($400E–$400F). - `Sound_UpdateAudioChannelPath_E6DF` ($E6DF) — pulse 2 channel: sets sweep ($4005 = $84), timer lo/hi ($4006/$4007), noise period/length. - `Sound_UpdateAudioChannelPath_E70B` ($E70B) — decrements `RandomState0`, reconfigures pulse 2 when expired. ### Sound state (zero page) | Address | Symbol | Use | |---------|--------|-----| | $DF | `SoundCommandState` | Sound command / state | | $E0 | `IndirectPtrLo` / `SoundDispatchIndex` | Indirect pointer low; sound dispatch index | | $E1–$E3 | `SoundDelay0`–`SoundDelay2` | Per-channel sound delays | --- ## 9. PPU / Rendering ### `InitializeVideoSystem` ($C1F2) - `jsr DisableRendering` ($C257) — wait for VBlank, turn off PPUMASK. - `jsr Ppu_HandleControl_C2B9` ($C2B9) — apply PPUCTRL shadow with NMI disabled. - Set `PpuCtrlShadow` ($0C) = $10, clear $0E/$0F/$14/$15. - `jsr HideAllSprites` ($C268) — fills $0700–$07FF with $F8 (offscreen) in 32-byte strides. - `jsr UploadSystemPalette` ($C2C3) — uploads 32 bytes of palette data from `SystemPaletteData` ($C2E7) to $3F00–$3F1F. - `jsr EnableNmiInPpuControl` ($C2B0) — enable NMI. ### `CommitPpuStateAndScroll` ($C296) - Writes `PpuMaskShadow` ($0D) to PPUMASK ($2001). - Sets PPU address to $0000, writes $00 to $2005 twice (scroll). - Writes `PpuCtrlShadow` ($0C) to PPUCTRL ($2000). ### Palette data (`SystemPaletteData`, $C2E7) 32 bytes of NES palette colors starting at $3F00. --- ## 10. Zero-Page / RAM Symbols The symbol table lives in `copilot/locals.inc` (hardware constants are in `copilot/symbols.inc`). Every address it names is used by name in `prg.s`: | Address | Symbol | Use | |---------|--------|-----| | $0B | `FrameSyncFlag` | Main-thread/NMI frame handoff | | $0C | `PpuCtrlShadow` | Software copy of PPUCTRL | | $0D | `PpuMaskShadow` | Software copy of PPUMASK | | $12 | `ControllerState1` | Controller port 1 sample | | $13 | `ControllerState2` | Controller port 2 sample | | $14 | `VramReadOffset` | NMI VRAM-transfer index into the $0600 table | | $15 | `VramWriteLimit` | Upper limit compared against $14 | | $16 | `NmiPathSelector` | $60=0 → sprite DMA path; $60≠0 → VRAM data path | | $1F | `ObjectScreenX` | Object coordinate temporaries used by object processing | | $20 | `ObjectScreenY` | | | $28 | `ObjectSpawnParam` | Object spawn parameters | | $29 | `ObjectSpawnY` | | | $2A | `ObjectSpawnX` | | | $2B | `ObjectSpawnWidth` | | | $2C | `ObjectSpawnHeight` | | | $33 | `FrameCounter` | Frame counter | | $54–$57 | `RandomState0`–`RandomState3` | Four-byte PRNG state used by `AdvanceRandomState` | | $59 | `GameMode` | Game mode / screen selector | | $5C | `ObjectStateFlags` | Object state trio | | $5D | `ObjectStateTimer` | | | $5E | `ObjectStateExtra` | | | $60 | `NmiWorkPending` | Nonzero → the NMI performs VRAM transfers | | $70 | `MainDelay` | Main-loop delay counter | | $71 | `SubDelay` | Secondary delay counter | | $72 | `ScreenState` | Screen state flag | | $73 | `ObjectXLimit` | Object X limit | | $93 | `StageCounter` | Stage counter | | $9C–$9F | `ObjectWork0`–`ObjectWork3` | Object work fields | | $A4 | `BonusTimer` | Bonus timer | | $AC–$AF | `AudioPtrLo/Hi`, `AudioArgLo/Hi` | Audio data / argument pointers | | $B0 | `GameModeCounter` | Game-mode counter | | $B1 | `ObjectType` | Object type | | $B5 | `FrameDelay` | Frame delay | | $DB | `FrameParity` | Frame-parity toggle, flipped by the NMI each frame | | $DF | `SoundCommandState` | Sound command state | | $E0 | `IndirectPtrLo` / `SoundDispatchIndex` | Indirect pointer for the RAM-clear loop; sound dispatch index in the sound engine | | $E1 | `IndirectPtrHi` / `SoundDelay0` | Indirect pointer high byte; per-channel delay base (indexed by X) | | $E2–$E3 | `SoundDelay1` / `SoundDelay2` | Remaining per-channel delays | | $E4 | `RandomStateAlt` | Distinct byte decremented at CPU $E71B (zero-page reuse) | **Aliases.** Two addresses carry two names each; `prg.s` uses the one that matches the calling routine. `$E0`/`$E1` are `IndirectPtrLo/Hi` in the RAM-clear loop (`InitializeSystemAndRunMainLoop` / `Main_WriteSoundDispatchIndex_C317`) and in the sound engine's pointer work, but `$E1` is `SoundDelay0` wherever the sound engine indexes the per-channel delay table (`SoundDelay0,x`, i.e. `$E1`–`$E3`). **Unnamed residue.** Most zero-page/RAM bytes the game touches have no entry in `locals.inc` and stay numeric — 758 operands address 179 distinct addresses outside the table above (e.g. the `$34`/`$35` indirect pointer used by gameplay data writes). Naming them is Pass-3 semantic work, not a substitution. --- ## 11. Data Tables | Label | Address | Description | |-------|---------|-------------| | `InesHeader` | $000000–$00000F (header segment) | iNES header (in `header.s`) | | `SystemPaletteData` | $C2E7–$C306 | 32-byte palette for $3F00 | | `MainStateDispatchTable` | $C75D–$C79C | 64-byte gameplay state dispatch table | | `SoundDispatchAddressTable` | $E6AD–$E6C8 | 14-entry sound handler jump table | | `PatternTableGraphics` | $004010+ (CHR segment) | CHR pattern table data (in `chr.s`) | --- ## 12. Summary of Execution Flow ``` Power On / Reset │ ▼ Reset ($C000) ───────────────────────────────────────┐ │ sei, disable PPU, cld, stack=$FF │ │ wait 2× VBlank │ ▼ │ jmp InitializeSystemAndRunMainLoop ($C30C) │ │ │ ├─ clear RAM ($0000–$07FF) │ ├─ InitializeVideoSystem ($C1F2) │ │ ├─ DisableRendering │ │ ├─ HideAllSprites │ │ ├─ UploadSystemPalette ($3F00–$3F1F) │ │ └─ EnableNmiInPpuControl │ ├─ InitializeApu ($E642) — silence all channels │ ├─ ComposeStatusScreen ($DE83) │ ├─ EnableNmiInPpuControl │ └─ MainGameLoop ($C368) ◄─────────────┘ │ MainGameLoop ($C368) ─────────────────────────────────┤ │ │ ├─ wait for controller button press │ ├─ handle SELECT / delay counters ($70,$71) │ ├─ increment game mode ($59) │ ├─ RunMenuSelectionScreen ($DA8E) [title] │ ├─ gameplay init ($C3B6–$C5FF) │ ├─ GameplayUpdate ($C79D) │ │ │ 80 object slots ($03E6 state array) │ │ ├─ object position ($0436/$0486) │ │ ├─ object timer ($0526) │ │ └─ object dispatch via StateDispatchTable │ └─ eventually → back to MainGameLoop │ │ NMI ($C01A) [every frame] │ │ │ ├─ OAM setup + DMA ($4014) │ ├─ VRAM transfer from $0600 table │ ├─ screen state transfer ($D9C6–$D9C9 pointers) │ ├─ controller read & debounce ($C1BF) │ ├─ Gameplay_ReadZpB2_E4C7 ($E4C7) │ ├─ SoundUpdate ($E65F) │ ├─ DMC start ($4010–$4013) │ ├─ PPU state & scroll commit ($C296) │ ├─ frame parity toggle ($DB) │ └─ rti │ │ Sound Engine ($E65F) [from NMI] │ ├─ per-channel delay countdown ($E1–$E3) │ ├─ SoundDispatchAddressTable ($E6AD) │ ├─ pulse 2, noise, DMC channel control │ └─ RandomStateAlt ($E4) decrement │ ``` --- ## 13. Notes - **Build is byte-identical:** The apparent one-byte difference (`dec $E4` vs `dec $54` at CPU $E71B) was resolved by recognizing zero-page reuse — `$54` is `RandomState0` used by most code, while `$E4` is a distinct byte (`RandomStateAlt`) decremented at that one location. Both are now separately named in `locals.inc`. - **Source annotation:** `prg.s` uses the canonical `; CPU:$C123 PRG:0123 AD 21 03` audit comment format on every instruction, enabling direct comparison with the binary. - **Code organization:** Code is organized as reachable routines traced from the NMI/RESET/IRQ control-flow graph; unreachable bytes are classified as explicit data regions. - **Legacy file:** `copilot/main.s` (8,070 lines) is a pre-standardization duplicate kept for reference; the build uses `prg.s` + `header.s` + `chr.s`. - **Vectors confirm** NMI at $C01A and RESET at $C000, matching the reset handler location. - **Known misnomer:** `Main_WriteSoundDispatchIndex_C317` ($C317) is actually the RAM-clear loop (writes $00 to $0100–$07FF via `IndirectPtrLo/Hi`), not a sound-dispatch write. The name reflects a prior pass's assumption; the operands now correctly use `IndirectPtrLo/Hi`. Renaming the label is a follow-up task.