# 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 via `SortedRecordTable`, x = 0), set `SpawnPixelPosition` = $32, run block-init loop (`jsr InitializeOrQueueObject` 50 times), `jsr SortOrInitializeRecords` | | 32–44 | Copy initialization data | Copy 15 bytes from $C085 to $0629 (initial game state table) | | 45–51 | Clear local variables | Zero the five-byte `CurrentRecord` at $03–$07 | | 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 IrqHandler` — unused) | --- ## 4. NMI / Frame Heartbeat — `NmiHandler` ($C094) The NMI fires once per frame and drives the frame pacing. It: 1. **Preserves registers** (A, X, Y on stack) 2. **OAM DMA**: writes `$00` to `OAMADDR`, then `$02` to `OAMDMA` (DMA from $0200–$02FF) 3. **PPU command buffer flush**: compares read/write offsets at $52/$53; if data pending, calls `FlushPpuCommandPreserved` ($C17C) 4. **Transfer sources**: `jsr UpdatePpuTransferSources` ($D60D) 5. **Build OAM**: `jsr BuildFrameOam` ($D798) 6. **Frame counter**: `inc FrameCounter` ($19) 7. **Scroll/rendering setup**: sets the `PPUADDR`/`PPUSCROLL` registers, conditionally waits for VBlank if rendering disabled (polling `PPUSTATUS`), writes `PPUCTRL` 8. **Audio**: `jsr UpdateAudioFrame` ($FFF7 bank call → `Sound_F758`) 9. **Sets `NmiCompletedFlag`** ($02 = $01) 10. **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, toggles `PpuControlBase` bit 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 bytes - `AppendLengthPrefixedPpuData` ($C139) — appends with length prefix - `BuildPpuCommand` ($C154, formerly `Sub_C154`) — formats address+length command - `FlushPpuCommandPreserved` ($C17C, formerly `Sub_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 below `RoundSpawnRandomLimit`, rejecting zero - `UpdateRoundSpawner` ($C790): timer-driven spawning; initializes object position, velocity (from tables at $C89F–$C8B1), flip flags, aux state; increments `SpawnSelector`; 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 slot - `TestBonusObjectPlayerOverlap` ($CBB2): rectangle overlap vs both players; applies horizontal/vertical impulses, collision sound - `TestBonusObjectCollection` ($CC3A): phase-3 collection; starts bonus timer - `TriggerBonusObjectEffect` ($CCBF): increments effect counter, calls `ApplyBonusEffect` ($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 sequence - `FindEligiblePlayerForCarrier` ($C614): scans player slots 7→0, finds one at lower screen boundary ($B4+) matching carrier target X - `BeginCarrierTargetSequence` ($C6C4): captures player position/state, resets timer - `UpdateCarrierArrivalSequence` ($C658): moves carrier down, checks contact, transitions player to hit/rescue state, sound request - `UpdateCarrierObjectDisplay` ($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 from `ParticleInitialVelocityTable` ($CEAE) - `UpdateParticleSystem` ($CDAA): integrates signed motion, applies scroll delta, retires offscreen particles - `DrawParticleSprites` ($CE2F): 3-sprite OAM per particle with animation/attribute masking ### 6.7 Sound Driver ($FFF7 region, bank-switched) - `UpdateAudioFrame` jumps to `Sound_F758` in 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 (see reuse note below) | | $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" | **Alias reuse.** Several zero-page addresses serve more than one subsystem, so `locals.inc` defines several names per address; `prg.s` uses whichever fits the routine reading it: | Address | Names used in `prg.s` | |---------|---------| | $12 | `RecordIndex` (sort), `Temp12` (generic scratch), `CollisionDeltaX`, `MotionDeltaLo` | | $13 | `ReverseRecordIndex` (sort), `CollisionDeltaY`, `MotionDeltaHi` | | $23–$26 | `SpawnCommandPointerLo/Hi` + `DispatchPointerLo/Hi`, `BoundsLeftPtrLo/Hi` + `BoundsRightPtrLo/Hi` (stage-collision bounds) | | $27–$2A | `BoundsTopPtrLo/Hi` + `BoundsBottomPtrLo/Hi` (stage-collision bounds) | | $3E | `SequencePlayerIndex` (bonus accumulation) / `CurrentObjectIndex` (object loops) | | $57–$5A | `PpuCommandData0/1/2` (PPU command buffer) / `HudCommandTile0`; `RoundHudCommandAddressHi/Lo` + `RoundHudCommandLength` + `RoundHudTile` in `DrawRoundStatusPanel` | | $55D/$567/$57B | `EffectObjectState` / `EffectObjectPosX` / `EffectObjectPosY` (spawn effects) vs `ParticleState` / `ParticlePosX` / `ParticlePosY` (particles) | --- ## 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. Naming and Residue Pass 2 (2026-10-19) replaced every address-derived definition with a semantic name — `Sub_Cxxx` for the routines that had no behavioural name yet, `_Block[_n]` for internal branch targets — so `prg.s` now contains **zero** `Label_` definitions. The 77 `Sound_Fxxx` entry points keep their addresses as names in the banked audio region. Pass 3 applies the two symbol tables to the instruction stream: - `symbols.inc` — the NES hardware constants (`PPUCTRL` … `APUFRAMECNT`). All **125** numeric register operands are now symbolic. - `locals.inc` — zero-page/RAM names plus forward-reference aliases for routine addresses (`AbsoluteDifference` = $F08E, `MainInitialization` = $F1D4, …). All but **16** numeric operands are now symbolic. Placeholder names renamed during the same work: - `Sub_C154` → **`BuildPpuCommand`** - `Sub_C17C` → **`FlushPpuCommandPreserved`** - `Sub_C40C` → **`SpawnRandomObject`** - `SpawnCommandStreamData_C4AD` → **`RoundSpawnCommandStream`** (format documented) **Residue.** `$13` carries three aliases (`ReverseRecordIndex`, `CollisionDeltaY`, `MotionDeltaHi`), none of which fits the **16** sites where it is used as an ordinary high-byte scratch beside `Temp12` — in `LoadStageObjectData`, `ReadPpuStreamByte`, `CreateBonusResultSprites`, `RenderObjectSprites_Load`, `ResolveObjectCollisionPair` and `SwapObjectIndexes`. Those operands are deliberately left numeric rather than given a speculative name; a neutral `$13` companion to `Temp12` would close them. Build verification: `make` in `copilot/` produces `reconstructed.nes`, compared whole-ROM against `rom.nes` (byte-identical).