Files
disasm/Defender 2 (U)/OVERVIEW.md
T

13 KiB
Raw Blame History

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).