13 KiB
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_FAF5withbne *-$1Bat CPU $E811, which produces the exact original bytesd0 e3(the disassembler had mislabeledbneasbpl). - The build now produces a byte-identical ROM.
3. Reset — CPU $FFFC → $C983
ResetHandler ($C983)
On power-on or reset:
sei— disable maskable interrupts.cld— clear decimal mode.lda #$00 / sta PPUCTRL— disable PPU rendering and NMI.dex / txs— initialize stack pointer to $FF.UpdateSideScrollingGameplay_C98C($C98C): wait for VBlank (pollPPUSTATUSbit 7), then clear a block of PPU memory viaConfigureHudAndPlayfieldPpu_C99D($C99D) using zero-page pointers $00/$01 and counter $01.- Set PPUCTRL = $00,
PpuControlShadow($10) = $00, PPUMASK = $02,PpuMaskShadow($11) = $02. - Set scroll registers:
PPUSCROLL= $00 twice,HorizontalScroll($12) = $00,VerticalScroll($13) = $00. - 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. - 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. Zp_1D($1D) = $55;SND_CHN($4015) = $0F (enable all APU channels);Ram_0100= $0F;Zp_36($36) = $01.- Enable NMI:
PpuControlShadow($10) | $80, store to shadow and PPUCTRL. - 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:
- Save registers (A, X, Y on stack).
ldx #$00— X = 0 for buffer copy loop.ConfigureHudAndPlayfieldPpu_CAA8($CAA8): copy 16 bytes from zero-page pointer $00,X into PPU registers (PPU command buffer upload), togglingPpuControlShadowbit 7.jsr ConfigureHudAndPlayfieldPpu_D2CF($CAB9) — additional PPU configuration.jsr TransferShipTargetOamBuffer($CABC) — OAM DMA: write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014).jsr UpdateSideScrollingGameplay_C073($CABF) — configure HUD/playfield PPU and reset scroll.- Restore PPU mask from
PpuMaskShadow($11). jsr PollShipControllers($CAC7) — read controller ports $4016/$4017 intoPreviousControllerButtons($19) andCurrentControllerButtons($1B).jsr TransformFlightState_CEE8($CACA) — process ship flight state.- If
Zp_3B($3B) != 0 →UpdateSideScrollingGameplay_CAD4; else →UpdateSideScrollingGameplay_C3CC($CAD1). UpdateSideScrollingGameplay_CAD4($CAD4):jsr ReadShipControls_F880,jsr UpdateSideScrollingGameplay_CBD4, thenjsr UpdateSideScrollingGameplay_D6D8($CADA).ConfigureHudAndPlayfieldPpu_CADF($CADF): pop saved registers (Y, X, A) from the stack — restores the context saved at the start of NMI.- Re-enable NMI:
PpuControlShadow| $80, store to shadow and PPUCTRL. 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:
makeincopilot/producesdefender2_rebuilt.nesthat is byte-identical todefender2.nesand to the PRG bank ofDefender 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 setsZp_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
.bytedata blocks interleaved with code; boundaries are marked as classified by Pass 1. locals.incprovides zero-page names;symbols.incprovides the NES hardware register set.- One build fix was required: a
bplbranch to an undefined symbolLookupTableEntry_FAF5was replaced withbne *-$1Bto produce the originald0 e3bytes (the disassembler had mislabeledbneasbpl).