Standardizes build process and adds OVERVIEW.md for each

This commit is contained in:
tmerritt committed 2026-10-15 18:50:41 -04:00
1 parent 25423d3828
commit a29b03b00b
135 files changed
+26562 -420

No files matched your search

+254
View File
@@ -0,0 +1,254 @@
# 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`).