16 KiB
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:
sei— disable maskable interrupts.lda #$00 / sta PPUMASK / sta PPUCTRL— disable PPU rendering and NMI.cld— clear decimal mode.ldx #$FF / txs— initialize stack pointer to $FF.- Wait for two startup VBlanks —
WaitForFirstStartupVBlank($C00D) andWaitForSecondStartupVBlank($C012) each pollPPUSTATUS($2002) for the VBlank bit. jmp InitializeSystemAndRunMainLoop($C30C) — transfer control to the system initializer.
4. System Initialization — InitializeSystemAndRunMainLoop ($C30C)
This is the main entry point after reset:
- Clear RAM — writes $00 to zero page and a range of RAM via an indirect pointer at $E0/$E1 (loops through $00–$FF pages).
jsr InitializeVideoSystem($C1F2) — disables rendering, clears shadow OAM, uploads the system palette, enables NMI.jsr InitializeApu($E642) — silences all APU channels, resets sound delay/state variables.- Sets up the sound dispatch index and writes zero to sound state variables ($E0–$E3).
- Sets stack pointer to $FF, enables APU ($4015 = $0F).
jsr ComposeStatusScreen($DE83) — draws the initial status/title screen.jsr EnableNmiInPpuControl($C2B0) — enables NMI via PPUCTRL shadow.- Sets
GameplayActive-style flag $72 = $01. - 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:
- Save registers (A, X, Y on stack).
- Set
FrameSyncFlag($0B) = $00. - Set OAM address to $00.
- Sprite DMA path — if $60 is zero, write OAM entry $0700–$0703 (sprite #0 at Y=$F8) and trigger OAMDMA ($4014 = $07).
- 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.
- Screen state / NMI work pending — based on $72 and $60, transfers various VRAM data blocks (title screen, gameplay screen, etc.) using
SetPpuAddress($C196). - 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. jsr Ppu_WriteZp10_C19D($C19D) — reads and debounces both controller ports, stores results inControllerState($12) and $13.jsr Gameplay_ReadZpB2_E4C7($E4C7) — gameplay input handling.jsr Sound_HandleControl_E65F($E65F) — sound engine update.- DMC control — if $38 and $59 are zero, starts DMC playback ($4010 = $0E, DMC address $C000, length $FF, enable channels).
- PPU scroll wait — if $60 is set, waits for VBlank (polls PPUSTATUS bit 6), then applies scroll from $0E/$0F.
- Toggles $DB with EOR #$05 (frame parity / flip-flop flag).
- 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_C1EBmasks 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) — decrementsRandomState0, 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 fromSystemPaletteData($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 $E4vsdec $54at CPU $E71B) was resolved by recognizing zero-page reuse —$54isRandomState0used by most code, while$E4is a distinct byte (RandomStateAlt) decremented at that one location. Both are now separately named inlocals.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.