Files
disasm/Clu Clu Land (JU)/OVERVIEW.md
T
tmerritt 9aef410424 Promotes Donkey Kong Jr
Adds remaining NROM as _wip
Update gitignore to not keep object files
Adds AGENTS.md as this is an AI helped project
2026-10-16 13:46:20 -04:00

320 lines
17 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.
# Clu Clu Land (JU) — Game Flow from Power-On
This document describes the execution flow of the disassembled Clu Clu Land (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 | Horizontal |
| PRG bank start | CPU $C000 |
| Source file | `copilot/prg.s` (9,150 lines) |
| Output | `reconstructed_prg.bin` (16,384 bytes) |
### Interrupt vectors (CPU $FFFA–$FFFF)
| Vector | Address | Handler |
|--------|---------|---------|
| NMI | $C1F1 | `NMIHandler` |
| RESET | $C196 | `ResetEntry` |
| IRQ / BRK | $C196 | `IRQHandler` (reuses `ResetEntry`) |
---
## 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 `rom.nes`.
---
## 3. Reset — CPU $FFFC → $C196
**`ResetEntry`** ($C196)
On power-on or reset:
1. `CLD` — clear decimal mode.
2. `LDA #$00 / STA PPUCTRL` — disable PPU rendering/NMI.
3. `SEI` — disable maskable interrupts.
4. `LDX #$FF / TXS` — initialize stack pointer to $FF.
5. **Wait for two VBlanks** — `InterruptPathLoop_C1A2` (`LDX #$02`, then poll `PPUSTATUS` bit 7 twice via `DEX`/`BNE`). This is the PPU warm-up delay.
6. **Clear RAM** — `LDY #$24` (or `$18` if `StartupPreserveFlag0`/$48 or `StartupPreserveFlag1`/$49 is nonzero), then `STA $0000,Y` in a loop clearing zero page + one page. The Y start value ($24 vs $18) is selected by the two preserve flags.
7. `JSR RunGameTaskRoutine_CBA6` ($C1C2) — run an initial game task routine with Y=$07, X=$06.
8. `LDA #$55 / STA FrameShiftRegister0` ($10) — initialize frame shift register 0.
9. `LDA #$12 / STA PPUCTRL / STA ShadowPPUCTRL` ($40) — set PPU control to $12 (NMI enabled, $2000 base) and shadow.
10. `LDA #$06 / STA PPUMASK / STA ShadowPPUMASK` ($41) — enable background + sprites, set mask shadow.
11. `LDA #$0F / STA APUSTATUS / STA $0100` — enable all four APU channels, write to $0100.
12. `JSR RunGameTaskRoutine_C3E4` ($C1DF) — run the C3E4 game task routine.
13. `LDA ShadowPPUCTRL / ORA #$80 / STA ShadowPPUCTRL / STA PPUCTRL` — set NMI enable bit in shadow and PPUCTRL (enables NMI).
14. Falls into **`InterruptPathDispatch_C1EB`** ($C1EB) — the permanent main loop.
---
## 4. Main Loop — `InterruptPathDispatch_C1EB` ($C1EB)
Once NMI is enabled, ResetEntry never returns. It enters an infinite loop:
```
InterruptPathDispatch_C1EB ($C1EB):
JSR EnginePath_F58D ; $C1EB: 20 8D F5 - frame/input processing
JMP InterruptPathDispatch_C1EB ; $C1EE: 4C EB C1 - loop forever
```
The real-time work happens in the **NMI** (see section 5). The main loop only repeatedly calls `EnginePath_F58D` and spins. `EnginePath_F58D` ($F58D) shifts the frame shift registers ($10–$17) right based on controller input bits ($10/$11 bit 2), producing a carry that drives rotation.
---
## 5. NMI / Frame Heartbeat — `NMIHandler` ($C1F1)
The NMI fires once per frame and is the central real-time driver. It:
1. **Save A** (`PHA`) and **save WorkPtr0Lo** ($00) onto the stack.
2. **PPU control**: `LDA ShadowPPUCTRL ($40) / AND #$7F` (clear NMI bit), store back to shadow and $2000 — this **temporarily disables NMI** during the handler to prevent re-entry.
3. **PPU mask**: `LDA ShadowPPUMASK ($41) / AND #$E7` (clear sprite/background enable bits), store to shadow and $2001 — rendering off during updates.
4. **Game state update**: `JSR UpdateGameStateRoutine_C996` ($C207) — core per-frame game state logic.
5. **OAM DMA**: write $00 to OAMADDR ($2003), then $02 to OAMDMA ($4014) — DMA $0200–$02FF to sprite RAM.
6. **Resource/level data**: `JSR ResourceAndLevelData` ($F20B) — PPU command buffer processing / nametable updates.
7. **Object paths**: `JSR ObjectPath_E113` ($E113), then `JSR ObjectPath_DDC1` ($DDC1) — object movement/collision.
8. **Restore PPU**: write ShadowPPUCTRL ($40) to $2000; `LDA ShadowPPUMASK / ORA #$18` (re-enable background + sprites), store to shadow and $2001 — rendering back on.
9. **Audio driver**: `JSR AudioDriverCode` ($F7C1) — process all audio channels.
10. **Engine path**: `JSR EnginePath_F5AE` ($F5AE) — controller strobing + input processing (`ProcessResourceStateRoutine_F5BE`).
11. **Object path**: `JSR ObjectPath_DDC1` ($DDC1) again.
12. **Game task**: `JSR RunGameTaskRoutine_C368` ($C368) — main game task dispatcher.
13. **PPU status poll**: `NMIReadPPUStatus_C234` — read PPUSTATUS (reset address latch).
14. **Re-enable NMI**: `LDA ShadowPPUCTRL / ORA #$80 / STA ShadowPPUCTRL / STA PPUCTRL`.
15. **Restore**: `PLA / STA WorkPtr0Lo ($00)`, `PLA`, `RTI`.
### AudioDriverCode ($F7C1)
Called from NMI. Processes four audio state routines in sequence:
- `ProcessAudioStateRoutine_F898`
- `ProcessAudioStateRoutine_FA2E`
- `ProcessAudioStateRoutine_FACF`
- `ProcessAudioStateRoutine_FB22`
Then clears audio request bytes $FB–$FF, handles the noise channel ($07FE, $4011), and returns.
---
## 6. Game Task Dispatcher — `RunGameTaskRoutine_C368` ($C368)
Called each NMI frame. This is the primary gameplay dispatcher with multiple entry points ($C368, $C3BC, $C3E4, $C402, $C463, $C47A, etc.). It branches on game state variables ($92, $B0, $8A, $8D, $8C, etc.) to:
- `ProcessResourceStateRoutine_F577` — resource processing
- `CoreGameCode` ($C245) — the large central object-processing routine (see section 7)
- `RunGameTaskRoutine_C6BE`, `C7EF`, `C7F3`, `C809`, `C816`, `C81A`, `C824`, `C830`, `C84B` — various sub-tasks
- `ProcessResourceStateRoutine_F40C / F408 / F31A / F77D` — resource/command buffer updates
The dispatcher uses a dense network of `CorePath*` / `UpdateGameState*` / `UpdatePlayerState*` / `RunGameTaskDispatch_*` branch targets driven by zero-page state ($8A–$8F, $90–$93, $A7, $AC–$AF, $B0, etc.).
---
## 7. Core Object Processing — `CoreGameCode` ($C245)
The largest single routine in the bank. Called from the task dispatcher. It processes objects in two main phases:
**Phase 1** ($C245–$C264):
- `JSR RunGameTaskRoutine_CBB9`
- `JSR ObjectPath_D34E`
- `JSR UpdateObjectRecord_D27D`
- Object record reads from `CurrentObjectPtrLo/Hi` ($08/$09): `ReadObjectRecord_DADA`, `ReadObjectRecord_CD7B`, `ReadObjectRecord_D11E`
**Phase 2** ($C264–$C2C2):
- `JSR ObjectPath_D06D`, `RunGameTaskRoutine_CBC6`, `ObjectPath_D34E`, `UpdateObjectRecord_D27D`
- Second set of object record reads
- Long chain of object buffer/update routines: `ObjectPath_D08F`, `ObjectPath_DD10`, `ObjectPath_DC5A`, `UpdateObjectBuffer_DB52`, `ObjectPath_D856`, `ObjectPath_D8BA`, `UpdateObjectBuffer_D64F`, `ObjectPath_D97B`, `UpdateObjectBuffer_D0C8`, `ObjectPath_CFA1`, `ObjectPath_D0B1`, `ProcessObjectStateRoutine_E6F3`, `ObjectPath_DA91`, `UpdateObjectBuffer_DBC7`, `ObjectPath_D0DF`, `ObjectPath_D1D6`, `UpdateObjectBuffer_DD8F`, `UpdateObjectBuffer_D0F6`, `UpdateGameStateRoutine_C47A`, `ObjectPath_D209`, then `RTS`.
**Object loop** (`CorePathLoop_C2C3` at $C2C3):
- `JSR RunGameTaskRoutine_C84B`
- Clears $0080–$009F (32 bytes) via indexed store
- `RoundOrSubstate` ($88) = 0
- Player/game state initialization based on `ContextIndex` ($4A)
---
## 8. PPU Command / Nametable Processing — `ResourceAndLevelData` ($F20B)
Called from NMI. Processes an embedded command stream at $0301:
- Reads PPU address high/low from $0301,Y
- Writes a counted block of bytes to $2007 (PPUDATA)
- Handles $3F-prefixed commands (palette) specially — re-writes PPUADDR
- This is the nametable/palette update pipeline driven during VBlank.
Supporting PPU helpers:
- `SetDefaultScroll` ($FFEB) — writes $08, $00 to PPUSCROLL
- `LoadPalette` style routines via `ProcessResourceStateRoutine_F40C/F408/F31A/F77D`
- `UpdateCommandBufferRoutine_F3A1`
---
## 9. Object System
Object processing is spread across many `ObjectPath_*` and `UpdateObjectBuffer_*` / `UpdateObjectRecord_*` / `ReadObjectRecord_*` / `ProcessObjectState*` routines in the $C000–$EFFF range:
| Group | Address Range | Purpose |
|-------|---------------|---------|
| ObjectPath_D06D / D08F / D0B1 / D0DF / D1D6 / D209 | $Cxxx | Object coordinate/state update |
| ObjectPath_D34E | $CD3E | Object processing core |
| ObjectPath_D856 / D8BA / D97B | $CDxx | Object buffer/rendering |
| ObjectPath_DC5A / DD10 / DDC1 | $CDxx–$DDxx | Object collision/movement |
| ObjectPath_E113 | $E113 | Object entry (called from NMI) |
| UpdateObjectRecord_D27D | $CD27D | Object record maintenance |
| ReadObjectRecord_DADA / CD7B / D11E | $CDxx–$DAxx | Object record reading |
| ProcessObjectStateRoutine_E6F3 / D1CE / D211 / D42C / D44A / D94D / DA26 | $CDxx–$E6xx | Object state machine dispatch |
| UpdateObjectBuffer_DB52 / D64F / DBC7 / DD8F / D0F6 | $CDxx–$DDxx | OAM buffer writes |
| ProcessCurrentObjectRoutine_CBE3 | $CBE3 | Current object processing |
Objects use `CurrentObjectPtrLo/Hi` ($08/$09) and `CurrentObjectIndex` ($0A) for indirect access. Object state arrays live in zero page ($90–$AF, $D0–$D8) and RAM ($0400–$07FF).
---
## 10. Zero-Page / RAM Symbols
| Address | Symbol | Use |
|---------|--------|-----|
| $00–$01 | WorkPtr0Lo/Hi | General indirect pointer (also saved across NMI) |
| $02–$03 | WorkPtr1Lo/Hi | Secondary pointer |
| $04–$05 | DataPtrLo/Hi | Data pointer |
| $06–$07 | ListPtrLo/Hi | List pointer |
| $08–$09 | CurrentObjectPtrLo/Hi | Current object pointer |
| $0A | CurrentObjectIndex | Current object index |
| $0B–$0C | AlternateObjectPtrLo/Hi | Alternate object pointer |
| $0D–$0E | TempDirection / TempClass | Temporary direction/class |
| $0F | LoopCounter | Loop counter |
| $10–$17 | FrameShiftRegister0–7 | Frame shift registers (input/frame state) |
| $20–$23 | CollisionRecordA0–A3 | Collision records |
| $24–$27 | VectorComponent0–3 | Vector components |
| $2C–$2F | TempField2C–2F | Temporary fields |
| $33–$34 | TempField33/34 | Temporary fields |
| $3F | TempField3F | Temporary field |
| $40 | ShadowPPUCTRL | $2000 mirror |
| $41 | ShadowPPUMASK | $2001 mirror |
| $42–$43 | ScrollOrRenderState0/1 | Scroll/render state |
| $44–$47 | ControllerButtonsBase/AltBase | Controller state ($44,$46 indexed) |
| $48–$49 | StartupPreserveFlag0/1 | Startup preserve flags |
| $4A | ContextIndex | Context index |
| $4B | ModeToggleFlag | Mode toggle |
| $4C | ActiveContext | Active context |
| $4D | PendingStateIndex | Pending state index |
| $4E | StateTransitionFlag | State transition flag |
| $4F | InputLockFlag | Input lock flag |
| $50 | GlobalTickDivider | Global tick divider |
| $51–$68 | TimerArrayBase / GameVar* | Timers and gameplay variables |
| $75–$7D | PlayerTimerBase / PlayerTimer1/2 / StateTimer / InputRepeatDelay | Player timers |
| $80–$8F | MatchState80–StateCounter8F | Match/game state (RoundOrSubstate=$88, GameState=$8A, GameSubstate=$8B) |
| $90–$93 | PlayerStateBase / PlayerState1 / CountdownValue / CountdownDivider | Player state |
| $9F | ObjectStateArray9F | Object state array |
| $A0–$A9 | StateVarA0–A9 | State variables |
| $AC–$AF | StateVarAC–AF | State variables |
| $B0 | ToggleStateB0 | Toggle state |
| $B2 | StateVarB2 | State variable |
| $D0–$D8 | ObjectVarD0–D8 | Object variables |
| $DA | ObjectVarDA | Object variable |
| $E9–$EF | ObjectVarE9–EF | Object variables |
| $F0–$F3 | AudioPulse1State–AudioNoiseState | Audio channel states |
| $F4 | AudioPeriodOrEnvelope | Audio period/envelope |
| $F5–$F6 | AudioSequencePtrLo/Hi | Audio sequence pointer |
| $F7 | AudioSequenceCursor | Audio sequence cursor |
| $F8–$F9 | AudioChannelCursor/Index | Audio channel state |
| $FA | AudioSequenceState | Audio sequence state |
| $FB–$FF | AudioRequest0–4 | Audio request flags |
---
## 11. Audio System
**AudioDriverCode** ($F7C1) — NMI-called audio driver:
- Strobes JOY2 ($4017 = $FF) to select noise mode
- Calls four channel processors: $F898, $FA2E, $FACF, $FB22 (pulse 1, pulse 2, triangle, noise)
- Clears audio request flags $FB–$FF
- Noise channel: reads $07FE, applies envelope from AudioNoiseState ($F3), writes to $4011
Audio workspace ($F0–$FF): channel states, sequence pointers/cursors, request flags.
Audio data tables (`AudioDataTables` at $F441 / $FF34): pitch/envelope lookup tables at $FF34–$FF40 and $FF51–$FFF0.
---
## 12. Data Regions
| Label | Address | Description |
|-------|---------|-------------|
| `DeveloperCreditText` | $C000 | "T.SAWANO." encoded credit string |
| `ResourceScriptPointerTable` | $C008 | 16 pointers into upper-PRG script area ($E75D–$EF91, $F117/$F11D) |
| `TitleOrMessageTileString` | $C044 | Encoded title/message tile values |
| `ResourceDescriptorData_C058` | $C058 | Variable-length resource descriptor records |
| `DirectionAndCoordinateTables_C0B4` | $C0B4 | Direction, coordinate, and signed-offset lookup tables |
| `StartupObjectTemplateData_C0E9` | $C0E9 | Startup object/template records |
| `MovementTables` | $C7BD | Movement-related tables |
| `ObjectEngineCodeAndTables` | $CCC8 | Object engine code + embedded tables |
| `AudioDataTables` | $F441 | Audio pitch/envelope tables |
| VectorTable | $FFFA–$FFFF | NMI=$C1F1, RESET=$C196, IRQ=$C196 |
---
## 13. Summary of Execution Flow
```
Power On / Reset
│
▼
ResetEntry ($C196) ───────────────────────────────────┐
│ CLD, disable PPU/NMI, SEI, stack=$FF │
│ wait 2× VBlank (PPU warm-up) │
│ clear RAM (zero page + one page) │
│ JSR RunGameTaskRoutine_CBA6 (init task) │
│ FrameShiftRegister0=$55, PPUCTRL=$12 (NMI on) │
│ PPUMASK=$06 (bg+sprites on), APU=$0F │
│ JSR RunGameTaskRoutine_C3E4 │
│ enable NMI bit in PPUCTRL │
▼ │
InterruptPathDispatch_C1EB ($C1EB) ───────────────────┤
│ │
├─ JSR EnginePath_F58D (frame shift regs) │
└─ JMP InterruptPathDispatch_C1EB ◄─────────────┘ (spin forever)
│
NMI ($C1F1) [once per frame] │
│ │
├─ save A, WorkPtr0Lo ($00) │
├─ PPUCTRL = shadow & #$7F (NMI off during H) │
├─ PPUMASK = shadow & #$E7 (rendering off) │
├─ JSR UpdateGameStateRoutine_C996 ($C996) │
├─ OAM DMA ($0200–$02FF → $4014) │
├─ JSR ResourceAndLevelData ($F20B) [PPU cmds] │
├─ JSR ObjectPath_E113 ($E113) │
├─ JSR ObjectPath_DDC1 ($DDC1) │
├─ PPUCTRL = shadow; PPUMASK = shadow | #$18 │
│ (rendering back on) │
├─ JSR AudioDriverCode ($F7C1) [4 channels] │
├─ JSR EnginePath_F5AE ($F5AE) [controller I/O]│
├─ JSR ObjectPath_DDC1 ($DDC1) [again] │
├─ JSR RunGameTaskRoutine_C368 ($C368) [game task]│
│ │
├─ read PPUSTATUS │
├─ PPUCTRL = shadow | #$80 (re-enable NMI) │
├─ restore A, WorkPtr0Lo ($00) │
└─ RTI │
Game Task Dispatcher ($C368) [from NMI]
├─ branches on $92,$B0,$8A,$8D,$8C,$8F,... │
├─ CoreGameCode ($C245) — main object processor │
├─ many RunGameTaskRoutine_* / ProcessResource* │
└─ returns to NMI (which continues to RTI)
```
---
## 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 $C1F1 and both RESET/IRQ at $C196 (IRQ reuses the reset handler).
- The main loop (`InterruptPathDispatch_C1EB`) is a tight infinite loop calling only `EnginePath_F58D`; all substantive per-frame work is done in the NMI handler.
- NMI temporarily disables itself (`AND #$7F` on PPUCTRL) and rendering (`AND #$E7` on PPUMASK) while it updates PPU state and objects, then re-enables both before RTI — a classic NES VBlank update pattern.
- Many routine labels carry address-based names (`ObjectPath_D34E`, `RunGameTaskRoutine_C368`, `ProcessObjectStateRoutine_E6F3`, etc.) reflecting the automated first-pass disassembly; semantic naming is provisional.
- The disassembly includes extensive inline `.byte` data blocks interleaved with code; boundaries are marked as "conservative Pass-1 classifications."
- `locals.inc` provides verified zero-page names; `symbols.inc` provides the NES hardware register set.