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

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