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

17 KiB
Raw Blame History

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.