# 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/main.s` 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 (per header byte $01) | | PRG bank start | CPU $C000 | | Source file | `copilot/main.s` (8,070 lines, ~614 KB) | | Output | `reconstructed_bomberman.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_bomberman.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` | `main.s:6972` | The original ROM has `dec $E4` at CPU `$E71B`, not `dec $54` | | `ControllerState` → `ControllerState1` | `main.s` (16 refs) | Clarifies this is controller port 1; added `ControllerState2 = $13` for port 2 | | `Main_HandleControl_C368` → `MainGameLoop` | `main.s` (3 refs) | Main per-frame game loop entry point | | `Gameplay_HandleControl_C79D` → `GameplayUpdate` | `main.s` (4 refs) | Gameplay object dispatcher/update | | `Sound_HandleControl_E65F` → `SoundUpdate` | `main.s` (3 refs) | Sound engine update called from NMI | --- ## 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 and a range of RAM via an indirect pointer at $E0/$E1 (loops through $00–$FF pages). 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. 4. Sets up the sound dispatch index and 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 `GameplayActive`-style flag $72 = $01. 9. Enters the main loop at `Main_HandleControl_C368` ($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 $14 ≠ $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 $72 and $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 a frame counter at $33, handles audio channel timing at $69/$6A. 8. `jsr Ppu_WriteZp10_C19D` ($C19D) — reads and debounces both controller ports, stores results in `ControllerState` ($12) and $13. 9. `jsr Gameplay_ReadZpB2_E4C7` ($E4C7) — gameplay input handling. 10. `jsr Sound_HandleControl_E65F` ($E65F) — sound engine update. 11. **DMC control** — if $38 and $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 $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 → `ControllerState` ($12); port 2 result → $13. --- ## 6. Main Loop The main loop starts at **`Main_HandleControl_C368`** ($C368): ``` Main_HandleControl_C368 ($C368): wait until ControllerState ($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 $B5 (frame delay) to reach zero. - `Main_WriteStageCounter_C613` ($C613) — updates stage counter, handles START button to begin gameplay. - `Gameplay_HandleControl_C79D` ($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`, `Gameplay_HandleControl_C79D` — 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 $E1–$E3. - Silences pulse 1, pulse 2, triangle, noise channel controls ($4000, $4004, $4008, $400C). - Enables APU channels ($4015 = $0F). ### `Sound_HandleControl_E65F` ($E65F) - Loops over 3 sound channels (X = 2 down to 0). - Decrements per-channel delay at $E1,X. - When delay expires, reads sound command state from $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 | SoundDispatchIndex / IndirectPtrLo | Sound dispatch pointer low | | $E1–$E3 | SoundDelay0–2 | 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 PPUCTRL shadow ($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 | Address | Symbol | Use | |---------|--------|-----| | $0B | FrameSyncFlag | NMI frame sync | | $0C–$0D | PpuCtrlShadow / PpuMaskShadow | PPU register shadows | | $12 | ControllerState1 | Controller port 1 state | | $13 | ControllerState2 | Controller port 2 state | | $14–$15 | VramReadOffset / VramWriteLimit | NMI VRAM-transfer window (index + limit) | | $16 | NmiPathSelector | Selects NMI sub-path (sprite DMA vs VRAM data) | | $1F–$20 | (unnamed) | Object X/Y temp | | $28–$2C | (unnamed) | Object spawn parameters | | $29–$2B | (unnamed) | Object coordinates | | $33 | FrameCounter | Frame tick counter | | $34 | IndirectPtrHi | Pointer high (used by sound/object) | | $36 | (unnamed) | Object X temp | | $38 | (unnamed) | DMC / audio flag | | $39 | (unnamed) | Stage counter flag | | $3C–$3F | (unnamed) | Delay counters | | $54–$57 | RandomState0–3 | PRNG state (4-byte RNG) | | $59 | GameMode | Game mode / screen state | | $5A | (unnamed) | Stage / level | | $5B | (unnamed) | Object X bound | | $5C–$5E | (unnamed) | Object state flags | | $5F | (unnamed) | Menu toggle | | $60 | NmiWorkPending | NMI VRAM-transfer work pending flag | | $61–$6A | (unnamed) | Object init / temp | | $68–$70 | MainDelay / SubDelay | Main loop delay counters | | $72 | ScreenState | Screen state flag | | $73 | ObjectXLimit | Object X limit | | $76–$7A | (unnamed) | Object position/state | | $7D–$7E | (unnamed) | Bomb / object state | | $78–$7C | (unnamed) | Player / bomb coords | | $79–$7A | (unnamed) | Player position | | $93 | StageCounter | Stage counter | | $94 | (unnamed) | Audio / object | | $9C–$9F | ObjectWork0–3 | Object work fields | | $A0–$A7 | (unnamed) | Object work / bonus | | $A4 | BonusTimer | Bonus timer | | $AC–$AE | (unnamed) | Audio data pointers | | $B0 | GameModeCounter | Game mode counter | | $B1 | ObjectType | Object type | | $B2 | (unnamed) | Audio flag | | $B5 | FrameDelay | Frame delay | | $DB | FrameParity | Frame parity toggle (NMI toggles each frame) | | $DF | SoundCommandState | Sound command state | | $E0–$E3 | SoundDispatchIndex / SoundDelay0–2 | Sound dispatch & delays | | $E4 | RandomStateAlt | Alternate random-state byte (dec'd at CPU $E71B; distinct from RandomState0 at $54) | --- ## 11. Data Tables | Label | Address | Description | |-------|---------|-------------| | `InesHeader` | $C000–$C00F | iNES header (embedded in source) | | `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` | $C010+ (CHR) | CHR pattern table data | --- ## 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`. - The source uses a "ROM $00XXXX" file-offset annotation scheme on every instruction, enabling direct comparison with the binary. - Code is organized as reachable routines traced from the NMI/RESET/IRQ control-flow graph; unreachable bytes are classified as explicit data regions. - The disassembly is a single file (`main.s`) with embedded iNES header and CHR data segments. - Vectors confirm NMI at $C01A and RESET at $C000, matching the reset handler location.