# Tennis (JU) — Game Flow from Power-On This document describes the execution flow of the disassembled Tennis (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` (9,268 lines) | | Output | `reconstructed_prg.bin` (16,384 bytes) | ### Interrupt vectors (CPU $FFFA–$FFFF) | Vector | Address | Handler | |--------|---------|---------| | NMI | $C07D | `NmiHandler` | | RESET | $C000 | `Reset` | | IRQ / BRK | $C000 | `Reset` (reused — 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 (all 16,384 bytes), and `prg.bin` matches the PRG bank of `Tennis (JU).nes`. --- ## 3. Reset — CPU $FFFC → $C000 **`Reset`** ($C000) On power-on or reset: 1. `cld` — clear decimal mode. 2. `sei` — disable maskable interrupts. 3. **Wait for VBlank** — poll `PPUSTATUS` bit 7 (`@Flow_C002`). 4. `PPUCTRL=$00`, `PPUMASK=$00` — disable rendering and NMI. 5. Stack pointer = $FF. 6. `X = FrameCounter` ($27), then `Y=$06`, `WorkPtrHi=$02`, `WorkPtrLo=$00`, `A=$00`. 7. **Clear RAM** — `sta (WorkPtrLo),y` loop decrementing `WorkPtrHi` from $02 down, effectively clearing $0000–$02FF. 8. `X` is 0 at this point; if non-zero (it never is on entry), skip; else `X=$5F`. 9. `stx $0500` — set $0500 to $5F (or 0). 10. `jsr ClearNametables` ($C195) — clear nametable memory. 11. `jsr HideAllSprites` ($C1A5) — hide all sprites by writing $F4 to OAM. 12. `PPUSCROLL=$00` twice; `GameActiveFlag=$01`. 13. `APUSTATUS=$0F` — enable all four APU channels. 14. `PPUCTRL=$90`, `PpuCtrlShadow=$90` — enable NMI, $2000 base. 15. `PpuMaskShadow=$06`. 16. Falls into **`MainLoop`** ($C04F) — the permanent foreground loop. --- ## 4. Main Loop — `MainLoop` ($C04F) After reset, control enters the infinite foreground loop: ``` MainLoop ($C04F): NmiWorkComplete ($30) = $00 if GameActiveFlag ($38) != 0: if GameStateStep ($3E) == 1: jsr RunAudioEngine ($FFF7) Flow_C060: jsr UpdateTimers ($C375) jsr UpdateGameState ($C0AA) NmiUpdatesEnabled ($32) = $01 Flow_C06A: if NmiWorkComplete ($30) != 0: FrameCounter ($27)++ NmiUpdatesEnabled ($32) = $00 jmp MainLoop else: jsr RollRng ($C456) jmp Flow_C06A ``` The main loop is **NMI-synchronized**: it waits for `NmiWorkComplete` ($30) to become non-zero (set by NMI each frame), then advances the frame counter, clears the update-enabled flag, and loops. If NMI work is not complete, it rolls the RNG and retries. --- ## 5. NMI / Frame Heartbeat — `NmiHandler` ($C07D) The NMI fires once per frame: 1. **Save registers** (A, X, Y on stack). 2. **OAM DMA**: write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014) — copy $0200–$02FF to sprite RAM. 3. If `NmiUpdatesEnabled` ($32) == 0, skip to `@Flow_C0A0`. 4. Otherwise: - `jsr WritePalette` ($C1B5) — conditional palette upload based on `PaletteUpdateMode` ($3C). - `jsr WritePpuBuffer` ($C2EC) — flush pending PPU command buffer at $0590. - `jsr SetScroll` ($C3FD) — write scroll from $1E/$1F to PPUSCROLL. - `jsr ReadControllers` ($C2A4) — read both controller ports. 5. `NmiWorkComplete ($30) = $01` — signal main loop that the frame update is done. 6. `NmiTriggerFlag ($33) = $01`. 7. Restore registers, `rti`. The label `IrqHandler` appears at the same `rti` address — IRQ/BRK vector points here, but it's unused (same as Reset). --- ## 5. Game State Machine — `UpdateGameState` ($C0AA) Called each main-loop iteration: ``` UpdateGameState ($C0AA): if GameActiveFlag ($38) == 0: Flow_C0B1: if ControllerHeld ($1A) & START ($10): if StartButtonLatch ($34) != 0 → skip else: StartButtonLatch=$01 Y = GameState ($3D) if GameState == 7 → Flow_C0EA if GameState == 4 → Flow_C0CC if GameState == 6 → skip else → Flow_C0FC Flow_C0FC: DispatchInlineAddressTable ($C38B) with GameState index → GameStateHandlerTable ($C101) else (GameActiveFlag == 0): Flow_C119: if ControllerHeld & $30 (START|SELECT): if bit $10 set → Flow_C134 else: GameActiveFlag=$00, GameState=$00, PendingStateFlag=$00 jsr MatchRoutine_CBCB, jsr DisableRendering GameMode=$SelectedGameMode ($E0), rts Flow_C134: (complex state-transition logic with StateTransitionFlag, GameMode, etc.) Flow_C180: GameStateStep ($3E) → DispatchInlineAddressTable → GameStateStepHandlerTable ($C185) ``` ### `MainStateHandlerTable` ($C101) — indexed by `GameState` ($3D) as a byte offset (even offsets $00–$16): | Offset | Handler | Description | |--------|---------|-------------| | $00 | `GameState00_InitializeMatch` ($C70A) | Match initialization | | $02 | `GameState01_WaitForServe` ($C797) | Wait for serve input | | $04 | `GameState02_PrepareRally` ($C7B7) | Prepare rally | | $06 | `GameState03_RunRally` ($C7C6) | Run rally | | $08 | `GameState04_PointEnded` ($C907) | Point ended | | $0A | `GameState05_UpdateScore` ($CA07) | Update score | | $0C | `GameState06_AdvanceGame` ($C9DE) | Advance game | | $0E | `GameState07_PauseOrTransition` ($C42B) | Pause/transition (RTS) | | $10 | `GameState08_WaitForTimer` ($CA28) | Wait for timer | | $12 | `GameState09_RestorePreviousState` ($CA31) | Restore state | | $14 | `GameState0A_ResumePreviousState` ($CA3A) | Resume state | | $16 | `GameState0B_FinishTransition` ($CA5B) | Finish transition | ### `GameStateStepHandlerTable` ($C185) — indexed by `GameStateStep` ($3E): | Offset | Handler | |--------|---------| | $00 | `ModeStep00_DrawSelectionScreen` ($CA87) | | $02 | `ModeStep01_Countdown` ($CAC9) | | $04 | `ModeStep02_InitializeSelection` ($CADD) | | $06 | `ModeStep03_UpdateSelection` ($CAE4) | | $08 | `ModeStep04_ConfirmSelection` ($CAEE) | | $0A | `ModeStep05_StartDemoOrMatch` ($CAF4) | | $0C | `ModeStep06_CompleteSetup` ($CC04) | ### Key state handlers: - **`GameState00_InitializeMatch`** ($C70A): First-time init — clear nametable, render routine, palette mode, set `PendingStateFlag`. Subsequent calls advance through sub-states ($87/$88 counter), render sprites, enable rendering, set `GameState=$07`, call `MatchRoutine_CBCB`. - **`GameState01_WaitForServe`** ($C797): Disable rendering, clear nametable, render court + score, zero point winners, clear animation flags, set ball trajectory flags, advance `GameState`. - **`GameState02_PrepareRally`** ($C7B7): Enable rendering, clear PPU mask shadow bit 4, hide sprites, render routine, advance `GameState`. - **`GameState03_RunRally`** ($C7C6): Render routine, set `PpuBufferReady`, `MatchRoutine_C7E7` (copy initial object state), `PhysicsRoutine_D7E1`, handle serving player, `EngineRoutine_C436`, advance `GameState`. --- ## 6. NMI-Driven Per-Frame Work The NMI handler (called every VBlank) performs: | Step | Routine | Address | Purpose | |------|---------|---------|---------| | OAM DMA | — | $C084–$C089 | Copy $0200–$02FF to sprite RAM | | Palette update | `WritePalette` | $C1B5 | Conditional palette upload ($3F00/$3F10) | | PPU buffer flush | `WritePpuBuffer` | $C2EC | Write pending PPU commands from $0590 buffer | | Scroll | `SetScroll` | $C3FD | Write scroll X/Y from $1E/$1F | | Controller read | `ReadControllers` | $C2A4 | Read both ports, build held/newly-pressed | The main loop calls `UpdateGameState` ($C0AA) which dispatches to the active game state handler, then advances timers and audio. --- ## 7. Object / Sprite System - **OAM buffer** at $0200–$02FF, written via `WriteMetaspriteToOam` ($C440). - **Object data pointers**: `ObjectDataPointerLo/Hi` ($A8/$A9), `ObjectDataOffset` ($AA), `ActiveObjectIndex` ($AB). - **Animation**: `AnimationFrame` ($B0), `AnimationTimer` ($B1), `PoseFrameTimer` ($B3), `PlayerAnimationId` ($B4), `PoseSequenceOffset` ($B5), `PlayerPoseId` ($B6), `PlayerPoseFlags` ($B7). - **Racket**: `RacketTargetX` ($B8), `RacketTargetY` ($B9), `RacketContactFlags` ($BA), `RacketAimOffset` ($BB). - **Player physics**: `PlayerMotionControl` ($60/$65 for P1/P2), X/Y position + subpixel ($61–$69), target coordinates ($80–$83), action type/state ($84–$86), facing direction ($87), position ($88–$89). - **Ball**: `BallMotionControl` ($6A), X/Y position + subpixel ($6B–$6E), height ($6F/$70), target offset ($71), vertical velocity ($72/$73), state flags ($7A), bounce state ($7B), contact state ($8F), trajectory flags ($8D), collision flags ($8E). --- ## 8. Audio System **`RunAudioEngine`** ($FFF7) — jumps to `AudioEngineUpdate` ($F8E0). Called from MainLoop when `GameStateStep == 1` and `GameActiveFlag != 0`. **`InitializeAudioEngine`** ($E1F8): - Clear audio request pointer ($F0/$F1 = $0006), channel table pointer ($F2/$F3 = $0620), sequence table pointer from ROM $F100/$F101. - `audioWorkValue=$40`, `APUSTATUS=$1F`, `APUFRAMECNT=$C0`. - Clear 16 bytes through ($F0), clear 16 entries at 8-byte stride through ($F2). **`InitializeAudioChannels`** ($EED0): - Clear audio channel flags, scan 16 eight-byte slots through ($F2). - Slots with values $01–$04 activate a channel: copy 4-byte parameters through indirect pointer $F6. **Audio data** at $FF00–$FFF5 (period/mask tables, sequences, instruments), $F450–$F49A (runtime command data), AudioEventSequenceData at $FDFA–$FFEA. Audio RAM symbols ($F0–$FF): `audioRequestPtr`, `audioChannelTablePtr`, `audioSequenceTablePtr`, `audioWorkPtr`, `audioWorkValue`, `audioChannelFlags`, `audioRequestIndex`, `audioChannelOffset`, `audioIndirectPtr`, plus channel-specific effect timers ($F5–$F6), sequence pointers/indices ($F7–$FA), stream position ($FB), event priority ($FC), request flags ($FD–$FF). --- ## 9. Zero-Page / RAM Symbols (key subset) | Address | Symbol | Use | |---------|--------|-----| | $01–$02 | WorkPtrLo/Hi | General indirect pointer | | $09 | PpuCtrlShadow | $2000 mirror | | $0A | PpuMaskShadow | $2001 mirror | | $1A | ControllerHeld | Controller buttons held | | $1B | Zp1B | Previous controller state | | $20 | Zp20 | Timer counter | | $25 | StateTimer | State countdown timer | | $27 | FrameCounter | Frame tick counter | | $30 | NmiWorkComplete | NMI frame-sync flag | | $31 | PpuBufferReady | PPU buffer ready flag | | $32 | NmiUpdatesEnabled | NMI updates enabled flag | | $33 | NmiTriggerFlag | NMI trigger flag | | $34 | StartButtonLatch | Start button debounce latch | | $35 | StateTransitionFlag | State transition flag | | $38 | GameActiveFlag | Game active flag | | $39 | GameMode | Current game mode | | $3C | PaletteUpdateMode | Palette update mode | | $3D | GameState | Main game state | | $3E | GameStateStep | Game state step/substep | | $3F | DemoCounter | Demo counter | | $43 | PendingStateFlag | Pending state flag | | $44 | StateSubstep | State substep | | $46 | PlayerCourtOrder | Player court order | | $48–$4B | ModeAnimTimer/Frame A&B | Mode animation timers/frames | | $4C | ServingPlayer | Serving player | | $4D | ModeAnimSubframe | Animation subframe | | $4E | MessageTimer | Message timer | | $4F | MessagePlayerIndex | Message player index | | $50 | PointWinner | Point winner | | $51–$53 | Player/2 Point/Game Score | Score values | | $57–$5D | Score-related state | Score display/update state | | $5E | PendingGameState | Pending game state | | $60–$6E | Player/Ball motion state | Player and ball physics | | $78–$7F | Animation/ball state | Animation flags, ball state/collision | | $80–$8F | Target/action/collision state | Player targets, action state, collision flags | | $A8–$AA | ObjectDataPointerLo/Hi, Offset | Object data pointers | | $AB | ActiveObjectIndex | Active object index | | $B0–$C9 | Animation/pose/racket state | Animation frames, pose, racket targets | | $CA–$CC | Contact/bounce state | Racket/ball contact state | | $CD–$CF | Movement/action state | Movement speed, computer/action state | | $E0 | SelectedGameMode | Selected game mode | | $E6–$FF | Audio engine state | Audio request pointers, channel flags, sequence state | --- ## 10. PPU / Rendering - **OAM DMA**: Every NMI, DMA from $0200–$02FF via `OAMDMA` ($4014). - **PPU buffer**: `WritePpuBuffer` ($C2EC) writes a command buffer at $0590 to PPU when `PpuBufferReady` ($31) is set. - **Scroll**: `SetScroll` ($C3FD) writes $1E/$1F to PPUSCROLL. - **Palette**: `WritePalette` ($C1B5) conditionally uploads from RAM buffers based on `PaletteUpdateMode` ($3C). - **Palette shadow**: `PpuCtrlShadow` ($09), `PpuMaskShadow` ($0A). - **Sprite hiding**: `HideAllSprites` ($C1A5) writes $F4 to OAM. - **Metasprite rendering**: `WriteMetaspriteToOam` ($C440) writes sprite data from a pointer. --- ## 11. Data Tables | Label | Address | Description | |-------|---------|-------------| | `MainStateHandlerTable` | $C101 | 12 main game state handler pointers | | `GameStateStepHandlerTable` | $C185 | 7 mode step handler pointers | | `GameStateHandlerTable` (game mode) | $CAD7 | 8 game-mode state handler pointers | | `InitialMatchObjectStateData` | $C847 | Initial object state data for match start | | `EngineTable_*` | Various | Inline data tables embedded between code | | `AudioEventSequenceData` | $FDFA | Audio event sequence data | | Audio tables | $FF00–$FFF5 | Period/mask tables, sequences, instruments | --- ## 12. Summary of Execution Flow ``` Power On / Reset │ ▼ Reset ($C000) ───────────────────────────────────────────────────────┐ │ cld, sei, wait VBlank, PPUCTRL/PPUMASK=$00 │ │ stack=$FF, clear RAM $0000–$02FF │ │ $0500=$5F, clear nametables, hide sprites │ │ PPUSCROLL=$00, GameActiveFlag=$01 │ │ APUSTATUS=$0F (all channels on) │ │ PPUCTRL=$90 (NMI on), PpuCtrlShadow=$90 │ │ PpuMaskShadow=$06 │ │ │ ▼ │ MainLoop ($C04F) ─────────────────────────────────────────────────────┤ │ │ ├─ NmiWorkComplete ($30) = $00 │ ├─ if GameActiveFlag && GameStateStep==1: RunAudioEngine ($FFF7) │ ├─ jsr UpdateTimers ($C375) │ ├─ jsr UpdateGameState ($C0AA) ──► state machine dispatch │ │ ├─ State $00: InitializeMatch │ │ ├─ State $01: WaitForServe │ │ ├─ State $02: PrepareRally │ │ ├─ State $03: RunRally │ │ ├─ State $04: PointEnded │ │ ├─ State $05: UpdateScore │ │ ├─ State $06: AdvanceGame │ │ ├─ State $07: PauseOrTransition (RTS) │ │ ├─ State $08–$0B: WaitForTimer / Restore / Resume / Finish │ │ └─ return to MainLoop │ │ │ ├─ NmiUpdatesEnabled ($32) = $01 │ ├─ wait for NmiWorkComplete ($30) != 0 ◄── NMI sets this │ ├─ FrameCounter++, NmiUpdatesEnabled=$00 │ └─ jmp MainLoop │ │ │ │ (if NmiWorkComplete==0: jsr RollRng ($C456), retry) │ │ │ NMI ($C07D) [once per frame] │ │ │ ├─ save A, X, Y │ ├─ OAM DMA ($0200–$02FF → $4014) │ ├─ if NmiUpdatesEnabled: │ │ jsr WritePalette ($C1B5) │ │ jsr WritePpuBuffer ($C2EC) │ │ jsr SetScroll ($C3FD) │ ├─ jsr ReadControllers ($C2A4) → $4D/$4E/$4F │ ├─ NmiWorkComplete ($30) = $01 (unblocks main loop) │ ├─ NmiTriggerFlag ($33) = $01 │ ├─ restore registers, rti │ │ │ IrqHandler ($C17E): rti (unused) │ ``` --- ## 13. Notes - **Build verification:** `make` in `copilot/` produces `reconstructed_prg.bin` that is **byte-identical** to `prg.bin` and to the PRG bank of `Tennis (JU).nes`. - Vectors confirm NMI at $C07D, and both RESET and IRQ at $C000 (the reset handler doubles as the IRQ handler, just a `rti`). - The foreground main loop (`MainLoop` at $C04F) is strictly NMI-synchronized via `NmiWorkComplete` ($30): it loops until NMI sets it to $01, processes game state and timers, then waits again. - `UpdateGameState` ($C0AA) dispatches to one of 12 main game states via `MainStateHandlerTable` at $C101, indexed by `GameState` ($3D). - State $04 contains a nested secondary state machine (`GameStateStepHandlerTable` at $C185, 7 steps) for mode selection/setup. - Many routine labels are address-based (`EngineRoutine_C1DD`, `MatchRoutine_C7E7`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional. - `symbols.inc` provides NES hardware register names and controller masks; `locals.inc` provides extensive zero-page symbol definitions covering player/ball physics, audio, and game state. - The disassembly includes inline data tables (`EngineTable_*`) embedded between code regions, preserved verbatim for byte-identity.