Files
disasm/Bomberman (U)/OVERVIEW.md
T

16 KiB
Raw Blame History

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.