Files
disasm/Clu Clu Land (JU)/OVERVIEW.md
T
tmerritt 71ada15de0 Promote Donkey Kong (JU): Pass 3 complete, whole-ROM byte-identical
Pass 3 named the 10 data regions in prg.s, then the project was promoted
out of _wip/ so `make` now proves header + PRG + CHR against the original
ROM instead of only the PRG bank.

Data regions named:
  LevelPtrTable        $C000-$C03B  (60 B)    level header data
  ObjectPtrTable       $C03C-$C043  (8 B)     4 pointer pairs
  LevelDataPtrTable    $C044-$C79D  (1882 B)  level data pointers
  SpriteInitPtrTable   labelled at $C4B0      see residue note
  UnreachedCode_D650   $D650-$D696  (71 B)    unreached code
  UnreachedCode_E144   $E144-$E165  (34 B)    unreached code
  MusicDataTable       $F55B-$FA47  (1261 B)  music/sound data
  APUDataTable         $FAE6-$FB74  (143 B)   APU pulse/noise tables
  SfxDataTable         $FE59-$FFEF  (407 B)   sound-effect data
  VectorTable          $FFF6-$FFFF  (10 B)    padding + NMI/RESET/IRQ

Promotion (per the AGENTS.md checklist):
  - git mv "_wip/Donkey Kong (JU)" "Donkey Kong (JU)"
  - add copilot/rom.nes -> ../Donkey Kong (JU).nes symlink
  - add header.s / chr.s (one-line .incbin of header.bin / chr.bin)
  - linker.cfg gains HEADER ($0000/16 B) and CHR ($0000/8 KB)
  - Makefile switched from `cmp reconstructed_prg.bin prg.bin` to
    `cmp reconstructed.nes rom.nes`
  - no _tools path change (this Makefile never referenced _tools/)
    and no `make regen` pipeline exists for it

Doc corrections:
  - project OVERVIEW.md said Mirroring Vertical; header byte 6 = $00
    means Horizontal (the root index was right) — fixed
  - root OVERVIEW.md: row 18 now BYTE-IDENTICAL, tier paragraph and
    summary table refreshed (they had drifted around the Lunar Pool and
    Urban Champion promotions), new per-project detail entry
  - counts corrected to 19 top-level / 17 _wip in OVERVIEW.md,
    TODO.md and AGENTS.md

Residue recorded rather than guessed:
  - Pass 2 is partial: 919 Label_ definitions remain (214 semantic
    labels applied); noted in TODO.md so nobody reads the promotion as
    full semantic naming
  - SpriteInitPtrTable's label sits at $C4B0: the routine at $C807 reads
    $C4A7,x/$C4A8,x, but that table starts mid-row and a label there
    would split the canonical 16-byte audit-comment line

Verification:
  make clean && make  -> "Byte-identical ROM verified."
  verify_pass1.py     -> 19/19 top-level build, Donkey Kong (JU) OK
                         (6014 instr / 12505 B), all comments canonical

Doc-drift sweep: build tiers, mirroring, and stale status tails

Reconciled the docs against verified reality after the Lunar Pool / Urban
Champion / Donkey Kong (JU) promotions and the 2026-10-19 audit-comment
standardization. No source behaviour changed except the two build fixes below.

Build tier (whole-ROM vs PRG-only)
  - AGENTS.md: the 17 `_wip/` projects are real disassemblies at the PRG-build
    tier, not "skeletons"; header.s/chr.s/rom.nes are promotion additions;
    verify_pass1.py covers the top-level projects.
  - Root OVERVIEW.md: `_wip` prose, audit-comment narrative and summary table
    refreshed (37.5% figure, SEEDED vs OK wording, re-trace false alarm).
  - Promoted projects whose build blocks still claimed `make` emits
    `reconstructed_prg.bin` compared to `prg.bin` (Balloon Fight, Baseball,
    CIRCUS, Clu Clu Land, Excitebike, NES PowerPad, Pac-Man (USA), Tennis):
    now describe the whole-ROM `reconstructed.nes` vs `rom.nes`.
  - Lunar Pool / Urban Champion / CIRCUS also had stale `cd "_wip/..."`
    build paths and "Promotion" items; retargeted to their top-level folders.

