# 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 `prg.s` in the repository (13,992 lines after the 2026-10-20 audit-trail repair; pre-repair counts in older copies differ). The rebuild is verified **byte-identical** to the original whole ROM (`rom.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 | `prg.s` (13,992 lines) | | Output | `reconstructed.nes` (whole ROM: 16-byte header + 16,384-byte PRG + 8,192-byte CHR) | ### 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 this folder assembles `prg.s` with `ca65`, links with `ld65`, and verifies against `rom.nes`. **Verification result: BYTE-IDENTICAL** — `reconstructed.nes` matches `rom.nes` 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`). ### 2026-10-20 fix (final byte-596 mismatch + audit trail + naming) Three follow-up repairs closed the project out: 1. **Byte-596 whole-ROM mismatch:** PRG `$0243`/`$024A` (CPU `$C242`/`$C249`) — the NMI mask block — emitted `A5 0A`/`85 0A` (`PpuMaskShadow`) where the ROM has `A5 11`/`85 11`. `$11` is a **second** PPU-mask shadow touched only there (NMI reads it, ORs `$1A`, writes it back and to `$2001`); it is now its own symbol, `NmiMaskShadow = $11` beside the neutral `Zp11 = $11` in `locals.inc`. 2. **Audit-trail repair:** 4,401 lines carried doubled `??` comment tails and 6,339 of 6,478 comment groups were systematically shifted against their mnemonics (the trail had been generated against an earlier, shorter source layout). Re-anchored from the `ca65 -l` listing via `_tools/repair_audit_comments.py`, which validates every comment against `prg.bin` and preserves non-audit notes such as `; state $02`. 3. **Operand naming:** all 58 raw `jsr/jmp $XXXX` operands were substituted with routine labels, plus 15 sub-entry equates in `locals.inc` (sub-entries are equates rather than mid-routine labels — a mid-routine `Name:` label re-scopes every `@local` after that point and breaks forward `@refs`). The four `jmp ($0000)` indirect dispatches became `jmp (IndirectJumpPtr)` ($00/$01 built from RAM by `DispatchIndirectHighByte`). `apply_symbols.py` remains unapplied for this project (inline `prg.s` definitions conflict with `locals.inc`); zero-page operands stay numeric as documented Pass 3 residue. ### 2026-10-21 pass: data-table interpretation Every data region in this project's `prg.s` was read, given a descriptive name and recorded in the **Pass 3 data-table inventory** below with its structure and the instruction that reads it. 133 labels that still carried an address-derived name from the first automated pass were renamed; no byte was changed and the build still verifies byte-identical. --- ## 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 | NmiMaskShadow | Second $2001 mirror used by NMI (read at $C242, OR $1A, written back at $C249); `PpuMaskShadow` = $0A remains the main shadow | | $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), `PpuMaskShadow` ($0A) and `NmiMaskShadow` ($11) are software mirrors, updated during NMI and written to $2000/$2001. `$11` is touched only by the NMI's mask OR ($C242/$C249) and was the source of the byte-596 rebuild mismatch. - **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 │ ``` ## Pass 3 data-table inventory `prg.s` for this project is hand-written rather than tracer-generated, so its data regions carry labels and `.byte`/`.word` directives instead of the tracer's `; ---- data $XXXX-$YYYY ----` blocks. The 2026-10-21 pass went through every one of those regions, gave the 133 labels that were still address-derived (`DataC0B0`, `ScriptE584`, `CoursePatternF112`, …) a descriptive name, and recorded here what each region is and which code reads it. "Read by" gives the address of the instruction in this dump; where a region is walked by a pointer-driven interpreter rather than named by an operand, the walker's address is given instead. ### NMI dispatch and menu state ($C000–$C18F) | Address | Size | Name | Structure | Read by | |---|---|---|---|---| | $C000–$C01B | 28 | `NmiStateHandlerTable` | 14 little-endian 16-bit handler addresses, one per NMI state $00–$0D. | `lda NmiStateHandlerTable,y` at $C2B1 and $C2B7 | | $C01C–$C029 | 14 | `NmiStateToSequenceIndex` | 14 bytes mapping each NMI state to a sequence index (0,1,2,3,4,2,3,4,6,5,2,3,7,8). | `lda NmiStateToSequenceIndex,x` at $C2F6 | | $C02A–$C032 | 9 | `NmiSequencePointerLow` | The low bytes of the nine NMI-sequence entry points. | `lda NmiSequencePointerLow,x` at $C2FA | | $C033–$C03B | 9 | `NmiSequencePointerHigh` | The matching high bytes (all $C0). | `lda NmiSequencePointerHigh,x` at $C2FF | | $C03C–$C07F | 68 | `NmiSequence0`…`NmiSequence8` | Nine sequences of 16-bit handler addresses (2, 3, 6, 5, 4, 6, 3, 3, 2 entries); the sequence walker is at $CB2C. | the sequence pointer at $CB36 indexes this area | | $C080–$C082 | 3 | `MenuInitialYTable` | The three menu entries' initial Y positions ($80, $90, $A0). | `lda MenuInitialYTable` at $C2E6 and `lda MenuInitialYTable,y` at $C364 | | $C083–$C087 | 5 | `MenuHorizontalXTable` | The five horizontal menu positions ($48, $60, $78, $90, $A8). | `lda MenuHorizontalXTable,y` at $C3B8 | | $C088–$C08A | 3 | `Sprite0InitialXTable` | The three sprite-0 X positions ($50, $48, $4C). | `lda Sprite0InitialXTable,y` at $C40E | | $C08B–$C08F | 5 | `MenuVerticalYTable` | The five vertical menu positions ($30, $48, $78, $90, $A8). | `lda MenuVerticalYTable,y` at $C3EC | | $C090 | 1 | `PersistentRecordHeader` | One byte, $C0, in front of the persistent-record template. | none directly | | $C091–$C0AF | 31 | `PersistentRecordInitialData` | The 31-byte template copied over both persistent records at power-on: 3-byte entries `$01 $10 $00`, then `$01 $0C $00`, `$01 $06 $00`, `$00 $01 $18 $00`, `$01 $16 $00`, `$01 $0A $00`, `$01 $14 $00`, `$01 $06 $00`. | `lda PersistentRecordInitialData,x` at $C1FF and `lda PersistentRecordInitialData,y` at $C52A | | $C0B0–$C0B3 | 4 | `RiderXPositionTable0` | Four one-byte X positions ($09, $0B, $0D, $0F). | none directly | | $C0B4–$C0B7 | 4 | `RiderXPositionTable1` | Four one-byte X positions ($0E, $1A, $26, $32). | none directly | | $C0B8–$C0BB | 4 | `RiderXPositionTable2` | Four one-byte X positions ($38, $48, $58, $68). | none directly | | $C0BC–$C0BF | 4 | `SlotPositionAddTable` | Four one-byte position additions ($18, $3F, $28, $20). | none directly | | $C0C0–$C0C3 | 4 | `SlotPlacementRecord0` | One four-byte slot record ($28, $38, $0C, $00). | none directly | | $C0C4–$C0C7 | 4 | `SlotPlacementRecord1` | One four-byte slot record ($3C, $1C, $C0, $7F). | none directly | | $C0C8–$C0C9 | 2 | `SlotACTargetTable` | Two target values ($06, $02). | `cmp SlotACTargetTable,y` at $CE96 | | $C0CA–$C0CB | 2 | `SlotACAlternateTargetTable` | Two alternate target values ($0A, $0B). | `cmp SlotACAlternateTargetTable,y` at $CEAA | | $C0CC–$C0CD | 2 | `SlotPositionThresholdTable` | A position threshold ($01) and a limit ($B0). | none directly | | $C0CE–$C0D0 | 3 | `Slot90BoundaryTable` | Three boundary values ($20, $40, $7F). | `bit Slot90BoundaryTable` at $C7F7; `cmp Slot90BoundaryTable,y` at $CDA5 and $CE48; `lda Slot90BoundaryTable,y` at $CE4D | | $C0D1–$C0D3 | 3 | `Slot94BoundaryTable` | Three boundary values ($03, $03, $01). | `cmp Slot94BoundaryTable,y` at $CD9C and $CE3E; `lda Slot94BoundaryTable,y` at $CE52 | | $C0D4–$C0D7 | 4 | `SlotTimerReloadTable` | Four timer reload values ($06, $04, $40, $58). | `lda SlotTimerReloadTable,y` at $CE8B | | $C0D8–$C0D9 | 2 | `PairedSlotValueTable` | Two bytes ($48, $48). | none directly | | $C0DA–$C0DD | 4 | `SequencePositionTable` | Four sequence positions ($78, $70, $80, $B0). | `lda SequencePositionTable,x` at $CC00 and $CCCA | | $C0DE–$C0E1 | 4 | `SequenceLimitTable` | Four sequence limits ($37, $3F, $3F, $47). | `lda SequenceLimitTable,x` at $CBD4 and $CC05 | | $C0E2–$C0E4 | 3 | `SequenceParameter02Table` | Three parameters ($B7, $B9, $B9). | `lda SequenceParameter02Table,x` at $CBD9 | | $C0E5–$C0E7 | 3 | `SequenceFlagsTable` | Three flag bytes ($01, $01, $41). | `lda SequenceFlagsTable,x` at $CBDE | | $C0E8–$C0EB | 4 | `SequencePointerOffsetTable` | Four pointer offsets into the sequence area ($04, $0C, $14, $1C). | `lda SequencePointerOffsetTable,x` at $CCD3 | | $C0EC | 1 | `IndexedParameterTable` | One parameter byte ($21). | `lda IndexedParameterTable,y` at $C952 | | $C0ED–$C0F5 | 9 | `IndexedParameterTable2` | Nine parameter bytes ($F2, $23, $43, $22, $32, $22, $8C, $24, $CF). | `lda IndexedParameterTable2,y` at $C958 | | $C0F6–$C110 | 27 | `PpuUpdatePointerLow` | The low bytes of the 27 PPU-update stream pointers. | `lda PpuUpdatePointerLow,x` at $C281 | | $C111–$C12B | 27 | `PpuUpdatePointerHigh` | The matching high bytes. | `lda PpuUpdatePointerHigh,x` at $C286 | | $C12C–$C12F | 4 | `NmiSequenceStepPointerLow` | Four low bytes of the sequence-step pointers ($3B, $77, $A4, $6B). | the sequence-step walker at $CB2C | | $C130–$C133 | 4 | `NmiSequenceStepPointerHigh` | Four high bytes ($CB, $CB, $CB, $CC). | the sequence-step walker at $CB2C | | $C134–$C13B | 8 | `StateSequenceStep2Template` | The eight-byte template the sequence-step-2 handler copies ($00, $FC, $FC, $FC, $79, $04, $72, $22). | `lda StateSequenceStep2Template,y` at $CC3A | | $C13C–$C141 | 6 | `Prepare0300StructureTemplate` | The six-byte header written into the $0300 PPU-update structure ($04, $21, $6F, $01, $00, $00). | `lda Prepare0300StructureTemplate,x` at $C5B2 | | $C142–$C146 | 5 | `StateInitVectorData` | Five bytes of state-initialisation data ($60, $9F, $00, $17, $04). | none directly | | $C147–$C14F | 9 | `Initialize02C0EntryTable` | The nine-byte $02C0 entry template. | the copy at $CD13 | | $C150–$C157 | 8 | `Copy02C0TemplateData` | The eight bytes copied to $02C0–$02C7. | `lda $C150,x` at $CD15 | | $C158–$C15F | 8 | `StateScriptData` | Eight bytes of state script ($07, $23, $78, $04, $1D, $F8, $FE, $FE). | none directly | | $C160–$C173 | 20 | `State03ScriptA`…`State03ScriptD` | Four short state scripts; the bytes are opcode-like (e.g. `$29 $01 $02 $00 $02 $29 $30 $35`). | the state-03 handler at $CB2C | | $C174–$C177 | 4 | `State03ScriptE` | Four more bytes of the same script ($01, $40, $7F, $09). | the state-03 handler at $CB2C | | $C178–$C17E | 7 | `RuntimeOamTemplate` | The seven-byte OAM shadow template. | `lda RuntimeOamTemplate,x` at $C5F2 | | $C17F | 1 | `ReceiveHeaderBase` | The expected first header byte ($E8). | `cmp ReceiveHeaderBase,y` at $C75C | | $C180–$C183 | 4 | `ExpectedReceiveHeader` | The four bytes the serial receive path checks the header against. | the header check at $C757 | ### PPU update and graphics data ($D3D6–$D917) | Address | Size | Name | Structure | Read by | |---|---|---|---|---| | $D3D6–$D46D | 152 | `PpuGraphicsData` | The head of the PPU-update stream: byte streams whose high nibble selects the operation, e.g. `$3F $00`, `$08 $29 $27 $22`, `$01 $1A $26 $33`. | `ProcessPpuUpdateStream` at $D3C1 walks it | | $D46E–$D560 | 243 | `PpuOamRecord01`…`PpuOamRecord13` | Thirteen blocks of four-byte records `[X, Y, tile, attribute]` — e.g. ($30, $00, $23, $D0), ($60, $55, $23, $F0), ($50, $AA, $27, $D0) — the OAM entries the update stream places. | `ProcessPpuUpdateStream` at $D3C1 walks it | | $D4C0–$D4E0 | 33 | `PpuCommandData` | Command bytes for the update stream ($20, $AB, $83, $31, $34, $37, $20, $B4, $83, $33, $36, $39, $20, …). | `ProcessPpuUpdateStream` at $D3C1 walks it | | $D561–$D917 | 951 | `GraphicsAndUiData` | The bulk of the update stream: more command bytes and the tile/attribute runs that paint the course. | `ProcessPpuUpdateStream` at $D3C1 walks it | ### Rider and slot data ($E53D–$E70A) | Address | Size | Name | Structure | Read by | |---|---|---|---|---| | $E53D–$E540 | 4 | `SlotDistanceThresholdTable` | Four distance thresholds. | `cmp SlotDistanceThresholdTable,y` at $E983 | | $E541–$E54D | 13 | `RiderConfigData` | Thirteen bytes of rider configuration. | none directly | | $E54E–$E553 | 6 | `PointerLowTable` | The low bytes of six script pointers. | `lda PointerLowTable,y` at $E80D | | $E554–$E559 | 6 | `RiderPointerHighTable` | The matching high bytes. | `lda RiderPointerHighTable,y` at $E812 | | $E55A–$E56D | 20 | `RiderScriptPointerLow` | The low bytes of the rider-script pointers. | the rider-script walker at $E762 | | $E56E–$E583 | 22 | `RiderScriptPointerHigh` | The matching high bytes. | the rider-script walker at $E762 | | $E584–$E696 | 275 | `RiderScript01`…`RiderScript19` | Nineteen rider movement scripts, 9–29 bytes each: opcode bytes with $00 as the terminator and $FF as a branch target. | `ProcessRiderScriptEntry` at $E762 walks them | | $E697–$E699 | 3 | `RiderInitData` | Three bytes of rider initialisation data. | none directly | | $E69A–$E6A0 | 7 | `RiderSpriteTileTable` | Seven rider sprite tile indices. | none directly | | $E6A1–$E6AC | 12 | `PpuRowDescriptor0`…`PpuRowDescriptor2` | Three four-byte PPU row descriptors. | `lda PpuRowDescriptorsE6A1,x` at $ECE6 | | $E6AD–$E6B6 | 10 | `PpuRowDescriptorData` | Ten further bytes of row-descriptor data. | `lda PpuRowDescriptorData,y` at $E7C5 | | $E6B7–$E6E0 | 42 | `CommandHandlerVectors` | 21 two-byte entries holding command-handler addresses, walked by the command dispatcher at $E794. | `lda CommandHandlerVectorsE6B7,y` at $E796 | | $E6E1–$E6F4 | 20 | `CommandHandlerData1` | Twenty bytes of command-handler parameters. | `lda CommandHandlerData1,y` at $EC8B | | $E6F5–$E70A | 22 | `CommandHandlerData2` | Twenty-two further bytes of command-handler parameters. | none directly | ### Course definition and background patterns ($ED3A–$F4FE) | Address | Size | Name | Structure | Read by | |---|---|---|---|---| | $ED3A–$ED49 | 16 | `RuntimeInitData` | Sixteen bytes of runtime initialisation data ($46, $59, $C8, $D2, $A7, $E0, $ED, $EE, …). | `lda RuntimeInitData,x` at $F501 | | $ED4A–$EE09 | 192 | `RuntimeCommandData` | The runtime command stream: opcode bytes with parameters. | the runtime-entry builder at $E1A3 walks it | | $EE0A–$EFFF | 502 | `CourseDefinitionData1` | The first half of the course definition: command records that place the course objects. | `ProcessCourseDefinition` at $F4FF walks it | | $F000–$F062 | 99 | `CourseDefinitionData2` | The second half of the course definition. | `ProcessCourseDefinition` at $F4FF walks it | | $F063–$F086 | 36 | `CourseStructurePointerLow` | The low bytes of the 36 course-structure pointers. | `LoadCourseStructurePointer` at $F82E | | $F087–$F0AA | 36 | `CourseStructurePointerHigh` | The matching high bytes. | `LoadCourseStructurePointer` at $F82E | | $F0AB–$F0B2 | 8 | `CourseTilePattern01` | Three-byte records `[count, tile, attribute]` — ($09, $3B, $3C), ($3D, $3B, $3C), ($3D, $00, $08) — the background tile runs of one course. | `ProcessCourseDefinition` at $F4FF walks the pattern area | | $F0B3–$F100 | 78 | `CourseStructureTemplate` | The course-structure template: the same three-byte record shape with longer runs, e.g. ($08, $D7, $40) repeated eight times. | the course-structure builder at $F64E | | $F101–$F109 | 9 | `CourseStructureTemplate2` | Nine more bytes of the same template. | the course-structure builder at $F64E | | $F10A–$F4FE | 1,013 | `CourseTilePattern02`…`CourseTilePattern33` | Thirty-two more runs of three-byte `[count, tile, attribute]` records — the background tile patterns of the remaining courses. The ROM gives no way to tell one course's pattern from another's, so the runs are numbered in address order. | `ProcessCourseDefinition` at $F4FF walks the pattern area | ### Audio ($F9A3–$FFF9) | Address | Size | Name | Structure | Read by | |---|---|---|---|---| | $F9A3–$F9B2 | 16 | `Pulse1EffectTable` | Sixteen bytes of pulse-1 effect parameters. | the pulse-1 updater at $F8D4 | | $F9B3–$FA4E | 156 | `AudioEffectData` | The sound-effect parameter block: per-effect note, volume and envelope bytes. | `adc AudioEffectDataF9B3,y` at $FAE4 | | $FB77–$FBFE | 136 | `NoiseAudioData` | The noise-channel period and envelope table. | the noise updater at $FC3D | | $FC89–$FC8C | 4 | `AudioControlTable` | Four control bytes for the sound-effect updater. | `ldy AudioControlTable,x` at $FD14 | | $FDD3–$FDFB | 41 | `SoundEffectDescriptorData` | The sound-effect descriptors: voice, period and envelope bytes. | `lda SoundEffectDescriptorData,y` at $FCA2 | | $FDFC–$FEFF | 260 | `AudioSequence01`…`AudioSequence04` | Four music sequences: opcode bytes with note and duration parameters. | `DecodeAudioEvent` at $FF53 walks them | | $FF00–$FF3D | 62 | `ApuPeriodTable` | 31 little-endian 16-bit APU period values — the note pitch table. | `lda ApuPeriodTable,y` at $F87C | | $FF3E–$FF4D | 16 | `AudioMaskTable` | Sixteen mask bytes used by the audio sequence decoder. | `lda AudioMaskTable,y` at $FF60 | | $FF4E–$FF52 | 5 | `AudioLookupTable` | Five lookup bytes used by the audio sequence decoder. | the decoder at $FF53 | | $FF64–$FFE9 | 134 | `AudioSequenceData` | The audio data stream the decoder walks (the bytes the sound-effect and music routines share). | `DecodeAudioEvent` at $FF53 | | $FFEA–$FFF9 | 16 | `AudioSequenceData2` | Sixteen further bytes of the same stream. | `DecodeAudioEvent` at $FF53 | | $FFFA–$FFFF | 6 | `InterruptVectors` | The three interrupt vectors. | the CPU | ### What the 2026-10-21 pass changed 133 labels in this project still carried an address-derived name from the first automated pass (`DataC0B0`, `ScriptE584`, `PpuRecordDataD46E`, `CoursePatternF112`, `AudioDataFF64`, …). Each was given a name that says what the bytes are — the rider scripts, the course tile patterns, the PPU update stream, the APU period table — and the inventory above records the structure and the reader for every region. No byte was changed: `make` still prints *Byte-identical ROM verified.* ### Still open - The 33 `CourseTilePattern` runs are cut at the boundaries the first pass used, which came from where the tracer stopped rather than from the data itself; a course's pattern probably runs across several of them. - `RiderXPositionTable0`–`2`, `SlotPlacementRecord0`–`1`, `PairedSlotValueTable`, `RiderConfigData`, `RiderInitData` and `StateInitVectorData` are read through computed pointers that this dump does not name; their role is inferred from their neighbours and their byte values, not from a reader. - `State03ScriptA`–`E` and `RiderScript01`–`19` are opcode streams; the interpreters that walk them (`$CB2C` and `$E762`) were not analysed in this pass, so the meaning of each opcode is not recorded. ## 14. Notes - **Build verification:** `make` in this folder produces `reconstructed.nes`, compared whole-ROM against `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. ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry.