Moves CIRCUS from _wip/CIRCUS to CIRCUS/ with full ROM build: - Adds header.s, chr.s, linker.cfg (HEADER+PRG+CHR segments) - Updates Makefile to build and verify reconstructed.nes vs rom.nes - Adds rom.nes copy + copilot/rom.nes symlink per repo convention - Verified byte-identical build Updates root OVERVIEW.md: - Removes CIRCUS from _wip skeleton table (25 -> 23 skeletons) - Adds CIRCUS as #12 in numbered inventory (BYTE-IDENTICAL, Pass 2/3) - Updates summary count from 11 to 12 projects
14 KiB
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 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 |
| 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 (~12,755 lines) |
| Output | reconstructed_prg.bin (16,384 bytes) |
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 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 CIRCUS.NES.
3. Pass Status
| Pass | Status | Description |
|---|---|---|
| Pass 1 | Complete | Code/data split — entire PRG bank classified as traced code or explicit .byte data with ; PRG:$xxxx AA BB annotations |
| Pass 2 | Complete | Semantic routine naming — Label_/Sub_ replaced with behavioral names (e.g. RunMainStateDispatcher, UpdateMenuState, ReadControllers); locals.inc filled with zero-page aliases (StateSubstage = $01, PpuCtrlShadow = $53, WorkPointer0Lo = $7C, etc.) |
| Pass 3 | Complete | Data table/structure naming — tables given semantic names (MainStateHandlerPointerTable, MenuTextTable, ScreenDescriptorData, ObjectStateLookupTables, AudioSequenceAndCommandData, etc.) |
4. Reset — CPU $FFFC → $C000
Reset ($C000)
On power-on or reset:
CLD— clear decimal mode;SEI— disable maskable interrupts.- Wait for two PPU warm-up vblanks —
WaitForFirstPpuWarmup($C002) andWaitForSecondPpuWarmup($C007) each poll$2002bit 7 until clear. JSR $C0B8(InitializePpuControl) — write$08to PPUCTRL,$00to PPUMASK.LDX #$FF / TXS— initialize stack pointer to $FF.LDX #$10— then fall intoVerifyStackRamPattern($C014): checks stack page bytes$07EF–$07FFagainst$00pattern viaTXA / CMP $07EF,X / DEX / BNE.ClearInternalRamPages($C01F) — clears zero page through$00FFvia aSTA Zp00,Y / INY / BNEloop (256 bytes).ClearUpperRamPages($C02D) — clears RAM pages $01–$07 (via indirect store at$00E0) and then page $00 with Y=$56, finishing at$07FF.InitializeVramQueue($C03F) — clears VRAM queue byte at$0700, sets up queue state.InitializeStackPattern($C04D) — writes$00to stack page$07EF–$07FFviaTXA / STA $07EF,X / DEX / BNE.JSR $C3B8— early initialization subroutine.JMP $C057(EnterMainLoop) — jumps tojmp $C057self-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:
- Save registers (
PHA / TXA / PHA / TYA / PHA). - Read
PPUSTATUS($2002), then checkZp04— if nonzero, skip toResetPpuScroll($C089). - Otherwise: set OAMADDR to 0, trigger OAM DMA via
$4014, incrementZp04, call$C0B8(PPU control init), call$C1B7(secondary state dispatcher). - Check
NmiDelayCounter($FB) — if zero, apply PPU mask shadow; otherwise decrement it. ApplyPpuMaskShadow($C082) — writesPpuMaskShadow($54) to PPUMASK ($2001).FinishNmiPpuUpdates($C084) — decrementsZp04.ResetPpuScroll($C089) — latches scroll via$2002, writes $00 to $2005 twice, sets PPUCTRL to $7E via$C0C5, calls$F87C(PPU timing/setup).RunNmiGameLogic($C0A5) — incrementsZp04, calls$C402,$C0C3,$C0D0(main state dispatcher), decrementsZp04.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:
- Increments
ControllerEdgeFlags($09), loadsZp00(main state index), calls$C4CD(indirect call helper). - Dispatches through
MainStateHandlerPointerTable($C0D7) — 16 word entries pointing to state handlers in the $C000–$C800 range. - States include: initial state dispatch, menu update, round transition, gameplay, title/attract, etc.
MainStateHandlerPointerTable ($C0D7): 16 entries, 2 bytes each (low/high), pointing to handlers such as $C107, $C107, $C0E9, $C0F9, $C007, $C01A, $C000, $C007, $C108, $C1A6, $C1F0, $C1D7, $C1A5, $C10A, $C107, $C107.
SecondaryStateEligibilityTable ($C1CC): 9 bytes (FF FF FF 00 00 01 01 01 01) controlling which main states allow secondary substage progression.
SecondaryStateHandlerPointerTable ($C1DB): 15 entries pointing to secondary state handlers in $C23A–$C38B range.
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 AudioSequenceAndCommandData ($FABB) — a large block at the end of PRG ($FABB–$FFFB) containing audio sequence bytes, command tables, and pitch/period data ($FFC0–$FFFD vectors region). 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 fromRoundParameterTable($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 copilot/locals.inc (161 lines).
9. Data Tables (prg.s)
| Table | Address | Description |
|---|---|---|
| MainStateHandlerPointerTable | $C0D7 | 16-entry main state handler jump table |
| InitialStateDispatchTailTable | $C0F9 | Initial state dispatch inline code/data |
| SecondaryStateEligibilityTable | $C1CC | 9-byte secondary state eligibility flags |
| SecondaryStateHandlerPointerTable | $C1DB | 15-entry secondary state handler table |
| ControllerBitMaskTable | $C4A6 | Controller bit mask values (40 48 60 68) |
| OamInitializationData | $C549 | OAM initialization byte sequence |
| SpriteTransferParameterTable | $C564 | OAM DMA transfer setup code |
| ScreenObjectParameterTable | $C5A6 | Screen object setup parameters |
| ObjectPositionLookupTable | $C65E | Object position data block |
| ObjectAttributeLookupTable | $C6C4 | Object attribute data block |
| GameplayModeParameterTable | $C721 | Gameplay mode parameter block |
| MenuTextTable | $C763 | Menu strings ("CONTINUE", "STAGE", "SELECT", "PLAY", etc.) |
| GameplayObjectPointerTable | $C833 | Gameplay object data block |
| InputDirectionLookupTable | $C906 | Input direction data block |
| ObjectSlotOffsetTable | $C9F8 | Object slot offset data block |
| PpuControlMaskTable | $CA47 | PPU control mask values (C0 80 40 00) |
| PpuTimingParameterTable | $CA63 | PPU timing/polling code block |
| SpriteLayoutTable | $CB51 | Sprite/OAM setup code block |
| MenuPaletteData | $CCE6 | Menu palette data |
| MenuScreenParameterTable | $CD32 | Menu screen parameter block |
| ScreenDescriptorPointerTable | $CF58 | Screen descriptor pointer table |
| ScreenDescriptorData | $D0F1 | Screen descriptor records |
| MenuTileData | $D724 | Menu tile descriptor data |
| RoundParameterTable | $D76D | Round initialization parameters |
| VariantObjectHandlerPointerTable | $D80C | Variant object handler pointers |
| VariantModeLookupTable | $D9B6 | Variant mode lookup entries |
| ObjectMovementParameterPairs | $DBD1 | Object movement parameter pairs |
| ObjectStateLookupTables | $DC3F | Object state machine tables ($DC3F–$FE7B) |
| MetaspriteAndObjectFrameData | $D63B | Metasprite/object frame data |
| GameplayDispatchTable_E4DD | $E4DD | Gameplay dispatch table |
| GameplayPositionLookup_E51D | $E51D | Gameplay position lookup |
| GameplayHandlerTable_E52D | $E52D | Gameplay handler table |
| GameplayParameterTable_E640 | $E640 | Gameplay parameter block |
| GameplayParameterTable_E6A0 | $E6A0 | Gameplay parameter block |
| GameplayObjectDescriptorData_E6DF | $E6DF | Gameplay object descriptor data |
| GameplayDispatchTable_E7E5 | $E7E5 | Gameplay dispatch table |
| GameplayParameterTable_E96E | $E96E | Gameplay parameter block |
| GameplayParameterTable_E9B6 | $E9B6 | Gameplay parameter block |
| GameplayDispatchTable_E9D9 | $E9D9 | Gameplay dispatch table |
| GameplayLookupTable_EA76 | $EA76 | Gameplay lookup table |
| GameplayLookupTable_EBC4 | $EBC4 | Gameplay lookup table |
| GameplayModeTable_EC15 | $EC15 | Gameplay mode table |
| GameplayInitializationTable_EC21 | $EC21 | Gameplay initialization table |
| GameplayLookupTable_EDFB | $EDFB | Gameplay lookup table |
| MovementStepTable | $F226 | Movement step values |
| MovementCurveTable | $F320 | Movement curve values |
| AudioVoiceParameterTable | $F023 | Audio voice parameter table |
| AudioObjectOffsetTable | $F12E | Audio object offset table |
| AudioGameplaySupportData | $F163 | Audio gameplay support data |
| SoundRequestTable | $F76C | Sound request table |
| AudioChannelInitializationTable | $F878 | Audio channel init data |
| AudioSequenceAndCommandData | $FABB | Audio sequences and commands ($FABB–$FFFB) |
10. Build
cd "_wip/CIRCUS/copilot"
make # → ca65 + ld65 + cmp reconstructed_prg.bin prg.bin → "BYTE-IDENTICAL"
11. Notes
- The main loop (
EnterMainLoopat $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.
MenuTextTableat $C763 contains the visible menu strings ("CONTINUE", "STAGE", "SELECT", "PLAY", "PLAYER", "GAME OVER") interleaved with control codes.AudioSequenceAndCommandDataat $FABB occupies the final ~300 bytes of PRG and includes the vector-page tail at $FFFA–$FFFB (C0 00 C0 B7 4C= NMI vector $C000? No — the vectors at $FFFA areC0 00(NMI=$C000?),C0 B7... wait, the actual vectors are at $FFFA-$FFFB in the file. Let me check: line 12754 shows.byte $C0, $00, $C0, $B7, $4Cat CPU:$FFFB PRG:3FFB. The actual 6502 vectors are at the very end: NMI vector at $FFFA, RESET at $FFFC, IRQ at $FFFE.)
12. Next Steps
- Promotion — move to a top-level
CIRCUS/folder, addheader.s/chr.s+ full-ROMmaketarget (as e.g.Burger Time (U)), then join the rootOVERVIEW.mdnumbered inventory. - Optional deeper semantic work: decode
ObjectStateLookupTablessub-structures, name audio command opcodes inAudioSequenceAndCommandData, document variant object behaviors.