Mirroring (header byte 6 bit 0: 0 = Horizontal, 1 = Vertical)
  - Corrected DK Jr. prose "horizontal mirroring (flag 6 = $01)" -> vertical,
    and both `_wip/Pac-Man` variant notes "Both use vertical mirroring" ->
    horizontal (both are byte-6 = $00).
  - Re-audited every project OVERVIEW: 0 rows and 0 prose claims now disagree.

Stale status tails
  - Retitled/restatused the `_wip` OVERVIEWs to their real `prg.s` (Golf,
    Ice Climber (U), Lode Runner, Millipede, Pac-Man Tengen, Pac-Man (U),
    Popeye 0/1, Raid, Raid a2, Hogan's Alley, Mario Bros); Golf's
    skeleton-era contents rows and Next Steps corrected.
  - Dropped the skeleton-era "## Next Steps (per `PROMPTS.md`)" heading and
    the "(from the skeleton)" wording across all project OVERVIEWs.
  - Rewrote Lunar Pool / Urban Champion Next Steps (both already Pass 1-3
    complete and promoted; their tails still described pending Pass 2 and
    promotion); fixed root row 13 (Wild Gunman) and row 20 (Duck Hunt).

Also lands two code fixes carried with this sweep:
  - `_wip/Hogan's Alley (JU)`: $E9F1-$E9FD and $EA13-$EA14 reclassified as
    data with canonical audit comments (the seeded build had decoded a data
    table base as code, breaking the build since 2ae09f7). `make` ->
    BYTE-IDENTICAL again; reconstructed_prg.bin/.lbl refreshed.
  - `_wip/Pyramid (U)`: `Label_D72E` -> `NMI:` (byte-identical).

Verification:
  make (Hogan's Alley, Pyramid)      -> BYTE-IDENTICAL, BYTE-IDENTICAL
  _tools/verify_pass1.py             -> 19/19 build, all canonical,
                                        "No truncated or unfaithful Pass 1"
  mirroring audit (rows + prose)     -> 0 mismatches

Donkey Kong (JU): complete Pass 2 — all 919 Label_ definitions named

The last non-zero Label_ residue in the repo is now gone. Every one of the
919 address-derived definitions in copilot/prg.s is replaced:

* 319 routine entry points (jsr/jmp targets):
  - 102 named from observed behaviour (LoadSpriteInitPointerToZp00 $C807,
    CopyVramDataToBuffer $CBDF, CheckSpriteListCollision $D8AD,
    UpdateRandomSeed $F4ED, ReadControllers $F50E, UpdateSoundEngine $FA48, ...)
  - 217 left as SubC<addr> — honest residue where the body is too
    context-dependent to name without guessing (Golf: 108, Mario Bros: 223)
  - the 77 already-named entries untouched
* 600 internal branch targets named <Routine>_Block / <Routine>_Block_n in
  address order within their owning routine (DK Jr./Balloon Fight convention).
  Ownership is control-flow containment, so a loop that physically precedes
  its own entry lands correctly (FlushPpuBuffer_Block $F1EC under $F228).
* Two proposal collisions disambiguated by indexing zero-page slot:
  UpdateObjectListZp5D/DispatchObjectStateZp5E ($DA16/$DA4C) vs
  UpdateObjectListZpAE/DispatchObjectStateZpAF ($E1E9/$E250).

jsr/jmp operands stay numeric (Wild Gunman style); only branch operands are
symbolic. Applied with _tools/pass2.py apply (919 defs + 2,622 refs rewritten,
comments re-aligned); the ROM still rebuilds byte-identically and
verify_pass1.py reports 19/19 canonical with no truncated split.

Docs synced: project OVERVIEW.md gains a Pass 2 Details section, root
OVERVIEW.md rows + conventions, TODO.md section 4 closed. dk_full_map.tsv now
records the complete applied map (its zp rows remain unapplied proposals).

_wip status correction: five false "Pass 2 complete" claims + map audit

The `_wip` tier's "Pass 2 complete" headers counted their draft maps' rows, not
the names actually present in `prg.s`, and described the count of *neutral*
`ZpXX` slots in `locals.inc` as "zero-page semantic aliases". Verified against
the sources and corrected in the root inventory rows, the project OVERVIEWs and
TODO:

  project                named/total  Label_ left  drafted map
  Pinball (JU)           180 / 681    501          210 rows, 179 applied
  Ice Climber (JE)        76 / 973    897          255 rows,  76 applied
  Donkey Kong Jr. Math    64 / 1035   971          243 rows,  62 applied
  Duck Hunt (JUE)         31 / 472    441          272 rows,  31 applied
  Ice Climber (U)          2 / 973    971          none - Pass 2 not started

No `_wip` project is Pass 2 complete. Ice Climber (U) was the worst case: the
2026-10-19 sweep itself restated it "Pass 2 complete" while describing its 971
`Label_` definitions in the same paragraph - a copy-paste from (JE); (U) has no
map file at all.

Drafted maps audited row by row against the committed split: they are not
applicable. 100/243 (DK Jr. Math), 96/255 (Ice Climber (JE)) and 17/210
(Pinball) rows point at addresses that are not instruction boundaries - they
were built against an older code/data split - while most of the rest are
unlabelled boundaries that no operand references, or (Duck Hunt) competing
candidate names for one label (`Label_C2BF` alone carries 205). Rows that are
both insertable and corroborated by control flow: 0 in every map. Recorded as
stale and kept for provenance; Pass 2 here needs a fresh map derived from the
committed split.

Also corrected while verifying:
- Mario Bros entry counts (docs said 252 / 90 + 165 / 477 sites / 156 neutral
  slots; the file holds 262 distinct `jsr`/`jmp` targets = 39 semantic + 223
  `SubC<addr>`, 478 sites, 151 slots).
- root `_wip` prose line/label ranges (4,482-17,180 lines, 192-971 `Label_`,
  11,126 total) and the Ice Climber code figure (74.6% / 6,112 instr, not
  74.7% / 6,113).
- TODO §4 re-scoped to the promoted tier, with the `_wip` residue table added.

Verification: `make` across all 17 `_wip` projects -> BYTE-IDENTICAL (17/17);
`_tools/verify_pass1.py` -> 19/19 build, all canonical, "No truncated or
unfaithful Pass 1 found". No source bytes changed.

Balloon Fight (JU): apply locals.inc/symbols.inc names to prg.s (Pass 3)

Substitutes symbol names for 922 numeric operands: 125 hardware registers
(symbols.inc) and 797 of 813 zero-page/RAM operands (locals.inc). The 15
multi-alias addresses were resolved per routine; 16 operands stay numeric
because $13 is used there as a plain high-byte scratch beside Temp12 and
none of its three aliases fits.

- _tools/apply_symbols.py: the substitution tool (dry run by default;
  --sites supplies per-routine choices for multi-alias addresses, --report
  lists the residue). Only the operand token is rewritten, so the
  `; CPU:$.... PRG:....  XX XX` audit comments survive byte for byte.
- prg.s: operand tokens only. make prints "Byte-identical ROM verified."
- OVERVIEW.md (project): instruction quotes now match prg.s, new
  alias-reuse table, and section 10 rewritten from the stale "497 Label_"
  prose to the real naming state and the 16-site residue.
- OVERVIEW.md (root), TODO.md, AGENTS.md: status, counts, tool list.

verify_pass1.py: 19/19 OK, all audit comments canonical.

Bomberman (U): apply locals.inc/symbols.inc names to prg.s (Pass 3)

Substitutes 362 zero-page operands in copilot/prg.s with the names already
declared in copilot/locals.inc: 354 from single-name addresses and 8 from the
two multi-alias addresses, resolved per site:

  $E0/$E1  IndirectPtrLo/Hi in the RAM-clear loop
              (InitializeSystemAndRunMainLoop -> Main_WriteSoundDispatchIndex_C317,
              which writes $00 across $0100-$07FF via (ptr),y),
              SoundDelay0 (the base of the per-channel delay table, indexed by X)
              in the sound engine;

No symbols.inc substitution was needed: copilot/prg.s contains no numeric
hardware operands ($2000-$4017) at all. All 362 operands were already written
with two hex digits, so no ca65 `a:` prefix was required.

make -> Byte-identical ROM verified. Operand token only; mnemonics, spacing and
the trailing `; CPU:$.... PRG:....  XX XX` audit comments are untouched.

Remaining numeric operands (758) address bytes that no .inc file names:
505 zero-page (133 addresses), 171 RAM $0100-$07FF (46 addresses) and 82
table reads in $8000+ (54 addresses).

Bomberman (U): substitute 24 in-file table labels at ROM operand sites

Uses the new `--labels` option of `apply_symbols.py` with the
ld65 `-Ln` output (`reconstructed.lbl`). Only labels that prg.s
actually defines and that live in the PRG (+) are considered,
so CHR/header segment labels cannot leak in.

24 numeric operands (`lda ,x`, `adc ,y`, etc.) now read
`lda SystemPaletteData,x`, `adc GameplayObjectClassTable,y`, etc.
All 24 point at exact label addresses; the remaining 58 ROM-space
numeric operands point at interior bytes or unlabelled tables and
stay numeric.

make -> Byte-identical ROM verified.

Bomberman (U): update docs for Pass 3 symbol application

- Project OVERVIEW.md: corrected source file from main.s to prg.s/header.s/chr.s;
  output name from reconstructed_bomberman.nes to reconstructed.nes;
  rewritten §10 Zero-Page/RAM Symbols table to match locals.inc (with aliases
  and unnamed residue documented);
  fixed §4 step 1/4 to use IndirectPtrLo/Hi and SoundDelay0 names;
  fixed §11 InesHeader location (now header.s);
  added §2 Pass 3 summary table;
  added §13 notes on legacy main.s and Main_WriteSoundDispatchIndex_C317 misnomer.

- Root OVERVIEW.md §3: updated source line count (7,553), file list, and added
  Pass 3 summary.

Both builds verified byte-identical.

Pass 3 symbol application to all 19 top-level projects (2026-10-19)

Ran _tools/apply_symbols.py --write on all 19 promoted projects to substitute
symbolic names from symbols.inc (hardware registers) and locals.inc (zero-page
RAM aliases, forward references) into prg.s operands. The tool preserves the
canonical audit comments (; CPU:$XXXX PRG:XXXX AA BB) and preserves byte
identity — all projects still rebuild byte-identical to their original ROMs.

Per-project operand substitutions:
  Baseball (UE)             121  (85 single + 36 hardware)
  Tennis (JU)                18  (18 single)
  Pac-Man (USA) (Namco)     524  (524 single; 727 multi-alias residues)
  CIRCUS                    118  (38 single + 80 hardware; 23 multi-alias residues)
  Wild Gunman (U)            76  (11 single + 65 hardware)
  Joypad Test Cartridge (U)  47  (47 hardware)
  NES PowerPad Test Card (U) 89  (89 hardware)
  MUSCLE (U)                220  (91 single + 129 hardware)
  Lunar Pool (U)             45  (18 single + 27 hardware)
  Donkey Kong (JU)           75  (17 single + 58 hardware)
  Urban Champion (JU)        70  (4 single + 66 hardware)

Already had symbols applied: Balloon Fight, Bomberman, Burger Time,
Clu Clu Land, Defender 2, Donkey Kong 3, Donkey Kong Jr.

Skipped: Excitebike (JU) — inline prg.s definitions conflict with locals.inc
values and source operands don't match ROM byte encodings; automatic
substitution breaks byte identity.

Documentation updated:
  OVERVIEW.md — added Pass 3 symbol notes to all 19 project entries,
  added Urban Champion as section #36
  TODO.md — current state now reflects complete symbol application across
  the promoted tier

Verification: python3 _tools/verify_pass1.py reports all 19 projects OK/SEEDED,
all canonical comments, no truncated or unfaithful Pass 1.
2026-10-19 14:55:08 -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.nes (whole ROM: 16-byte header + 16,384-byte PRG + 8,192-byte CHR)

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.nes matches rom.nes exactly (header + PRG + CHR).


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.nes, compared whole-ROM against 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.