13 KiB
Balloon Fight (JU) — Game Flow from Power-On
This document describes the execution flow of the disassembled Balloon Fight (JU) 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. Reset Vector — CPU $FFFC → $C000
Reset ($C000–$C07E)
On power-on or reset, the 6502 jumps to $C000. The reset routine performs cold-start hardware initialization:
| Step | Code | Purpose |
|---|---|---|
| 1 | lda #$00 / sta PPUCTRL |
Disable NMI and PPU rendering |
| 2 | lda #$00 / sta PPUMASK |
Disable screen output |
| 3–6 | Poll $2002 twice |
Wait for two full VBlank periods (PPU warm-up) |
| 7 | sei / cld |
Disable interrupts, clear decimal mode |
| 8 | ldx #$FF / txs |
Initialize stack pointer |
| 9–13 | Clear Zero Page | Zero all of RAM $00–$FF |
| 14–21 | Warm-start signature check | Compare RAM $07FA–$07FC against "HAL" ($C082–$C084). If valid, skip full memory clear |
| 22–31 | Initialize memory | Clear zero page + stack page ($0700–$07FF), set $15 = $32, run block-init loop (jsr $D6DE 50 times), jsr $C579 |
| 32–44 | Copy initialization data | Copy 15 bytes from $C085 to $0629 (initial game state table) |
| 45–51 | Clear local variables | Zero $03–$06 |
| 52–56 | Copy saved signature | Re-store "HAL" to $07FA–$07FC |
| 57 | jmp MainInitialization |
Transfer control to $F1D4 |
Warm-start signature: "HAL" stored at $C082; RAM copy at $07FA. This lets the game resume state after a soft reset without re-initializing everything.
2. Main Initialization
MainInitialization is at $F1D4 (forward-referenced in locals.inc). It sets up the first round, initializes object pools, and enters the main loop. Its body lives in the upper bank region (not fully shown in the decoded $C000–$C07E range).
3. Interrupt Vectors — CPU $FFFA–$FFFF
Located at the end of the bank (PRG offset $3FFA):
| Vector | Address | Handler |
|---|---|---|
| NMI | $C094 | NmiHandler |
| RESET | $C000 | Reset |
| IRQ | $C0F7 | IrqHandler (self-loop jmp $C0F7 — unused) |
4. NMI / Frame Heartbeat — NmiHandler ($C094)
The NMI fires once per frame and drives the frame pacing. It:
- Preserves registers (A, X, Y on stack)
- OAM DMA: writes
$00to$2003, then$02to$4014(DMA from $0200–$02FF) - PPU command buffer flush: compares read/write offsets at $52/$53; if data pending, calls
FlushPpuCommandPreserved($C17C) - Transfer sources:
jsr UpdatePpuTransferSources($D60D) - Build OAM:
jsr BuildFrameOam($D798) - Frame counter:
inc FrameCounter($19) - Scroll/rendering setup: sets $2006/$2005 scroll registers, conditionally waits for VBlank if rendering disabled, writes $2000
- Audio:
jsr UpdateAudioFrame($FFF7 bank call →Sound_F758) - Sets
NmiCompletedFlag($02 = $01) - Restores registers,
rti
The main game loop runs during VBlank and waits for this flag.
5. Main Game Loop — $C209
After initialization, control reaches the per-frame loop at $C209:
UpdateGameplayObjects ($E691) ; update all 20 gameplay-object slots
UpdateSpecialObjectSystem ($C6F9) ; every other frame (frame counter / 2)
[round spawner / HUD / spawning logic]
[object physics & collision]
[particle system]
[OAM sorting / sprite setup]
WaitForFrame ($F465) ; wait for NMI-completed flag
jmp back to $C209
Key flow details:
- Scroll handling ($C227): if
PpuScrollX($17) is non-zero, togglesPpuControlBasebit 0 (screen scroll page flip), decrements scroll X. - Carrier/rescue sequence ($C233): updates carrier position ($0488), target player, sound requests.
- Effect object pool (8 slots, $055D–$0584): updates timers, respawns offscreen.
- Gameplay object pool (20 slots, $0530–$055C): movement, position integration.
- Round spawner ($C790): on frame boundaries, counts down
RoundSpawnDelay, finds free slots, spawns round objects from tables ($C88F–$C8B5), plays spawn sound. - HUD updates ($C831): when spawn delay reaches 1, writes round HUD digits to PPU buffer.
- Sprite sorting:
SortOrInitializeRecords($C579) maintains a sorted table of up to 50 five-byte records ($0700–$07F9) for OAM priority.
6. Subsystems
6.1 PPU Command Buffer ($C12D–$C1C4)
A buffered PPU update system. Commands are staged at $0300 with format:
- Byte 0: PPU address high | $20
- Byte 1: PPU address low
- Byte 2: length (number of data bytes following)
Routines:
AppendBytesToPpuBuffer($C131) — appends raw bytesAppendLengthPrefixedPpuData($C139) — appends with length prefixBuildPpuCommand($C154, formerlySub_C154) — formats address+length commandFlushPpuCommandPreserved($C17C, formerlySub_C17C) — drains buffer to $2006/$2007, preserving X/Y- Special handling for palette writes ($3Fxx range)
6.2 Round Spawning ($C77A–$C88A)
ChooseRoundSpawnIndex($C77A): picks a random spawn index belowRoundSpawnRandomLimit, rejecting zeroUpdateRoundSpawner($C790): timer-driven spawning; initializes object position, velocity (from tables at $C89F–$C8B1), flip flags, aux state; incrementsSpawnSelector; requests spawn sound ($F0 bit $04)UpdateRoundHud/ConfigureRoundPresentation($C831–$C88A): writes round number / HUD tiles to PPU buffer
Spawn tables ($C88F–$C8B5): flip X/Y, X/Y offsets, parameter, velocity Y, position hi, velocity X — six entry types.
6.3 Gameplay Objects ($C8B7–$C9DC)
UpdateRoundObjectSpritesAndPhysics ($C8B7) updates slots 1 and 0:
- Builds 3-sprite OAM per object from tile tables ($C9DD–$CA33)
- Integrates fixed-point position ($C9B6)
- Bounds testing (
TestGameplayObjectAgainstBounds, $CA67) against rectangular regions - Velocity reflection on collision (
BounceGameplayObjectX/Y, $CA4F/$CA37) - Object removal when offscreen
6.4 Bonus Objects ($CB74–$CCCA)
UpdateBonusObjectSystem($CB74): per-frame phase/timer advance for the single active bonus slotTestBonusObjectPlayerOverlap($CBB2): rectangle overlap vs both players; applies horizontal/vertical impulses, collision soundTestBonusObjectCollection($CC3A): phase-3 collection; starts bonus timerTriggerBonusObjectEffect($CCBF): increments effect counter, callsApplyBonusEffect($E983)DrawBonusObjectHud($CCCB): 3-row HUD tile display from phase tables ($CD26–$D046)
6.5 Carrier / Rescue Sequence ($C5C3–$C715)
UpdateCarrierSequence($C6F9): advances carrier target X while idle; finds eligible player; starts sequenceFindEligiblePlayerForCarrier($C614): scans player slots 7→0, finds one at lower screen boundary ($B4+) matching carrier target XBeginCarrierTargetSequence($C6C4): captures player position/state, resets timerUpdateCarrierArrivalSequence($C658): moves carrier down, checks contact, transitions player to hit/rescue state, sound requestUpdateCarrierObjectDisplay($C5C3): builds 8-sprite carrier metasprite, updates target player position relative to carrier- Tables:
NormalDisplayModeTable($C5B7),AlternateDisplayModeTable($C5BB),CarrierAnimationSequenceTable($C6B8)
6.6 Particle System ($CD4A–$CEAD)
InitializeParticleSlots($CD4A): 10 slots, offscreen Y ($F0)SpawnTimedParticle($CD5A): random timer, finds free slot, assigns position/velocity fromParticleInitialVelocityTable($CEAE)UpdateParticleSystem($CDAA): integrates signed motion, applies scroll delta, retires offscreen particlesDrawParticleSprites($CE2F): 3-sprite OAM per particle with animation/attribute masking
6.7 Sound Driver ($FFF7 region, bank-switched)
UpdateAudioFramejumps toSound_F758in another bank- Sound sequence pointer table at $3BCA (32 entries)
- Encoded sequence data $3BCA–$3FF1 (note/duration bytes with $02/$FF/$8x control codes)
- APU channel init at $F7xx region (
InitializeApuChannelState,SilencePulseChannels) - Sound request flags: $F0 (flags 0), $F1 (flags 1), $F2 (flags 2), $F3 (flags 3)
7. Memory Map (Key Zero Page / RAM)
| Address | Name | Use |
|---|---|---|
| $00 | PpuControlShadow |
$2000 mirror |
| $01 | PpuMaskShadow |
$2001 mirror |
| $02 | NmiCompletedFlag |
VBlank sync flag |
| $12–$13 | RecordIndex / ReverseRecordIndex |
Sorted-record insertion |
| $14–$15 | SpawnParameter / SpawnPixelPosition |
Spawn args |
| $16 | RenderingDisabledFlag |
Rendering state |
| $17 | PpuScrollX |
Scroll X |
| $18 | PpuControlBase |
Base PPU control |
| $19 | FrameCounter |
Frame tick |
| $1B | RandomState |
PRNG state |
| $1D–$1E | RecordPtrLo/Hi |
Record table pointer |
| $23–$24 | SpawnCommandPointerLo/Hi |
Command stream pointer |
| $25–$26 | DispatchPointerLo/Hi |
Spawn dispatch indirect jump |
| $52–$53 | Ppu buffer read/write offsets | Command buffer |
| $54–$56 | Ppu command address/length | Pending PPU command |
| $7F | PlayerLifecycleState |
Per-player lifecycle |
| $88 | PlayerSlotState |
Per-player active state |
| $91 | PlayerPosX / Player0PosX |
Player X |
| $99 | CarrierTargetX |
Carrier target |
| $9A | PlayerPosY / Player0PosY |
Player Y |
| $A2 | CarrierPosY |
Carrier Y |
| $BD | PlayerCollisionDisable |
Collision immunity |
| $F0–$F3 | Sound request flags 0–3 | Audio requests |
| $0300–$03FF | PPU command buffer | Staged PPU updates |
| $0489–$048D | Carrier sequence state | Active, mode, target, drop, timer |
| $0490–$0530 | Gameplay object pool (20 slots) | State, pos, velocity |
| $055D–$0584 | Effect object pool (8 slots) | State, pos |
| $0558–$05CE | Particle / sequence state | 10-slot particle pool |
| $05D1–$060E | Bonus object state | Timer, phase, pos, HUD |
| $0700–$07F9 | Sorted record table | 50 × 5-byte OAM sort records |
| $07FA–$07FC | Warm-start signature | "HAL" |
8. Object Pools
| Pool | Slots | State array | Purpose |
|---|---|---|---|
| Gameplay objects | 20 ($00–$13) | $0530 | Round enemies/objects, movement & physics |
| Effect objects | 8 ($00–$07) | $055D | Short-lived spawn effects |
| Particles | 10 ($00–$09) | $055D | Visual particles (shared state array, different offset interpretation) |
Slot state convention: negative ($FF) = inactive/free, zero or positive = active.
9. Summary of Execution Flow
Power On / Reset
│
▼
Reset ($C000) ────► hardware init, clear RAM, validate "HAL" signature
│
▼
jmp MainInitialization ($F1D4)
│
▼
[first-round setup, object pool init]
│
▼
Main Loop ($C209) ──────────────────────────────────────────────┐
│ │
├─ UpdateGameplayObjects ($E691) │
├─ UpdateSpecialObjectSystem ($C6F9) [alt frames] │
├─ Carrier sequence ($C6F9 family) │
├─ Round spawner ($C790) + HUD ($C831) │
├─ Object physics & collision ($C8B7, $CA67) │
├─ Bonus object system ($CB74) │
├─ Particle system ($CDAA) │
├─ OAM sort ($C579) │
├─ WaitForFrame ($F465) ◄──────── waits for NMI flag ──────┘
│
▼ (loop back to $C209)
NMI ($C094) [once per frame]
├─ OAM DMA ($4014)
├─ Flush PPU command buffer ($C17C)
├─ Update transfer sources ($D60D)
├─ Build frame OAM ($D798)
├─ Frame counter++, scroll setup
├─ UpdateAudioFrame ($FFF7 → Sound_F758)
└─ Set NmiCompletedFlag, rti
10. Notes on Provisional Areas
Some routines in the disassembly still carry placeholder names (Label_Cxxxx, Sound_FBxx) and some data blocks were previously marked "undecoded." These have been cleaned up in this review:
Sub_C154→BuildPpuCommandSub_C17C→FlushPpuCommandPreservedSub_C40C→SpawnRandomObjectSpawnCommandStreamData_C4AD→RoundSpawnCommandStream(format documented)
The 497 Label_ branch targets are local intra-routine labels; none are subroutine entry points (confirmed against all jsr/jmp targets in the source). The 77 Sound_FBxx labels are sound-driver entry points in the banked audio region.
Build verification: make in copilot/ produces reconstructed_prg.bin that is byte-identical to prg.bin and to the PRG bank of balloon.nes.