# CIRCUS — Game Flow from Power-On This document describes the execution flow of the disassembled CIRCUS PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `prg.s` in this project folder. The rebuild is verified **byte-identical** to the original `prg.bin`. --- ## 1. ROM and Memory Layout | Property | Value | |----------|-------| | Format | iNES | | PRG | 1 × 16 KB (mapped at CPU $C000–$FFFF) | | CHR | 1 × 8 KB | | Mirroring | Vertical | | PRG bank start | CPU $C000 | | Source file | `prg.s` (18,596 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 | $C05A | `NmiHandler` | | RESET | $C000 | `Reset` | | IRQ / BRK | $4CB7 | Outside mapped space — effectively unused | --- ## 2. Build Status `make` in the project folder assembles `header.s`, `prg.s` and `chr.s` with `ca65`, links them with `ld65`, and compares the result against `rom.nes`. **Verification result: BYTE-IDENTICAL** — `reconstructed.nes` matches `rom.nes` exactly (header + PRG + CHR): ``` Byte-identical ROM verified. ``` --- ## 3. Pass Status | Pass | Status | Description | |------|--------|-------------| | Pass 1 | Complete | Code/data split — the whole PRG bank is classified as traced code or explicit `.byte` data with canonical audit comments. The split was redone from a Mesen CDL capture (`circus_gameplay.cdl`), whose jump-target seeds reach the code behind the game's computed dispatch | | Pass 2 | Complete (2026-10-21) | Every `jsr`/`jmp` target that still carried an address-derived `SubC` name was given a semantic name — **258 entry points renamed**, and the 586 internal branch-target children (`SubC_Loop`, `SubC_OnNonzero`, …) were re-based onto their parent's new name. **0 address-derived `jsr`/`jmp` targets remain.** `locals.inc` still holds the generated `ZpXX` aliases with no behavioural names; the zero-page roles are documented in the inventory below instead | | Pass 3 | Data tables done | Every data block is interpreted and listed in **section 9, "Pass 3 data-table inventory"**: 57 tables named (including the four inline handler-pointer tables that the `jsr DispatchHandlerPointerTable` dispatcher reads), and every block that no instruction reads is recorded as unreferenced data or as code the tracer could not reach | ### Entry-point naming (2026-10-21) The 258 `SubC` entry points were re-read against the code that calls them and against the data tables they consume, and each was given a name that says what the ROM shows it is for. Only labels and the comments that name them changed — not one instruction byte — and the whole-ROM `cmp` still reports *Byte-identical ROM verified.* | Area | Evidence in the dump | Names given | |---|---|---| | Computed dispatch | `DispatchHandlerPointerTable` (`$C4CD`) pops the return address off the stack, uses it as the base of the inline pointer table that follows the `jsr`, and jumps through the 16-bit entry the index selects — that is how `MainStateHandlerPointerTable`, `SecondaryStateHandlerPointerTable`, `ActorUpdateHandlerPointerTable` and `ActorActionHandlerPointerTable` are all reached | `DispatchHandlerPointerTable`, `DispatchMainStateHandler`, `DispatchSecondaryState`, `DispatchActorState`, `DispatchActorHandler`, `DispatchObjectAction`, `RunMainStateHandler`, `AdvanceMainState`, `RunActorScreenScript`, `StartActorUpdate`, `StartActorAction`, `StartActorScript`, `StartObjectScreenScript` | | Screen script | `$C4E3` walks a byte-coded screen script: `$FD` = "write N bytes", `$FE` = "new VRAM address", `$FF` = "end"; `SetPpuVramAddress` pushes the address to `PPUADDR` and the bytes go to `PPUDATA` | `RunScreenScript`, `RunScreenScriptRows`, `RunScreenScriptByIndex`, `RunScreenScriptThree`, `RunScreenScriptFive`, `SetPpuVramAddress`, `SetPpuVramAddressOne`, `SetPpuVramAddressTwo`, `ClearVramToAttribute`, `WriteAttributeRun`, `WriteAttributeByte`, `WriteAttributeNibble`, `WriteAttributeBytesHigh`, `WriteAttributeBytesLow`, `WriteAttributeBytesRow`, `ScrollScreenUp`, `ScrollScreenByLine` | | Actors | `$0500`-per-slot records hold an actor's X/Y (`$0501`/`$0502`), its tile, its attribute and its hit box (`$03C8`-`$03CD`); `ActorStateSizeTable` says how many state bytes each of the five actor slots owns | `LoadActorPosition`, `SaveActorCoordinates`, `StoreActorBox`, `ComputeBoxOverlap`, `CheckActorBoxOverlap`…`Five`, `ComputeActorFacing`, `WrapActorCoordinate`, `ClampActorCoordinate`, `PackCoordinateBytes`, `ApplyCoordinateMask`, `ResetActorSlots`, `ResetActorSlotsTwo`, `NextActorSlot`, `PreviousActorSlot`, `SelectActorSubSlot` | | Rings | `UpdateRingActor` runs the ring actor; the ring's screen position is held in `$0593`/`$0594` and its hit is tested against `$0590`/`$0591`/`$0592` | `UpdateRingActor`, `SetRingCoordinate`, `CheckRingHit`, `IsRingInPlay`, `SetActorBoxTargetLow`, `SetActorBoxTargetHigh`, `StoreActorBoxTarget`, `DrawActorBox`, `SetScrollTarget`, `CheckActorHitBox` | | Object script | `$0700` is the OAM shadow the object writer fills; the object script at `$0310`-`$0317` is a byte stream with `$FF` as the end marker | `StartOamSpriteList`, `ProcessOamSpriteList`, `QueueOamSprite`, `ResetSpriteCounter`, `ResetSpriteState`, `ReadObjectScriptByte`, `AdvanceObjectScript`, `StoreObjectScriptValue`, `StoreSpriteAttribute`, `LoadObjectSpriteScript`, `WriteObjectSpriteBytes` | | Audio | `$F80D` takes a sound id, splits it into a channel and a note index, and the `$FB00` region holds the per-channel periods; `UpdateSoundChannels`/`WriteSoundChannelRegisters` write the APU registers | `PlaySoundEffectById`, `PlaySoundEffectPair`, `SetSoundEffectIndex`, `SetSoundScriptIndex`, `SetSoundVramAddress`, `UpdateSoundChannels`, `WriteSoundChannelRegisters`, `ResumeSoundChannel`, `SetSoundChannelCount`, `SetActorSoundState` | | Controller / PPU | `$C402` latches and shifts `$4016`; the PPU writes are `$2000`/`$2001` through the `Zp53` shadow | `ReadControllerButtons`, `CheckControllerState`, `AreAnyButtonsPressed`, `IsCoordinateBusy`, `WritePpuCtrl`, `SetPpuCtrlBit2`, `ClearPpuCtrlBit1`, `ClearPpuCtrlBit2`, `MaskPpuCtrlBits`, `WritePpuCtrlMasked`, `SetPpuMaskValue`, `ResetScrollPosition` | **Residue: none.** `grep -nE "^(SubC|Sub)_?[0-9A-F]{4}" prg.s` returns nothing — every label in the file now carries a semantic name. Where the ROM does not reveal a role, the routine was given a name that states what its instructions do rather than a guess at a game concept (for example `DivideActorValueBy16` for `lsr a` four times, and `MultiplyActorValueBy16` for `asl a` four times). --- ## 4. Reset — CPU $FFFC → $C000 **`Reset`** ($C000) On power-on or reset: 1. `CLD` — clear decimal mode; `SEI` — disable maskable interrupts. 2. **Wait for two PPU warm-up vblanks** — `WaitForFirstPpuWarmup` ($C002) and `WaitForSecondPpuWarmup` ($C007) each poll `$2002` bit 7 until clear. 3. `JSR $C0B8` (`InitializePpuControl`) — write `$08` to PPUCTRL, `$00` to PPUMASK. 4. `LDX #$FF / TXS` — initialize stack pointer to $FF. 5. `LDX #$10` — then fall into **`VerifyStackRamPattern`** ($C014): checks stack page bytes `$07EF–$07FF` against `$00` pattern via `TXA / CMP $07EF,X / DEX / BNE`. 6. **`ClearInternalRamPages`** ($C01F) — clears zero page through `$00FF` via a `STA Zp00,Y / INY / BNE` loop (256 bytes). 7. **`ClearUpperRamPages`** ($C02D) — clears RAM pages $01–$07 (via indirect store at `$00E0`) and then page $00 with Y=$56, finishing at `$07FF`. 8. **`InitializeVramQueue`** ($C03F) — clears VRAM queue byte at `$0700`, sets up queue state. 9. **`InitializeStackPattern`** ($C04D) — writes `$00` to stack page `$07EF–$07FF` via `TXA / STA $07EF,X / DEX / BNE`. 10. `JSR $C3B8` — early initialization subroutine. 11. `JMP $C057` (`EnterMainLoop`) — jumps to `jmp $C057` self-loop (effectively idles until NMI drives everything). **Key point:** Like Donkey Kong Jr. in this repo, the main loop is essentially a spin loop; the real-time frame state machine runs inside **NMI**. --- ## 5. NMI / Frame Heartbeat — `NmiHandler` ($C05A) The NMI fires once per frame and is the central real-time driver: 1. **Save registers** (`PHA / TXA / PHA / TYA / PHA`). 2. Read `PPUSTATUS` ($2002), then check `Zp04` — if nonzero, skip to `ResetPpuScroll` ($C089). 3. Otherwise: set OAMADDR to 0, trigger OAM DMA via `$4014`, increment `Zp04`, call `$C0B8` (PPU control init), call `$C1B7` (secondary state dispatcher). 4. Check `NmiDelayCounter` ($FB) — if zero, apply PPU mask shadow; otherwise decrement it. 5. **`ApplyPpuMaskShadow`** ($C082) — writes `PpuMaskShadow` ($54) to PPUMASK ($2001). 6. **`FinishNmiPpuUpdates`** ($C084) — decrements `Zp04`. 7. **`ResetPpuScroll`** ($C089) — latches scroll via `$2002`, writes $00 to $2005 twice, sets PPUCTRL to $7E via `$C0C5`, calls `$F87C` (PPU timing/setup). 8. **`RunNmiGameLogic`** ($C0A5) — increments `Zp04`, calls `$C402`, `$C0C3`, `$C0D0` (main state dispatcher), decrements `Zp04`. 9. **`RestoreNmiRegisters`** ($C0B2) — `PLA / TAY / PLA / TAX / PLA / RTI`. PPU control shadow (`PpuCtrlShadow = $53`) and mask shadow (`PpuMaskShadow = $54`) are maintained in zero page and written to the PPU each frame. --- ## 6. Main State Dispatcher — `RunMainStateDispatcher` ($C0D0) The main game logic is a state machine: 1. Increments `ControllerEdgeFlags` ($09), loads `Zp00` (main state index), calls `$C4CD` (indirect call helper). 2. Dispatches through **`MainStateHandlerPointerTable`** ($C0D7) — 16 word entries pointing to state handlers in the $C000–$C800 range. 3. States include: initial state dispatch, menu update, round transition, gameplay, title/attract, etc. **`MainStateHandlerPointerTable`** ($C0D7): 6 entries, 2 bytes each (low/high), pointing to the handlers `$C107`, `$C107`, `$C0E9`, `$C0F9`, `$C107` and `$C81A`. It is the inline table that follows the `jsr DispatchHandlerPointerTable` at $C0D4; the dispatcher reads it through the return address it pulls off the stack. **`SecondaryStateActionTable`** ($C1CC): 9 bytes (`FF FF FF 00 00 01 01 01 01`), one per main state, saying what the frame does after the secondary handler has run: `$FF` takes the negative branch to `$C1C8`, `$00` returns, `$01` goes on to `DispatchSecondaryState`. **`SecondaryStateHandlerPointerTable`** ($C1D8): 9 entries pointing to the secondary state handlers `$C1EA`, `$C212`, `$C23A`, `$C25D`, `$C27D`, `$C2F5`, `$C303`, `$C360` and `$C38B`. Like the main table it is the inline block that follows a `jsr DispatchHandlerPointerTable` (at $C1D5). --- ## 7. Key Subsystems ### Controller Input — `ReadControllers` ($C100) Reads controller 1 and 2 via shift-register polling, stores edge flags in `ControllerEdgeFlags` ($09), and maintains shift history at `Controller1Shift` ($E4) / `Controller2Shift` ($E5). Uses **`ControllerBitMaskTable`** ($C4A6: `40 48 60 68`) for bit testing. ### Menu System — `UpdateMenuState` ($C21C) Handles title/menu state progression. Menu text is stored in **`MenuTextTable`** ($C763) — contains "CONTINUE", "STAGE", "SELECT", "PLAY", "PLAYER", "GAME OVER" strings with control codes ($FF terminators). Menu selection tracked at `MenuSelectionIndex` ($0302). ### Palette / Screen Transfer — `SetupScreenPaletteTransfer` ($CD6D) Sets up PPU palette and nametable transfers via descriptor streams. Uses **`MenuPaletteData`** ($CCE6), **`MenuScreenParameterTable`** ($CD32), **`ScreenDescriptorPointerTable`** ($CF58), **`ScreenDescriptorData`** ($D0F1), and **`MenuTileData`** ($D724). Palette RAM address set via `SetPaletteRamAddress` ($CA04). ### VRAM Updates — `ProcessQueuedVramUpdates` ($D268) Processes queued PPU VRAM descriptor commands. Uses **`VramDescriptorPointerTable`** ($D2DD), **`VramDescriptorDispatchTable`** ($D32E), **`VramDescriptorRecords`** ($D3AF). Executed via `ExecuteVramDescriptorStream` ($D330). ### Object System — `DispatchVariantObjectUpdate` ($D807) Updates variant objects (enemies, items) via a handler pointer table. Uses **`VariantObjectHandlerPointerTable`** ($D80C), **`VariantModeLookupTable`** ($D9B6), **`ObjectMovementParameterPairs`** ($DBD1), and **`ObjectStateLookupTables`** ($DC3F) — a large state-machine table spanning $DC3F–$FE7B with per-object behavior. ### Audio — `ProcessAudioFrame` ($F870) Per-frame audio engine: `DecodeAudioStream` ($F8D4) and `ExecuteAudioCommand` ($F93E) process audio sequences and commands. Uses **`SoundSequencePointerTable`** ($FABB) — 7 x 16-bit LE pointers to the sound sequences — and the audio data area at $FADA–$FFFC, which holds the sequence pointers, command streams and pitch/period data. Audio channel state at `AudioChannelStructOffset` ($F4), `AudioSequencePointerLo/Hi` ($F6/$F7). ### Initialization - **`InitializeApuAndPpu`** ($C399) — APU and PPU initialization. - **`InitializeNametablePages`** ($C40F) — clears nametable memory. - **`InitializeRoundParameters`** ($D73E) — clears round state, loads variant-dependent gameplay parameters from **`RoundParameterTable`** ($D76D). - **`CurrentGameVariant`** ($63) — selects current game variant/mode. --- ## 8. Zero-Page Aliases (`locals.inc`) | Alias | Address | Role | |-------|---------|------| | Zp00 | $00 | Main state index / general purpose | | StateSubstage | $01 | Secondary state substage | | StateTimer | $02 | State timer countdown | | Zp04 | $04 | NMI phase / PPU update gate | | PpuCtrlShadow | $53 | PPUCTRL shadow register | | PpuMaskShadow | $54 | PPUMASK shadow register | | CurrentGameVariant | $63 | Current game variant | | WorkPointer0Lo | $7C | Indirect pointer 0 low | | WorkPointer0Hi | $7D | Indirect pointer 0 high | | WorkPointer1Lo | $7E | Indirect pointer 1 low | | WorkPointer1Hi | $7F | Indirect pointer 1 high | | Controller1Shift | $E4 | Controller 1 shift register | | Controller2Shift | $E5 | Controller 2 shift register | | AudioChannelStructOffset | $F4 | Audio channel struct offset | | AudioSequencePointerLo | $F6 | Audio sequence pointer low | | AudioSequencePointerHi | $F7 | Audio sequence pointer high | Full list in `locals.inc` (161 lines). --- ## 9. Pass 3 data-table inventory Every `; ---- data $XXXX-$YYYY ----` region in `prg.s` is listed here, one row per region, with the bytes' structure and the instruction that reads it. A region that is only partly table and partly code says so: the tracer's data blocks are contiguous runs of bytes it never reached, so a block can hold a table, a table *and* unreached code, or code alone. ### How the blocks were classified Three sources of evidence were used: * the **Mesen CDL capture** (`circus_gameplay.cdl`). Its Data flag marks every byte the CPU read without fetching it as an opcode, and its Code flag marks every byte the CPU fetched as an opcode. A block whose bytes are flagged Data is a table; a block whose bytes are flagged Code is code the static tracer could not reach. * the **tracer's own decode**. A block that decodes as a long run of instructions with coherent operands (`jsr`, `lda (zp),y`, branches into the middle of the block) is code, not data. * the **consuming code**. Every instruction whose operand lands inside a block was read, including the indexed forms (`lda $C1B2,x`) the analyzer's own report misses, and the indirect forms. ### The dispatch mechanism `DispatchHandlerPointerTable` ($C4CD) is the game's computed dispatcher. It takes the handler index in A, doubles it, pulls the return address of the `jsr DispatchHandlerPointerTable` that called it, and reads a 16-bit pointer out of the inline table that follows that call: ``` DispatchHandlerPointerTable: asl a / tay / iny ; Y = 2*index + 1 pla / sta ZpE0 / pla / sta ZpE1 ; ZpE0/E1 = the address after the call lda (ZpE0),y / tax / iny / lda (ZpE0),y sta ZpE1 / stx ZpE0 jmp (ZpE0) ``` So a `jsr DispatchHandlerPointerTable` followed by a run of bytes is a **handler pointer table**, and the four such tables in this ROM are named here. The tracer split one of them badly: the table at $D80C has four entries, but the tracer emitted the last two as instructions (`sta $51D8` and `cmp $D95F,y`), because the pointer bytes `$8D $D8` and `$51 $D9` happen to decode as opcodes. ### Inventory | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | `$C0D7-$C0E2` | 12 | `MainStateHandlerPointerTable` | 6 x 16-bit LE handler pointers ($C107, $C107, $C0E9, $C0F9, $C107, $C81A), the inline table that follows `jsr DispatchHandlerPointerTable` at $C0D4; the index is the game-state byte Zp00 | indirectly, by `lda (ZpE0),y` at `$C4D6` and `$C4DA`; the CDL marks $C0D7-$C0DA (entries 0 and 1) as Data | | `$C100-$C106` | 7 | - | unreferenced; decodes as 3 instructions (`ldx $01` / `bne $C107` / `jmp $CA63`) - code the tracer could not reach | not referenced by any instruction | | `$C1B2-$C1B6` | 5 | `ActorStateSizeTable` | 5 x 8-bit counts (`05 05 02 01 04`), one per actor index Zp64 0-4: how many state bytes that actor owns | `lda $C1B2,x` at `$C140`; the value is the loop count that clears the actor's state at $80 (or $90) in `NextActorSlot` | | `$C1CC-$C1D4` | 9 | `SecondaryStateActionTable` | 9 x 8-bit action codes, one per main state (index Zp00): `$FF` = negative, `$00` = zero, `$01` = positive | `lda $C1CC,x` at `$C1BE`; `$00` takes the `beq` to the RTS at `$C1CB`, `$FF` takes the `bmi` to `$C1C8`, `$01` falls through to `jmp DispatchSecondaryState` | | `$C1D8-$C1E9` | 18 | `SecondaryStateHandlerPointerTable` | 9 x 16-bit LE handler pointers ($C1EA, $C212, $C23A, $C25D, $C27D, $C2F5, $C303, $C360, $C38B), the inline table that follows `jsr DispatchHandlerPointerTable` at $C1D5. The tracer cut it at $C1E4 because the pointer bytes `$C3` decode as an undocumented `DCP` opcode | indirectly, by `lda (ZpE0),y` at `$C4D6` and `$C4DA` | | `$C212-$C24F` | 62 | - | unreferenced; decodes as 25 consecutive instructions and the CDL flags 25 of its bytes as Code - code the tracer could not reach. It is the target of entry 2 of the secondary handler table | not referenced by any instruction | | `$C2F5-$C302` | 14 | - | unreferenced; the CDL flags all 14 bytes as Code - code the tracer could not reach (target of entry 5 of the secondary handler table) | not referenced by any instruction | | `$C360-$C388` | 41 | - | unreferenced; decodes as 19 consecutive instructions (`sty Zp67` / `sty Zp66` / `lda $0100` / branches) - code the tracer could not reach (target of entry 7 of the secondary handler table) | not referenced by any instruction | | `$C4A6-$C4A9` | 4 | `ActorPpuControlTable` | 4 x 8-bit PPUCTRL values (`$40 $48 $60 $68`), one per actor index Zp50 & 3: sprite pattern table $1000, VRAM increment +32, BG nametable $2400 | `lda $C4A6,x` at `$C476`; the value is stored in Zp03, the per-actor PPU control byte the PPU update code tests (bit 5 at `$C312`, bits 6/7 at `$C670`/`$C6C8`) and writes to PPUCTRL | | `$C4FF-$C504` | 6 | - | unreferenced; decodes as 3 instructions (`lda #$00` / `beq` / `lda #$FF`) - code the tracer could not reach | not referenced by any instruction | | `$C549-$C555` | 13 | - | unreferenced; decodes as 7 instructions (a copy loop `dey` / `lda (ZpE0),y` / `sta (ZpE2),y` / `iny` / `bne` / `rts`, plus the two `inc` that open the next routine) - code the tracer could not reach | not referenced by any instruction | | `$C5A6-$C5B1` | 12 | `ActorOamSpriteTemplate` (`$C5A6`), `ActorSpriteYTable` (`$C5AE`) | `$C5A6-$C5AD` = 8 bytes = two OAM entries [Y, tile, attribute, X] = ($00, $B0, $00, $4E) and ($00, $B1, $00, $56); `$C5AE-$C5B1` = 4 x 8-bit Y positions ($A8, $B0, $B8, $C0), one per actor index Zp50 | `lda $C5A6,x` at `$C591` copies the template to the OAM shadow at $0200 with X = $07; `lda $C5AE,x` at `$C59C` writes the actor's Y to both sprites at $0200 and $0204 | | `$C65A-$C661` | 8 | `ActorBehaviourPointerTable` | 4 x 16-bit LE pointers to the per-actor update routines ($C606, $C5F5, $C67B, $C662) | not referenced by any instruction - reached through an indirect jump, so no operand names an address inside it | | `$C763-$C819` | 183 | `MenuTextTable` | the game's menu strings as ASCII with control codes (`$FF` = end of string, `$FE` = new line, `$16` = cursor move), each string introduced by a positioning byte: "CONTINUE" ($C763), "HI"/"STAGE" ($C76E), "1P" ($C784), "2P", "!STAGE", "PLAY", "SELECT", ": 1986", "KONAMI", "BY SOFTPRO", "1 PLAYER", "2 PLAYERS", "GAME OVER", "PLAY 1" | not referenced by any instruction - the text is reached through the screen/VRAM transfer engine, whose address is computed at run time | | `$C928-$C990` | 105 | `ActorSpriteRecordPointerTable` | `$C928-$C92F` = 4 x 16-bit LE pointers to the per-actor sprite records that follow ($C932, $C943, $C954, $C96F); the records themselves run from $C932 to $C990 | `lda $C928,x` at `$C90A` and `lda $C929,x` at `$C90F` with X = Zp64 * 2; the pair becomes the Zp7C/Zp7D pointer that `lda (Zp7C),y` at `$C916` reads | | `$C9D3-$C9DC` | 10 | `ActorUpdateHandlerPointerTable` | 5 x 16-bit LE handler pointers ($E532, $D9E9, $EC41, $EF8F, $F163), the inline table that follows `jsr DispatchHandlerPointerTable` at $C9D0; one entry per actor index | indirectly, by `lda (ZpE0),y` at `$C4D6` and `$C4DA` | | `$C9E7-$C9EB` | 5 | `ActorObjectTypeTable` | 5 x 8-bit actor role codes (`07 07 04 02 06`), one per actor index | `lda $C9E7,x` at `$C9DF` with X = Zp64; the value becomes Zp79 and is handed to the sprite update at the routine at $D001 | | `$C9F8-$CA03` | 12 | - | unreferenced; decodes as 6 instructions (two 5-byte routines: `clc` / `clc` / `adc #$10` / `tay` / `rts` and `clc` / `sec` / `sbc #$10` / `tay` / `rts`) - code the tracer could not reach | not referenced by any instruction | | `$CA47-$CA4A` | 4 | `SoundEffectNoteTable` | 4 x 8-bit values (`$C0 $80 $40 $00`) played one at a time to the sound engine | `lda $CA47,x` at `$CA3D`, which loops downwards (`dex` / `bpl`) handing each value to `jsr $F80D` | | `$CB2C-$CB2C` | 1 | - | a single stray byte `$7B` between two instructions; not the start of any instruction in this dump | not referenced by any instruction | | `$CBBC-$CBD9` | 30 | `SoundEffectDefinitionTable` | 3 x 10-byte sound-effect records: [$07][$05][$7A][$20][$02][$05][$7A][$20][$4E][$8F], [$07][$05][$BA][$2E][$02][$05][$BA][$2E][$50][$91], [$04][$03][$FA][$24][$00][$07][$FA][$24][$52][$93] | `lda $CBBC,x` at `$CB80` (non-zero means "this actor has a sound"), then `lda $CBBE,x` .. `lda $CBC5,x` at `$CB8C`-`$CBB6`; the bytes become Zp2E, Zp26, Zp37, Zp30, Zp31, APUPULSE2CTRL, APUTRIANGLECTRL, ZpF9 and the sound id passed to the sound routine at $F80D | | `$CCE6-$CD27` | 66 | `ScreenParameterPointerTable` | `$CCE6-$CCE7` = the 16-bit LE pointer $CCE8; `$CCE8-$CD27` = the 64-byte screen parameter stream it points at | `lda $CCE6` at `$CCB9` and `lda $CCE7` at `$CCBE`; the pair becomes the ZpE4/ZpE5 pointer the screen transfer runs on | | `$CF3F-$CF48` | 10 | `ActorTargetPositionTable` | 5 x 16-bit LE constants ($0A00, $0700, $0800, $1400, $0500), one per actor index | `lda $CF3F,x` at `$CF12` and `lda $CF40,x` at `$CF19` with X = Zp64 * 2; the pair is subtracted from the actor's 16-bit position Zp6A/Zp6B and the low byte of the difference becomes the actor's screen Y | | `$CFB7-$CFCD` | 23 | `ScreenScriptData` | `$CFB7-$CFB8` = the 16-bit LE pointer $CFB9; `$CFB9-$CFCD` = a 21-byte screen script (`10 11 11 11 11 12 $FE 3B 00 00 30 5B 7A $FE 20 21 21 21 21 22 $FF`) | `lda $CFB8` at `$CEBD`/`$CECD` and `lda $CFB7` at `$CEC2`/`$CED2`; the pair becomes the Zp7C/Zp7D script pointer | | `$CFDA-$CFDD` | 4 | `ActorOamSpriteTemplateAlt` | one OAM entry [Y = $26, tile = $5A, attribute = $02, X = $EC] | `lda $CFDA,x` at `$CFD0`, copied to the OAM shadow at $0200 with X = $03 | | `$D158-$D1C3` | 108 | `ObjectScriptPointerTable` | 54 x 16-bit LE pointers, one per object type, to the object's command stream | `lda $D158,y` at `$D037` and `lda $D159,y` at `$D03C` with Y = the object type * 2; the pair becomes the Zp7C/Zp7D pointer that `lda (Zp7C),y` at `$D04B` walks | | `$D2DD-$D2FC` | 32 | `ObjectUpdateRoutinePointerTable` | 16 x 16-bit LE pointers to the per-object-type update routines ($E24D ... $E3AF) | `lda $D2DD,x` at `$D1CD` and `lda $D2DE,x` at `$D1D2` with X = the object type * 2 | | `$D32E-$D341` | 20 | `ScreenStateScriptPointerTable` | 10 x 16-bit LE pointers to the screen-state script records ($D3AA, $D3B9, $D3E1, $D3C8, $D3AA, $D3B9, $D3AA, $D3B9, $D3EA, $D3C8) | `ldy $D32E,x` at `$D342` and `lda $D32E,x` at `$D346`; the pair becomes the Zp7C/Zp7D pointer | | `$D3AA-$D3C8` | 31 | `ScreenStateScriptRecords` | two 14-byte groups of seven 16-bit LE handler pointers ($E086, $E3C4, $E477, $E459, $E463, $E46D, $E428), then a count byte | not referenced by any instruction - reached through `ScreenStateScriptPointerTable` at `$D32E` | | `$D3CA-$D3F2` | 41 | `ScreenStateScriptRecordsAlt` | a run of [3-byte pointer][1-byte value] records ($E3C4/$00, $E46D/$C0, $E43F/$60, $E3C4/$00, $E46D/$C0, $E43F/$02, $E420/$00, $E420/$02, $E420/$E0, $E424/$E0) | not referenced by any instruction - reached through `ScreenStateScriptPointerTable` at `$D32E` | | `$D4C7-$D4CD` | 7 | `ObjectStepSizeTable` | 7 x 8-bit step sizes (`01 02 04 05 08 10 50`), one per object/animation type | `lda $D4C7,y` at `$D4A0` with Y = the object type in $0324; the value becomes the low byte of ZpEA | | `$D53D-$D543` | 7 | `ObjectSpriteTileTable` | 7 x 8-bit CHR tile indices ($A7, $12, $22, $23, $94, $A4, $EF), one per object type | `lda $D53D,x` at `$D51B` with X = the object type in $0324; the value is written to the tile byte of the OAM entry at $0201+$18 | | `$D63B-$D656` | 28 | `ObjectDefinitionPointerTable` | `$D63B-$D640` = three 16-bit LE pointers ($D641, $D645, $D649); `$D641-$D656` = a 22-byte object definition stream | `lda $D63B,y` at `$D5AC` and `lda $D63C,y` at `$D5B1` with Y = the object type * 2; a second pair is loaded from `$D64D`/`$D64E` at `$D5B6`/`$D5BB` | | `$D670-$D713` | 164 | `ScreenLayoutScript` | a 164-byte stream of nametable/sprite placement commands, in the same "opcode + operands" form as the other scripts; the CDL flags all 164 bytes as Data, so the CPU does read it | not referenced by any instruction - no operand names an address inside it | | `$D724-$D76C` | 73 | `RoundSetupScript` | `$D724-$D73D` = a 26-byte command stream ending in `$FF`; `$D73E-$D76C` = unreached code (the routine that clears the round state) | not referenced by any instruction | | `$D7C5-$D806` | 66 | `RingMovementParameterTable` | two parallel tables indexed by the ring index in $03B2: `$D7C5-$D7E6` = 34 x 8-bit parameters and `$D7E7-$D806` = 32 x 8-bit values | `lda $D7C5,y` at `$D785` and `$D796`, `lda $D7E7,y` at `$D78C` and `$D79D` | | `$D80C-$D813` | 4 | `ActorActionHandlerPointerTable` | 4 x 16-bit LE handler pointers ($D814, $D88D, $D951, $D95F), the inline table that follows `jsr DispatchHandlerPointerTable` at $D809. The tracer emitted the last two entries as instructions (`sta $51D8` at $D80E, `cmp $D95F,y` at $D811) because the pointer bytes decode as opcodes | indirectly, by `lda (ZpE0),y` at `$C4D6` and `$C4DA`; the CDL marks $D80C-$D80D as Data and $D814 as executed | | `$D88A-$D8DC` | 83 | - | unreferenced; decodes as 41 consecutive instructions - code the tracer could not reach (the second entry of the handler table at $D80C points into it at $D88D) | not referenced by any instruction | | `$D8E0-$D8E3` | 4 | - | unreferenced; decodes as 2 instructions (`lda #$10` / `sta Zp77`) - code the tracer could not reach | not referenced by any instruction | | `$D951-$D9AA` | 90 | - | unreferenced; decodes as 43 consecutive instructions - code the tracer could not reach (the third and fourth entries of the handler table at $D80C point into it at $D951 and $D95F) | not referenced by any instruction | | `$D9B9-$D9F2` | 58 | `ObjectCoordinatePointerTable` (`$D9BC`) | `$D9B9-$D9BB` = `jmp $F7EE` (code); `$D9BC-$D9C3` = four 16-bit LE coordinate pairs ($0002, $0103, $0201, $0002); `$D9C4-$D9F2` = unreached code | the pair is read by the unreached code at `$D9E4` (`lda $D9BC,y` / `lda $D9BD,y`); no reached instruction names an address inside the block | | `$DBAA-$DBB5` | 12 | `ActorCoordinateTable` (`$DBAA`), `ItemCoordinateTable` (`$DBB2`) | four (X, Y) pairs ($04, $1A), ($04, $1A), ($04, $1C), ($04, $1C) for the actors, then two more ($03, $25) and ($06, $25) for the items | `lda $DBAA,y` at `$DA45` and `lda $DBAB,y` at `$DA4B` with Y = the actor index * 2 (X to $03A2, Y to $03A0); `lda $DBB2,y` at `$DB13` and `lda $DBB3,y` at `$DB19` for the item pair | | `$DC49-$DC49` | 1 | `ObjectTimerConstant` | a single byte `$1A` | `lda $DC49,x` at `$DC1B`, stored in Zp86 | | `$DCF0-$E483` | 1940 | - | unreferenced; a 1,940-byte region of per-object records. The CDL flags 435 of its bytes as Data, so the CPU does read it, but no instruction in this dump names an address inside it and its record layout is not established from this dump | not referenced by any instruction | | `$E4DD-$E4DF` | 3 | `ActorOamSpriteBytes` | 3 bytes ($B0, $02, $03) = the Y, tile and attribute of the sprite at $0218 | `lda $E4DD,y` at `$E4D1`, copied to $0218-$021A | | `$E51D-$E520` | 4 | `ActorOamSpriteTileTable` | 4 x 8-bit CHR tile indices ($02, $36, $13, $37) for the sprite at $0218 | `lda $E51D,y` at `$E516` with Y = Zp9D & 3; the value is written to the tile byte at $0219 | | `$E532-$E561` | 48 | - | unreferenced; the CDL flags all 48 bytes as Code - code the tracer could not reach (the first entry of the handler table at $C9D3 points at it) | not referenced by any instruction | | `$E640-$E64B` | 12 | - | unreferenced; decodes as 5 instructions - code the tracer could not reach | not referenced by any instruction | | `$E6A0-$E6AF` | 16 | `ObjectInitialValueTable` (`$E6A0`), `ObjectInitialStateTable` (`$E6A8`) | two parallel 8-byte tables of initial values: `$E6A0-$E6A7` for Zp70-Zp77 and `$E6A8-$E6AF` for Zp80-Zp87 | `lda $E6A0,y` at `$E667` and `lda $E6A8,y` at `$E66C`, eight bytes at a time (`iny` / `inx` until Y = 8) | | `$E6DF-$E79E` | 192 | `ActorMovementParameterTable` | 192 x 8-bit movement parameters, one per (mode, actor, direction): the low two bits are the direction and bits 4-7 hold a 4-bit parameter (1-15) | `lda $E6DF,y` at `$E6BF` with Y = 16*mode + 4*actor + direction, computed from Zp64 and Zp66 at `$E6B5`-`$E6BE` | | `$E7E5-$E7EA` | 6 | `ObjectStepLimitTable` | 3 x (step, limit) pairs: ($5F, $B0), ($94, $FF), ($FE, $01) | `lda $E7E5,y` at `$E7BF` (the step is added to the counter Zp9A) and `lda $E7E8,y` at `$E7C9` (the limit that ends the movement) | | `$E96E-$E97D` | 16 | `ObjectStateInitialTables` | four parallel 4-byte tables at $E96E, $E972, $E976 and $E97A, each copied into Zp8C-Zp8F | `lda $E96E,y` at `$E905`, `lda $E972,y` at `$E921`, `lda $E976,y` at `$E95E` and `lda $E97A,y` at `$E89D` | | `$E9B6-$E9BB` | 6 | `RoundStateInitialTable` | 6 bytes ($04, $A0, $70, $F6, $00, $22) | `lda $E9B6,y` at `$E980`, copied to $00D1-$00D6 | | `$E9D9-$EABF` | 231 | - | unreferenced; decodes as a long run of instructions - code the tracer could not reach | not referenced by any instruction | | `$EB23-$EB97` | 117 | - | unreferenced; decodes as a long run of instructions - code the tracer could not reach | not referenced by any instruction | | `$EBC4-$EC93` | 208 | `ObjectSpriteRowTable` (`$EBC4`), `ObjectSpriteRowTableAlt` (`$EBC7`) | `$EBC4-$EBC9` = two 3-byte tables of consecutive values: $16, $17, $18 and $13, $14, $15 - the row offsets of a three-row sprite; `$EBCA-$EC93` = unreached code | `lda $EBC4,y` at `$EBB3` and, when the object index Zp79 is below 4, `lda $EBC7,y` at `$EBBC` | | `$ECA1-$ED20` | 128 | - | unreferenced; decodes as a long run of instructions - code the tracer could not reach | not referenced by any instruction | | `$EDFB-$EDFD` | 3 | `ObjectTypeTable` | 3 x 8-bit object type codes ($20, $21, $35) | `lda $EDFB,y` at `$EDE1`, stored in Zp76 | | `$EE5F-$EEC1` | 99 | - | unreferenced; decodes as a long run of instructions - code the tracer could not reach | not referenced by any instruction | | `$EF70-$EF73` | 4 | `ObjectOffsetTable` | 4 x 8-bit offsets ($00, $06, $04, $02) | `lda $EF70,y` at `$EF64` with Y = ($0504,x - 6); the value is added to Zp77 and stored in $03CB | | `$EF8D-$F06D` | 225 | - | unreferenced; a mixed region: `$EF8D-$EF8E` = 2 bytes ($00, $18) that the routine at $EF76 copies into Zp8C/Zp8D, `$EF8F-$F02C` = unreached code, `$F02D-$F06D` = a table of 16-bit LE position values. No reached instruction names an address inside the block | `lda $EF8D,y` at `$EF76` and `$EF7D` (the two bytes only) | | `$F08C-$F187` | 252 | - | unreferenced; decodes as a long run of instructions - code the tracer could not reach | not referenced by any instruction | | `$F1B5-$F1CE` | 26 | `SoundChannelParameterTable` (`$F1B5`), `SoundChannelParameterTableAlt` (`$F1C2`) | two 13-byte parameter tables for the sound engine's per-channel state | `lda $F1B5,y` at `$F196` and `lda $F1C2,y` at `$F19D`; the value is stored in the channel array at Zp7C | | `$F226-$F23E` | 25 | `RingSpawnThresholdTable` | 25 ascending 8-bit thresholds ($00, $00, $00, $01, $03, $05 ... $5C) | `cmp $F226,y` at `$F1F9` searches for the first entry that is not below A; `lda $F22F,y` at `$F208` fetches the value | | `$F320-$F341` | 34 | `ObjectStepTable` | 34 x 8-bit step values ($10, $17, $20 ... $33) | `lda $F320,y` at `$F2FB`, added to the object's position at Zp73/Zp83 | | `$F514-$F51C` | 9 | `BitMaskTable` | 9 x 8-bit masks ($00, $20, $40, $60, $80, $A0, $C0, $E0, $FF), one per bit index | `lda $F514,y` at `$F4F5`, stored in ZpAE | | `$F65B-$F665` | 11 | `ObjectStateTransitionTable` | `$F65B-$F663` = 9 x 8-bit next-state values ($00, $00, $02, $02, $04, $04, $06, $06, $06); `$F664-$F665` = unreached code | `lda $F65B,y` at `$F64B` with Y = the current state ZpAB; the value becomes the new state and is doubled into ZpAD | | `$F744-$F757` | 20 | `ObjectPositionTable` | 5 x (16-bit LE X, 16-bit LE Y) pairs: ($0050, $00D0), ($0150, $01D0), ($0250, $02D0), ($0350, $03D0), ($0450, $FFFF - the end marker) | `lda $F745,y` at `$F6F8` (the Y half; `$FF` ends the list) and `lda $F744,y` at `$F705` (the X half) | | `$F76C-$F7D9` | 110 | `RingParameterTable` | `$F76C-$F76F` = 4 x 8-bit base offsets ($00, $09, $12, $1B); `$F770-$F7D9` = 96 bytes of per-ring parameters, of which `$F7A6-$F7B9` is a descending run of thresholds and `$F7BA-$F7D9` a table of 16-bit LE position values | `lda $F76C,y` at `$F75F`, `lda $F770,y` at `$F766`, `sbc $F7A6,y` at `$F444`, `lda $F7B6,y` at `$F505` and `lda $F7B7,y` at `$F50B` | | `$F878-$F87B` | 4 | `SoundChannelStateOffsetTable` | 4 x 8-bit offsets ($10, $21, $32, $43) into the sound engine's per-channel state | `ldx $F878,y` at `$F81E` with Y = A >> 4 & 3 | | `$FABB-$FAD6` | 28 | `SoundSequencePointerTable` (`$FABB`), `SoundNotePeriodTable` (`$FAD1`) | `$FABB-$FAD0` = 7 x 16-bit LE pointers to the sound sequences; `$FAD1-$FAD6` = 6 x 8-bit note periods | `lda $FABB,y` at `$FA66` and `$FA6C` (the pair becomes the ZpF0/ZpF1 sequence pointer); `lda $FAD1,y` at `$F83D` and `lda $FAD2,y` at `$F844` (the period high and low bytes) | | `$FADA-$FFFC` | 1315 | - | the audio engine's data area: a table of 16-bit LE sequence pointers at $FADA, then sound-effect and music command streams, note/frequency tables and waveform data, ending just before the 6502 vectors at $FFFA | `lda $FEFA,x` at `$C578` (the byte the sound engine uses as a volume/envelope index); the rest of the area is walked by the audio routines | | `$FFFF-$FFFF` | 1 | - | a single stray byte `$4C`; not referenced by any instruction | not referenced by any instruction | ### Notes on the blocks that are not tables - The CDL capture settles the code/data question for the blocks whose bytes the CPU actually touched: `$C212-$C24F`, `$C2F5-$C302`, `$E532-$E561` and `$C5AA-$C5D8`'s neighbours are flagged Code, while `$C763-$C819`, `$D670-$D713`, `$DCF0-$E483`, `$CFB7-$CFCD` and `$CCE6-$CD27` are flagged Data. The blocks the capture never reached at all (the CPU never read or executed a byte in them) are classified from their decode alone, and every one of them decodes as a coherent run of instructions. - The big unreferenced region `$DCF0-$E483` (1,940 bytes) is the one block whose layout is not established. It is recorded here with its ROM provenance rather than given a speculative name. - `$C763-$C819` (the menu text) and `$C65A-$C661` (the actor behaviour pointers) are read through pointers the code computes at run time, so no instruction in this dump names an address inside them; the CDL flags the menu text as Data, which is what a text table must be. --- ## 10. Build ```bash cd "CIRCUS/copilot" make # → ca65 + ld65 + cmp reconstructed.nes rom.nes → "Byte-identical ROM verified." ``` --- ## 11. Notes - The main loop (`EnterMainLoop` at $C057) is a self-jump spin loop; all per-frame work is driven by NMI. - The IRQ vector ($4CB7) points outside mapped PRG space and is never used. - `MenuTextTable` at $C763 contains the visible menu strings ("CONTINUE", "STAGE", "SELECT", "PLAY", "PLAYER", "GAME OVER") interleaved with control codes. - The audio data area runs from $FADA to the vectors at $FFFA: `SoundSequencePointerTable` at $FABB, the note-period table at $FAD1, and then the command streams and waveform data that fill the rest of the bank up to $FFFC. --- ## 12. Next Steps - **Pass 2, routine names** — the routines are still `SubC` apart from `Reset` and `NmiHandler`. The inventory names the *tables* each routine reads, which is the evidence a semantic routine name needs. - **Zero-page aliases** — `locals.inc` still holds the generated `ZpXX` names. The inventory's "read by" column gives the role of the variables the tables feed (`Zp64` = actor number, `Zp50` = actor index, `Zp00` = game state, `Zp63` = game variant, `Zp03` = per-actor PPU control byte), which is where the behavioural names come from. - **The `$DCF0-$E483` region (1,940 bytes)** — the one block whose record layout is not established. It needs the code that walks it (reached through a computed address the tracer cannot follow) before it can be split into named per-object records. - Optional deeper work: decode the audio command opcodes in the `$FADA-$FFFC` audio area, and the "opcode + operands" script format shared by `$CFB7`, `$CBBC`, `$D670` and `$D724`. ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry.