Files
disasm/Excitebike (JU)/OVERVIEW.md
T

418 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Excitebike (JU) — Game Flow from Power-On
This document describes the execution flow of the disassembled Excitebike (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. 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` (14,111 lines) |
| Output | `reconstructed_prg.bin` (16,384 bytes) |
### Interrupt vectors (CPU $FFFA–$FFFF)
| Vector | Address | Handler |
|--------|---------|---------|
| NMI | $C23B | `NMI` |
| RESET | $C184 | `Reset` |
| IRQ / BRK | $FFF0 | (`$FFF0` — points into audio data region, unused) |
---
## 2. Build Status
`make` in `copilot/` assembles `prg.s` with `ca65`, links with `ld65`, and verifies against `prg.bin`.
**Verification result: BYTE-IDENTICAL** — `reconstructed_prg.bin` matches `prg.bin` exactly (headers, CHR, and all PRG bytes).
### Build fix applied
The original source contained a `bpl LookupTableEntry_FAF5` branch at CPU $FAB4 (line 9425 of `prg.s`), but the symbol `LookupTableEntry_FAF5` was undefined. The branch target CPU $FAF5 fell in the middle of what the disassembler classified as `lda #$16` (`A9` at $FAF4, `16` at $FAF5). The fix:
- Replaced `bpl LookupTableEntry_FAF5` with `bne *-$1B` at CPU $FAB4, producing the exact original bytes `d0 e3` (the disassembler had mislabeled `bne` as `bpl`).
---
## 3. Reset — CPU $FFFC → $C184
**`Reset`** ($C184)
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. `ldx #$FF / txs` — initialize stack pointer to $FF.
5. **Wait for two VBlanks** — poll `PPUSTATUS` bit 7 twice (`@vblank1`, `@vblank2`). This is the PPU warm-up delay.
6. **Reset magic check** — compare `ResetMagic1` ($05CE) against `$A5` and `ResetMagic2` ($05CF) against `$5A`:
- If both match → warm start: set Y = $04 (preserve more RAM)
- If either mismatches → cold start: set Y = $07 (clear more RAM)
7. **Clear RAM** — clear zero page and one page ($0000–$00FF, or $0000–$06FF depending on warm/cold start) using indirect pointer at $00/$01.
8. **Persistent RAM init** — if reset magic was valid, `jsr InitializePersistentRam` ($C278):
- Copy 31-byte template (`PersistentRecordInitialDataC091` at $C091) to both `PersistentRecord0` ($0580) and `PersistentRecord1` ($05A3)
- Fill `PersistentBuffer` ($06E0) with $40, set byte at $07AF to $09
- Set reset magic bytes to $A5/$5A
- Zero tail bytes of both persistent records
9. `sta APUDMALOAD` — disable DMC.
10. `lda #$06 / sta PPUMASK / sta $18` — enable background + sprites, set $18.
11. `jsr InitializeRuntimePpuState` — fill OAM shadow with $F8, process PPU entries, zero scroll/render fields.
12. `lda #$90 / jsr SetPpuControl` — set PPUCTRL = $90 (NMI enabled, $2000 base).
13. Falls into **`@main`** ($C1A4) — the permanent main loop.
---
## 4. Main Loop — `@main` ($C1A4)
After reset, `Reset` never returns. It enters the infinite main loop:
```
@main ($C1A4):
jsr ShiftRuntimeState ; $E8BC — shift runtime bytes $18-$1F right
lda NmiSyncState ; $48
bmi @main ; loop if $FF (NMI_SYNC_PENDING)
beq @main ; loop if $00
; NmiSyncState == $01 (NMI_SYNC_COMPLETE): proceed
jsr DisableNmi ; $C362 — clear NMI bit in PPUCTRL
lda NmiSyncState
cmp #NMI_SYNC_COMPLETE ; $01
bne @alternate ; if not complete, receive instead
jsr TransmitRuntimeDataCode ; $C6C8 — serial transmit runtime data
jmp @finish
@alternate:
jsr ReceiveRuntimeDataCode ; $C739 — serial receive runtime data
@finish:
lda #NMI_SYNC_PENDING ; $FF
sta NmiSyncState ; $48 — reset sync flag for next frame
jsr WaitForVblankEndAndEnableNmi ; wait for VBlank, set NMI bit
jmp @main ; loop forever
```
The main loop is **NMI-synchronized**: it blocks until `NmiSyncState` ($48) becomes $01 (set by NMI each frame), then performs serial I/O (transmit or receive runtime data), resets the sync flag to $FF, waits for the next VBlank, enables NMI, and repeats.
### `ShiftRuntimeState` ($E8BC)
Shifts runtime state bytes $18–$1F right by one bit. The carry-in is derived from controller bit 1 ($19 bit 2), enabling serial/data shifting tied to controller input.
---
## 5. NMI / Frame Heartbeat — `NMI` ($C23B)
The NMI fires once per frame and is the central real-time driver:
1. **`jsr DisableNmi`** ($C362) — clear bit 7 of `PpuControlShadow` ($10), write to PPUCTRL ($2000). NMI is disabled during the handler to prevent re-entry.
2. **PPU mask**: if $45 == 0, `PPUMASK = PpuMaskShadow | $1A` (enable background + sprites); otherwise skip.
3. **OAM DMA**: write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014) — copy $0200–$02FF to sprite RAM.
4. **`jsr NmiPpuUpdate`** ($C27E) — process one PPU update command stream (indexed by $45), then reset $0300/$0301/$45 and PPU address to $3F00.
5. **Optional rider/lane update**: if $47 != 0, `jsr UpdateRiderLanePositionStates` and `jsr NmiRuntimeUpdate`.
6. **Runtime counters**: if $03B1 | $03B2 == 0, increment $3F and `jsr UpdateRuntimeCounters`.
7. **`jsr ReadControllers`** ($E8E5) — read both controller ports ($4016/$4017), update `Controller1Held` ($14), `Controller1Pressed` ($15), `Controller2Held` ($16), `Controller2Pressed` ($17).
8. **`jsr UpdateAudio`** ($F8DE) — update all four APU channels (Pulse 1, Pulse 2, Noise, Sound Effect) plus sound effects and DMA.
9. **`jsr DispatchNmiState`** ($C2A9) — dispatch to the current NMI state handler (see below).
10. **Read PPUSTATUS** ($2002) to reset the address latch.
11. **`jsr WaitForVblankEndAndEnableNmi`** — wait for VBlank end, set NMI bit in PPUCTRL.
12. **`rti`** — return from interrupt.
### `NmiPpuUpdate` ($C27E)
Processes a single PPU update command:
- Load pointer from `PpuUpdatePointerLow/High` ($C0F6/$C111 tables) indexed by $45.
- Call `ProcessPpuUpdateStream` ($D3C1) which reads the command stream and writes bytes to $2007.
- Zero out $0300, $0301, and $45.
- Reset PPU address to $3F00 (four writes to $2006).
---
## 6. NMI State Machine — `DispatchNmiState` ($C2A9)
The NMI dispatcher supports two modes based on `NmiDispatchMode` ($40):
### Mode 0 — Direct state dispatch (`DispatchNmiState` @ $C2A9)
If `NmiDispatchMode` == 0:
- Load `NmiState` ($41), double it, use as index into `NmiStateHandlerTable` ($C000–$C01B).
- Jump indirect through zero-page $00/$01 (high byte left in A, stored to $01 by code at $CB36).
**NMI State Handler Table** ($C000–$C01B) — 14 entries for states $00–$0D:
| State | Handler Address | Purpose |
|-------|----------------|---------|
| $00 | `NmiState00` | Menu/title — selection handling |
| $01 | $C395 | Menu horizontal input |
| $02 | `UpdateGameplayRuntime` | Main gameplay update |
| $03 | `DispatchNmiSequenceStep` | Sequence step dispatch |
| $04 | `NmiState04_07` | Shared state $04/$07 handler |
| $05 | `UpdateGameplayRuntime` | Main gameplay update |
| $06 | `DispatchNmiSequenceStep` | Sequence step dispatch |
| $07 | `NmiState04_07` | Shared state $04/$07 handler |
| $08 | $C3BD | Menu vertical input |
| $09 | `UpdateRaceRuntime` | Race gameplay update |
| $0A | `UpdateGameplayRuntime` | Main gameplay update |
| $0B | `DispatchNmiSequenceStep` | Sequence step dispatch |
| $0C | $C43D | Serial handshake state $0C |
| $0D | $C41C | Serial handshake state $0D |
### Mode 1 — Sequence dispatch
If `NmiDispatchMode` != 0:
- Disable PPU rendering.
- Wait until $30 == 0 (not busy).
- Look up sequence pointer from `NmiStateToSequenceIndex` + `NmiSequencePointerLow/High` tables.
- Call the sequence handler at the current `NmiSequenceStep` ($44).
- If step < 0 (after increment/decrement), go to `InitializeRuntimePpuState`.
**NMI Sequence Pointers** ($C03C–$C07F) — 9 sequences:
| Index | Address | Handlers |
|-------|---------|----------|
| 0 | $C03C | $C2BD, $C514 |
| 1 | $C040 | `SetSerialUpdateSelector06`, $C3F3, $C514 |
| 2 | $C046 | `SerialUpdateSelectorCluster`, `ClearRuntimeZeroPage`, `InitializeCourseRuntime`, `SetSerialUpdateSelector17`, `SetSerialUpdateSelector0E`, `InitializeGameRuntime` |
| 3 | $C052 | `SetSerialUpdateSelector06`, `SetSerialUpdateSelector04`, `SelectNmiUpdateForState`, `CallC92CWithZero`, `CompareAndUpdateRecord` |
| 4 | $C05C | `SetSerialUpdateSelector06`, `SetSerialUpdateSelector09`, `Prepare0300Structure`, $C514 |
| 5 | $C064 | $C551, `ClearRuntimeZeroPage`, `InitializeCourseRuntime`, `SetSerialUpdateSelector17`, $C562, $C5C2 |
| 6 | $C070 | `SetSerialUpdateSelector06`, $C575, $C514 |
| 7 | $C07A | `ClearRuntimeZeroPage`, `RestoreWorkingRecordFields`, $C518 |
| 8 | $C076 | $C582, $C518 |
---
## 7. Key NMI State Handlers
### `NmiState00` ($C346)
Title/menu state:
- If $31 == 0: initialize $43, $4A = 0, $03F7 = $0F, `NmiState` = 2 (gameplay).
- Else: `jsr UpdateMenuSelection` with count 3; if no input change, set sprite 0 Y from `MenuInitialYTable`; otherwise handle selection done and advance state.
### `NmiState01` ($C397)
Menu horizontal input:
- Filter controller buttons, `jsr UpdateMenuSelection` (count 5).
- If input changed, move horizontal marker; else store `MenuSelectionIndex` to $43 and advance to next NMI state, entering sequence mode.
### `NmiState08` ($C3BD)
Menu vertical input:
- `jsr UpdateMenuSelection` (count 6).
- Based on selection: advance state, jump to sequence entry, or handle choice 5 (reset runtime, preserve selection).
### `NmiState04_07` ($C465)
Shared handler for states $04/$07:
- If $34 == 0: advance NMI state and enter sequence mode; clamp state to max 7.
- Else: `jsr $C92C`, check bit 4 of $3F, either return or `jmp StateHelperCAFA`.
### `NmiState0C` / `NmiState0D` ($C41C / $C43D)
Serial handshake states:
- State $0C: if sequence step == 0, set sync complete; else if sync pending, copy record tails and continue handshake.
- State $0D: similar handshake with persistent record synchronization.
---
## 8. Gameplay Runtime
### `UpdateGameplayRuntime` ($C99A)
Called from NMI states $02, $05, $0A:
- `jsr $CA08` (update four rider slots).
- If $03B1 | $03B2 != 0: jump to PPU update tail.
- Otherwise: `jsr $D918`, `jsr $DD8D`, `jsr UpdateAllRiderScriptStateE733`, `jsr $E927`.
- If $52 != 0: `jsr StateHelperCA9B`.
- If $03F2 != 0: optional updates ($DA9F, $DBC4, $DEBB).
- `jsr UpdateGlobalCounters`, `jsr BuildGlobalDisplayEAC5`, `jsr CompareRuntimeSlots`, `jsr CheckSlotState`.
- `jsr UpdateTriggeredSlots`, `jsr UpdateFourSlots`, `jsr UpdateFourSlotPositionsE96C`, `jsr UpdateDisplayState`, `jsr UpdateFourSlot8CE836`, `jsr BuildFourSlotSpritesEA44`, `jsr ProcessCourseDefinition`, `jsr UpdateSlotPairState`, `jsr UpdateGlobalFC`, `jsr UpdateGlobalDisplay`, `jsr ProcessRuntimeEntries`, `jsr PrepareSortedSlots`, `jsr UpdateRuntimeSlots`, `jsr UpdateRiderCourseStateE70B`, `jsr UpdatePpuState`, `jsr ProcessRiderSlots`, `jmp ProcessSortedSlots`.
### `UpdateRaceRuntime` ($C8D6)
Race-specific update:
- `jsr $CA1B`, `jsr UpdateGlobalRuntimeEB17`, `jsr UpdateAccumulator`, `jsr UpdateGlobalPosition`, `jsr UpdateRiderCourseStateE70B`, `jsr ProcessCourseDefinition`, `jsr $F68D`, `jsr $F755`, `jsr $C229`, then `jmp UpdatePpuState`.
---
## 9. Audio System
**`UpdateAudio`** ($F8DE) — called from NMI:
1. `APUFRAMECNT` ($4017) = $C0.
2. `jsr UpdatePulse1Channel`, `jsr UpdatePulse2Channel`, `jsr UpdateNoiseChannel`, `jsr UpdateSoundEffect`.
3. Zero out $FF, $FE, $FD, $FB.
4. If $07FF != 0: `sta APUDMALOAD` ($4011), decrement $07FF.
Audio channel configuration uses tables at $FF00+ (period tables) and $FF3E+ (mask tables). Audio sequence/data lives at $FF64–$FFE9.
---
## 10. Serial I/O System
Excitebike uses the controller port $4016 for serial communication (likely the Famicom 3D System or a similar serial peripheral — peripheral identity unknown):
**Transmit path** (`TransmitRuntimeDataCode` at $C6C8):
- Configure a 64-byte block from $0060, transmit it.
- Configure a 20-byte block from $05E0 (working buffer), transmit it.
- Uses pulse-length encoding: long pulse = 1, short pulse = 0, MSB first.
- Preamble: short pulses, then long pulses, then short pulses, then one long pulse.
**Receive path** (`ReceiveRuntimeDataCode` at $C739):
- Disable NMI, configure receive blocks, receive data.
- Validate 5-byte header against `ReceiveHeaderTableBaseC17F` ($C17F: $E8).
- If valid: set state variables ($A0–$A3), set $45 = $1A, wait for NMI update.
- Receive second block (working buffer), validate checksum, return success/failure.
Serial helpers: `TransmitByte` ($C66E), `ReceiveByte` ($C6A5), `TransmitPulseShort` ($C64E), `TransmitPulseLong` ($C653), `ReceiveSerialBit` ($C691), `SerialWaitPulse` ($C639).
---
## 11. Zero-Page / RAM Symbols
| Address | Symbol | Use |
|---------|--------|-----|
| $00–$01 | TMP00/TMP01 | General scratch / indirect pointer |
| $08–$09 | SerialBitCountLo/Hi | Serial bit counter |
| $0E | SerialBlockPtr | Serial block pointer |
| $0D | SerialBlockLength | Serial block length |
| $10 | PpuControlShadow | $2000 mirror |
| $11 | PpuMaskShadow | $2001 mirror |
| $12–$1F | — | Runtime state bytes (shifted each frame by `ShiftRuntimeState`) |
| $14 | Controller1Held | Controller 1 held buttons |
| $15 | Controller1Pressed | Controller 1 newly pressed |
| $16 | Controller2Held | Controller 2 held buttons |
| $17 | Controller2Pressed | Controller 2 newly pressed |
| $31 | — | Menu/timing state |
| $34 | — | Menu selection state |
| $3F | — | Runtime counter |
| $40 | NmiDispatchMode | NMI dispatch mode (0=direct, 1=sequence) |
| $41 | NmiState | Current NMI state ($00–$0D) |
| $42 | MenuSelectionIndex | Current menu selection |
| $43–$4A | — | State/temp variables |
| $44 | NmiSequenceStep | Current NMI sequence step |
| $45 | — | PPU update index |
| $47 | — | Optional update flag |
| $48 | NmiSyncState | $FF=PENDING, $01=COMPLETE |
| $49 | InputActionLatch | Menu input latch |
| $A4 | RiderMotionState | Rider motion state ($01=disabled, $02=active, $03=transition, $04=complete) |
| $0100 | — | DMA/APU scratch |
| $0200 | OamBuffer | OAM shadow / sprite DMA source |
| $0300–$0301 | — | PPU update command structure pointer |
| $03B1–$03B2 | — | Timed update flag |
| $0580 | PersistentRecord0 | First persistent record (31 bytes + tails) |
| $05A3 | PersistentRecord1 | Second persistent record |
| $05CE–$05CF | ResetMagic1/2 | $A5/$5A warm-start signature |
| $05E0 | WorkingBuffer | Working buffer for serial I/O |
| $06E0 | PersistentBuffer | Persistent buffer (256 bytes) |
| $07FF | — | Audio DMA trigger |
---
## 12. PPU / Rendering
- **OAM DMA**: Every NMI, DMA from $0200–$02FF via `OAMDMA` ($4014).
- **PPU update streams**: Indexed pointer tables at $C0F6 (low) / $C111 (high), 27 entries. Processed by `ProcessPpuUpdateStream` ($D3C1) during NMI.
- **Scroll**: `PpuControlShadow` ($10) and `PpuMaskShadow` ($11) are software mirrors, updated during NMI and written to $2000/$2001.
- **OAM shadow**: Filled with $F8 (offscreen) by `FillOamShadowWithF8` during runtime PPU init.
---
## 13. Summary of Execution Flow
```
Power On / Reset
│
▼
Reset ($C184) ────────────────────────────────────────┐
│ sei, cld, disable PPU/NMI, stack=$FF │
│ wait 2× VBlank (PPU warm-up) │
│ check reset magic ($A5,$5A at $05CE/$05CF) │
│ valid → warm start (preserve more RAM) │
│ invalid → cold start (clear more RAM) │
│ clear RAM ($0000–$00FF or $0000–$06FF) │
│ if valid: InitializePersistentRam │
│ (copy template to both persistent records) │
│ disable DMC, PPUMASK=$06, PPUCTRL=$90 (NMI on) │
│ InitializeRuntimePpuState │
│ │
▼ │
@main ($C1A4) ────────────────────────────────────────┤
│ │
├─ jsr ShiftRuntimeState ($E8BC) ──────────────┐
│ shift $18-$1F right, carry from controller │
│ │
├─ lda NmiSyncState ($48) │
├─ bmi @main (if $FF = PENDING, loop) │
├─ beq @main (if $00, loop) │
│ (only proceed when NMI set it to $01) │
│ │
├─ jsr DisableNmi ($C362) │
├─ if NmiSyncState == $01: │
│ jsr TransmitRuntimeDataCode ($C6C8) │
│ else: │
│ jsr ReceiveRuntimeDataCode ($C739) │
│ │
├─ lda #NMI_SYNC_PENDING ($FF) │
│ sta NmiSyncState │
├─ jsr WaitForVblankEndAndEnableNmi │
└─ jmp @main ◄──────────────────────────────────┘
│
NMI ($C23B) [once per frame] │
│ │
├─ jsr DisableNmi (PPUCTRL bit7 = 0) │
├─ PPUMASK = PpuMaskShadow \| $1A (if $45==0) │
├─ OAM DMA ($0200-$02FF → $4014) │
├─ jsr NmiPpuUpdate ($C27E) [PPU update stream] │
├─ if $47 != 0: │
│ jsr UpdateRiderLanePositionStates │
│ jsr NmiRuntimeUpdate ($D14E) │
├─ if $03B1|$03B2 == 0: inc $3F, jsr UpdateRuntimeCounters │
├─ jsr ReadControllers ($E8E5) │
├─ jsr UpdateAudio ($F8DE) [4 channels + SFX] │
├─ jsr DispatchNmiState ($C2A9) │
│ ├─ NmiDispatchMode==0: direct state table │
│ │ $00=NmiState00 (menu/title) │
│ │ $01=$C395 (menu horiz) │
│ │ $02=$C99A (UpdateGameplayRuntime) │
│ │ $03=DispatchNmiSequenceStep │
│ │ $04/$07=NmiState04_07 │
│ │ $05=$C99A (UpdateGameplayRuntime) │
│ │ $06=DispatchNmiSequenceStep │
│ │ $07=NmiState04_07 │
│ │ $08=$C3BD (menu vert) │
│ │ $09=$C8D6 (UpdateRaceRuntime) │
│ │ $0A=$C99A (UpdateGameplayRuntime) │
│ │ $0B=DispatchNmiSequenceStep │
│ │ $0C=$C43D (serial handshake) │
│ │ $0D=$C41C (serial handshake) │
│ └─ NmiDispatchMode!=0: sequence dispatch │
│ (9 sequences at $C03C-$C07F) │
│ │
├─ read PPUSTATUS │
├─ WaitForVblankEndAndEnableNmi │
│ (set NmiSyncState = $01 implicitly via │
│ main loop's view of completion) │
└─ rti │
Serial I/O (from main loop, once per frame)
├─ TransmitRuntimeDataCode ($C6C8) [when sync complete]
│ ├─ 64-byte block from $0060 │
│ └─ 20-byte block from $05E0 (working buffer)│
└─ ReceiveRuntimeDataCode ($C739) [alternate]
├─ validate 5-byte header ($C17F) │
├─ receive second block │
└─ checksum verify │
Audio (from NMI via UpdateAudio at $F8DE)
├─ Pulse 1 channel ($4000-$4003) │
├─ Pulse 2 channel ($4004-$4007) │
├─ Noise channel ($400C-$400F) │
├─ Sound effect │
└─ DMC load if $07FF != 0 │
```
---
## 14. Notes
- **Build verification:** `make` in `copilot/` produces `reconstructed_prg.bin` that is **byte-identical** to `prg.bin` and to the PRG bank of `rom.nes`.
- Vectors confirm NMI at $C23B, RESET at $C184, IRQ at $FFF0 (points into audio data region — likely a placeholder or unused).
- The main loop (`@main` at $C1A4) is strictly NMI-synchronized via `NmiSyncState` ($48): it loops until the NMI sets the state to $01 (COMPLETE), performs serial I/O, resets to $FF (PENDING), waits for VBlank, and repeats.
- Serial I/O uses controller port $4016 for pulse-length encoded data transmission/reception — likely for the Famicom 3D System or a similar serial peripheral.
- The NMI state machine supports both direct state dispatch (14 states via table at $C000) and sequence dispatch (9 sequences via tables at $C03C–$C07F).
- Many routines carry address-based labels (`NmiState04_07`, `UpdateSideScrollingGameplay_C073`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional.
- `symbols.inc` (embedded at the top of `prg.s`) provides NES hardware register names and key zero-page symbols.
- One build fix was required: an undefined branch target `LookupTableEntry_FAF5` was replaced with a PC-relative `bne *-$1B` to produce the original `d0 e3` bytes.