# Defender 2 (U) — Game Flow from Power-On This document describes the execution flow of the disassembled Defender 2 (U) 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 `defender2.nes`. --- ## 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 | | PRG bank start | CPU $C000 | | Source file | `copilot/prg.s` (11,905 lines) | | Output | `defender2_rebuilt.nes` (24,592 bytes) | ### Interrupt vectors (CPU $FFFA–$FFFF) | Vector | Address | Handler | |--------|---------|---------| | NMI | $CAA1 | `NmiHandler` | | RESET | $C983 | `ResetHandler` | | IRQ / BRK | $CA9E | `IrqHandler` (self-loop `jmp $CA9E` — unused) | --- ## 2. Build Status `make` in `copilot/` assembles `header.s`, `prg.s`, and `chr.s` with `ca65`, links with `ld65`, and verifies against `defender2.nes`. **Verification result: BYTE-IDENTICAL** — `defender2_rebuilt.nes` matches `defender2.nes` exactly (headers, CHR, and all PRG bytes). ### Build fix applied The original source referenced `LookupTableEntry_FAF5` at a `bpl` instruction (CPU $FAB4), but the symbol was undefined. The branch target CPU $FAF5 falls in the middle of what the disassembler classified as `lda #$16` (A9 at $FAF4, 16 at $FAF5). The fix: - Replaced the symbolic `bpl LookupTableEntry_FAF5` with `bne *-$1B` at CPU $E811, which produces the exact original bytes `d0 e3` (the disassembler had mislabeled `bne` as `bpl`). - The build now produces a byte-identical ROM. --- ## 3. Reset — CPU $FFFC → $C983 **`ResetHandler`** ($C983) On power-on or reset: 1. `sei` — disable maskable interrupts. 2. `cld` — clear decimal mode. 3. `lda #$00 / sta PPUCTRL` — disable PPU rendering and NMI. 4. `dex / txs` — initialize stack pointer to $FF. 5. `UpdateSideScrollingGameplay_C98C` ($C98C): wait for VBlank (poll `PPUSTATUS` bit 7), then clear a block of PPU memory via `ConfigureHudAndPlayfieldPpu_C99D` ($C99D) using zero-page pointers $00/$01 and counter $01. 6. Set PPUCTRL = $00, `PpuControlShadow` ($10) = $00, PPUMASK = $02, `PpuMaskShadow` ($11) = $02. 7. Set scroll registers: `PPUSCROLL` = $00 twice, `HorizontalScroll` ($12) = $00, `VerticalScroll` ($13) = $00. 8. Configure HUD and playfield PPU regions via repeated calls to `ConfigureHudAndPlayfieldPpu_C092` ($C092) with various source/destination parameters — uploads background tiles, HUD elements, and playfield data. 9. Initialize game RAM variables: `Ram_03FB` = $80, `Ram_06AB` = $90, `Ram_06CB` = $94, `Ram_06EB` = $98, `Ram_05C0` = $03, `Ram_0580` = $04, `Ram_060B` = $68, `Ram_064B` = $70. 10. `Zp_1D` ($1D) = $55; `SND_CHN` ($4015) = $0F (enable all APU channels); `Ram_0100` = $0F; `Zp_36` ($36) = $01. 11. Enable NMI: `PpuControlShadow` ($10) \| $80, store to shadow and PPUCTRL. 12. Falls into `UpdateSideScrollingGameplay_CA6A` ($CA6A) — the permanent main loop. --- ## 4. Main Loop — `UpdateSideScrollingGameplay_CA6A` ($CA6A) Once NMI is enabled, `ResetHandler` never returns. It enters the main loop: ``` UpdateSideScrollingGameplay_CA6A ($CA6A): jsr UpdateSideScrollingGameplay_C40D ; $CA6A: 20 0D C4 lda Zp_A2 ; $CA6D: A5 A2 beq UpdateSideScrollingGameplay_CA6A ; $CA6F: F0 F9 (spin while Zp_A2 == 0) dec Zp_A2 ; $CA71: C6 A2 jsr UpdateSideScrollingGameplay_E47A ; $CA73: 20 7A E4 jsr UpdateSideScrollingGameplay_DC7B ; $CA76: 20 7B DC jsr UpdateSideScrollingGameplay_E6E1 ; $CA79: 20 E1 E6 jsr UpdateSideScrollingGameplay_D91A ; $CA7C: 20 1A D9 jsr UpdateSideScrollingGameplay_E6D5 ; $CA7F: 20 D5 E6 jsr UpdateSideScrollingGameplay_E7AA ; $CA82: 20 AA E7 jsr UpdateSideScrollingGameplay_E029 ; $CA85: 20 29 E0 jsr UpdateSideScrollingGameplay_DE78 ; $CA88: 20 78 DE jmp UpdateSideScrollingGameplay_CA6A ; $CA8B: 4C 6A CA (loop forever) ``` The main loop spins waiting for `Zp_A2` ($A2) to become nonzero (set by the NMI), then decrements it and calls a chain of per-frame update routines covering flying entities, object updates, collisions, scrolling, and rendering. It then loops back to itself. --- ## 5. NMI / Frame Heartbeat — `NmiHandler` ($CAA1) The NMI fires once per frame and is the central real-time driver: 1. **Save registers** (A, X, Y on stack). 2. `ldx #$00` — X = 0 for buffer copy loop. 3. `ConfigureHudAndPlayfieldPpu_CAA8` ($CAA8): copy 16 bytes from zero-page pointer $00,X into PPU registers (PPU command buffer upload), toggling `PpuControlShadow` bit 7. 4. `jsr ConfigureHudAndPlayfieldPpu_D2CF` ($CAB9) — additional PPU configuration. 5. `jsr TransferShipTargetOamBuffer` ($CABC) — OAM DMA: write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014). 6. `jsr UpdateSideScrollingGameplay_C073` ($CABF) — configure HUD/playfield PPU and reset scroll. 7. Restore PPU mask from `PpuMaskShadow` ($11). 8. `jsr PollShipControllers` ($CAC7) — read controller ports $4016/$4017 into `PreviousControllerButtons` ($19) and `CurrentControllerButtons` ($1B). 9. `jsr TransformFlightState_CEE8` ($CACA) — process ship flight state. 10. If `Zp_3B` ($3B) != 0 → `UpdateSideScrollingGameplay_CAD4`; else → `UpdateSideScrollingGameplay_C3CC` ($CAD1). 11. `UpdateSideScrollingGameplay_CAD4` ($CAD4): `jsr ReadShipControls_F880`, `jsr UpdateSideScrollingGameplay_CBD4`, then `jsr UpdateSideScrollingGameplay_D6D8` ($CADA). 12. `ConfigureHudAndPlayfieldPpu_CADF` ($CADF): pop saved registers (Y, X, A) from the stack — restores the context saved at the start of NMI. 13. Re-enable NMI: `PpuControlShadow` \| $80, store to shadow and PPUCTRL. 14. `pla / rti` — restore A and return from interrupt. The NMI is the frame heartbeat: it handles PPU updates, OAM DMA, controller polling, and gameplay state updates, all during VBlank. --- ## 6. PPU / Rendering ### `ConfigureHudAndPlayfieldPpu_C092` ($C092) - Reads PPUSTATUS to reset address latch. - Loads `PpuControlShadow` ($10), ANDs with $FB (disable sprite rendering bit), stores to PPUCTRL and shadow. - Sets up PPU address and writes tile runs to $2007 — used for HUD and playfield background updates. ### `TransferShipTargetOamBuffer` ($C087) - Write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014) — DMA sprite data from $0200–$02FF. ### `PollShipControllers` ($C434) - Reads both controller ports, tracking previous and current button states. ### `UpdateSideScrollingGameplay_C073` ($C073) - Configures HUD/playfield PPU upload and resets scroll. Used during NMI and gameplay state transitions. --- ## 7. Zero-Page / RAM Symbols | Address | Symbol | Use | |---------|--------|-----| | $00–$03 | ScratchValue0–3 | General scratch values | | $04–$05 | DataPointerLo/Hi | Data pointer | | $06–$07 | TemporaryPointerLo/Hi | Temporary pointer | | $08–$0D | Zp_08–Zp_0D | Zero-page scratch | | $0F | Zp_0F | Zero-page scratch | | $10 | PpuControlShadow | $2000 mirror | | $11 | PpuMaskShadow | $2001 mirror | | $12 | HorizontalScroll | Horizontal scroll value | | $13 | VerticalScroll | Vertical scroll value | | $14–$18 | Zp_14–Zp_18 | Zero-page scratch | | $19 | PreviousControllerButtons | Previous frame controller state | | $1A | Zp_1A | Zero-page scratch | | $1B | CurrentControllerButtons | Current frame controller state | | $1C–$1F | Zp_1C–Zp_1F | Zero-page scratch | | $20–$3F | Zp_20–Zp_3F | Zero-page scratch (extensive use) | | $36 | Zp_36 | Game flag | | $3B | Zp_3B | Gameplay branch flag | | $3F | Zp_3F | Zero-page scratch | | $40–$47 | Zp_40–Zp_47 | Zero-page scratch | | $4C | Zp_4C | Zero-page scratch | | $54–$5F | Zp_54–Zp_5F | Zero-page scratch | | $60–$61 | Zp_60–Zp_61 | Zero-page scratch | | $A0–$A7 | Zp_A0–Zp_A7 | Zero-page scratch ($A2 used as NMI frame sync) | | $A9–$AB | Zp_A9–Zp_AB | Zero-page scratch | | $AC–$AF | Zp_AC–Zp_AF | Zero-page scratch | | $B0–$B9 | Zp_B0–Zp_B9 | Zero-page scratch | | $BA–$BF | Zp_BA–Zp_BF | Zero-page scratch | | $C0–$C4 | Zp_C0–Zp_C4 | Zero-page scratch | | $D0–$D5 | Zp_D0–Zp_D5 | Zero-page scratch | | $D7–$DF | Zp_D7–Zp_DF | Zero-page scratch | | $E0–$EF | Zp_E0–Zp_EF | Zero-page scratch | | $F0–$F7 | Zp_F0–Zp_F7 | Zero-page scratch | | $FA–$FB | Zp_FA–Zp_FB | Zero-page scratch | | $FD–$FF | Zp_FD–Zp_FF | Zero-page scratch | | $0100 | Ram_0100 | APU/channel state | | $0200 | OamBuffer | Sprite DMA source | | $0201–$0203 | Ram_0201–Ram_0203 | RAM scratch | | $0214, $0244, $0299, $029C–$029D, $029F–$02FE | Ram_* | Game state RAM | | $0330–$0331 | Ram_0330–Ram_0331 | Gameplay state | | $0380–$03FF | Ram_0380–Ram_07FF | Extensive game state RAM (objects, entities, scores, etc.) | | $0540–$05FF | Ram_0540–Ram_05FF | Gameplay variables | | $0600–$06FF | Ram_0600–Ram_06FF | Entity/object arrays | | $0782–$07F2 | Ram_0782–Ram_07F2 | Late-game state | --- ## 8. Routine Naming Patterns The disassembly uses consistent name prefixes that indicate routine purpose: | Prefix | Example | Meaning | |--------|---------|---------| | `UpdateSideScrollingGameplay_*` | `UpdateSideScrollingGameplay_C073` | Side-scrolling gameplay logic | | `UpdateGameplayStateField_*` | `UpdateGameplayStateField_C1F7` | Gameplay state field updates | | `TransformFlightState_*` | `TransformFlightState_C109` | Ship flight/transform state | | `ConfigureHudAndPlayfieldPpu_*` | `ConfigureHudAndPlayfieldPpu_C092` | PPU HUD/playfield configuration | | `CalculateMovementAndClamp_*` | `CalculateMovementAndClamp_C183` | Movement calculation with clamping | | `UpdateFlyingEntity_*` | `UpdateFlyingEntity_CF11` | Flying entity (ship/enemy) updates | | `ReadShipControls_*` | `ReadShipControls_F880` | Ship controller input | | `ReadObjectRecord_*` | `ReadObjectRecord_DADA` | Object record reading | | `ProcessObjectStateRoutine_*` | `ProcessObjectStateRoutine_CBE3` | Object state machine | | `UpdateObjectBuffer_*` | `UpdateObjectBuffer_DB52` | OAM buffer updates | | `UpdateObjectRecord_*` | `UpdateObjectRecord_D27D` | Object record maintenance | --- ## 9. Summary of Execution Flow ``` Power On / Reset │ ▼ ResetHandler ($C983) ───────────────────────────────┐ │ sei, cld, disable PPU, stack=$FF │ │ wait VBlank, clear PPU memory │ │ set PPUCTRL=$00, PPUMASK=$02, scroll=$00 │ │ upload HUD/playfield tiles via C092 │ │ init game RAM, APU channels ($4015=$0F) │ │ enable NMI (PpuControlShadow \| $80) │ ▼ │ jmp UpdateSideScrollingGameplay_CA6A ($CA6A) │ │ │ │ Main loop — waits for NMI frame sync: │ │ wait until Zp_A2 ($A2) != 0 │ │ dec Zp_A2 │ │ jsr E47A, DC7B, E6E1, D91A, E6D5, E7AA, │ │ E029, DE78 │ │ jmp back to CA6A │ │ │ └────────────────────────────────────────────────┘ │ NMI ($CAA1) [once per frame] │ │ │ ├─ save A, X, Y │ ├─ PPU command buffer upload ($CAA8) │ ├─ ConfigureHudAndPlayfieldPpu_D2CF ($CAB9) │ ├─ OAM DMA ($4014 from $0200) │ ├─ ConfigureHudAndPlayfieldPpu_C073 ($C073) │ ├─ restore PPU mask from shadow │ ├─ PollShipControllers ($CAC7) │ ├─ TransformFlightState_CEE8 ($CEE8) │ ├─ branch on Zp_3B: CAD4 or C3CC │ ├─ CAD4: ReadShipControls_F880, CBD4, D6D8 │ ├─ restore registers, re-enable NMI │ └─ rti │ IRQ ($CA9E) — self-loop (unused) ``` --- ## 10. Notes - **Build verification:** `make` in `copilot/` produces `defender2_rebuilt.nes` that is **byte-identical** to `defender2.nes` and to the PRG bank of `Defender 2 (U).nes`. - Vectors confirm NMI at $CAA1, RESET at $C983, and IRQ at $CA9E (self-loop, unused). - The main loop (`CA6A`) is a tight NMI-synchronized loop; all substantive per-frame work happens either in the NMI handler or in the routines called from the main loop after the NMI sets `Zp_A2`. - Many routines carry address-based names (`UpdateSideScrollingGameplay_C073`, `TransformFlightState_C109`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional. - The disassembly includes inline `.byte` data blocks interleaved with code; boundaries are marked as classified by Pass 1. - `locals.inc` provides zero-page names; `symbols.inc` provides the NES hardware register set. - One build fix was required: a `bpl` branch to an undefined symbol `LookupTableEntry_FAF5` was replaced with `bne *-$1B` to produce the original `d0 e3` bytes (the disassembler had mislabeled `bne` as `bpl`).