Files
disasm/Donkey Kong Jr. (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

24 KiB
Raw Blame History

Donkey Kong Jr. (JU) — Game Flow from Power-On

This document describes the execution flow of the disassembled Donkey Kong Jr. (JU) PRG bank (16 KB, mapped at CPU $C000–$FFFF), starting from power-on. It is based entirely on copilot/prg.s (13,634 lines) in the repository, originally generated by pass1_trace.py from the interrupt vectors and refined through Pass 2/3 semantic labeling. The rebuild is verified byte-identical to the original rom.nes (make in copilot/ produces reconstructed.nes matching rom.nes exactly).

Listing: project 11 in the top-level OVERVIEW.md index. NROM cartridge (mapper 0), iNES flags: 1 × 16 KB PRG, 1 × 8 KB CHR, vertical mirroring (flag 6 = $01).

ROM SHA-256: 7d5f77878af7646296d994da14b25213741f6e0e0bc5deadd32a406d044a5788

Current label status — all 1,035 top-level labels are semantically named; every data region is emitted as explicit .byte data with its ROM provenance in comments (no .incbin blocks remain), and 688 local Label_ branch targets are routine-internal.


1. Reset Vector — CPU $FFFC → $C66E

ResetHandler ($C66E–$C6B3)

Unlike most projects in this repository (whose reset handlers sit at $C000 or nearby entrypoints such as $C40B), Donkey Kong Jr.'s reset entry is at $C66E. On power-on or reset, the 6502 jumps to $C66E. The reset routine performs cold-start hardware initialization:

Step Code Purpose
1 sei / cld Disable interrupts, clear decimal mode
2 lda #$10 / sta PPUCTRL Disable NMI and PPU rendering immediately
3 ldx #$FF / txs Initialize stack pointer
4 Poll $2002 until bit 7 set Wait for one VBlank period (PPU warm-up)
5–8 Indirect clear loop via $00/$01 pointer Clear all of RAM $0000–$07FF (8 pages, walked downward from $0700)
9 jsr InitializePpuOamAndDmc ($C6B6) PPU, shadow-OAM, and DMC initialization (see §2)
10 lda #$6F / sta TitleCursorY Initialize the title-cursor sprite Y position ($6F)
11 lda #$18 / sta GameModeTimer Set attract-sequence timer (24)
12 lda #$01 / sta PlayerLivesOrModeCount, sta PendingGameMode Arm the pre-game attract path
13 lda #$00 / sta GameMode Set current game mode to 0
14 Toggle $2000 bit 7 via PPUControlShadow Enable NMI — actual execution begins
15 Label_C6B0: jsr AdvanceRandomLfsr / jmp Idle loop; the reset path never returns

Architectural note: After reset, the CPU idles forever (jmp Label_C6B0); the loop body's only activity is jsr AdvanceRandomLfsr to churn the 8-byte PRNG. All game logic — attract screen, titles, gameplay, sound — runs entirely inside the NMI handler, which is the opposite of most NES titles in this collection (where the NMI flag merely paces a foreground main loop).


2. Initialization — InitializePpuOamAndDmc ($C6B6–$C6E5)

Called from ResetHandler:

  1. PPU control: PPUCTRL = $10, saved to PPUControlShadow ($10)
  2. PPU mask: PPUMASK = $06 (background + sprite rendering), shadow at $11
  3. Scroll: both scroll registers cleared, shadows at $12/$13
  4. Shadow OAM fill: fills $0200–$02FF with $FF (all sprites hidden)
  5. jsr ClearGameplayNametable ($CA16) — clears the background nametable
  6. APUDMALOAD = $00 ($4011, DMC volume/level reset)

3. Interrupt Vectors — CPU $FFFA–$FFFF

Located at the end of the bank (PRG offset $3FFA):

Vector Address Handler
NMI $C6E6 NmiHandler
RESET $C66E ResetHandler
IRQ $FFF0 IrqHandler (single asl a byte; see the deferred-task note in §6.1)

The IRQ's apparent "oddity" is now explained: DeferredTaskHandlerTable deliberately addresses its entry 0 at mirrored CPU $BFFE, which is the IRQ-vector word at PRG $3FFE. The table leverages the address mirror so that a zero task type falls through to the unused asl a byte at $FFF0 — a register-saving trick rather than a real handler.


4. NMI / Frame Heartbeat — NmiHandler ($C6E6–$C76D)

The NMI is the entire per-frame scheduler. It:

  1. Preserves state: pushes A, X, Y, plus zero page $00–$07 (an 11-byte save; other projects only save the registers)
  2. Suspends NMI: clears bit 7 of PPUControlShadow and writes $2000 — prevents re-entrancy while the frame logic runs
  3. OAM DMA: jsr PerformOamDma ($EEB0) — sprite DMA from shadow OAM $0200
  4. VRAM queue flush: jsr FlushQueuedVramCommands ($EE9C) — drains the $0300 command queue during rendering
  5. jsr WriteInvertedRenderingMask ($CA7A) — redraws PPUMASK state
  6. jsr UpdateAudioFrame ($F91F) — per-frame audio channel update
  7. Controllers: jsr ReadControllers ($F219) — serial pad read for both ports
  8. Frame logic dispatch (see §5) based on PendingGameMode ($47), GameMode ($46), and GameplaySubmode ($4D)
  9. jsr ProcessQueuedTask ($EE3A) — pops one pending task from the $0300 queue (see §6.1)
  10. Resumes NMI: re-enables bit 7 of PPUControlShadow / $2000 — allows the next frame
  11. Restores $00–$07, A, X, Y, rti

5. Frame State Machine (inside the NMI)

The frame-mode dispatch at $C71B–$C748:

lda PendingGameMode ($47)
    │
    ├─ non-zero ── pending attract boot ──┬─ PlayerLivesOrModeCount == 0 → UpdateGameModeTransition ($C897)
    │                                     │                                (sound timers only)
    │                                     └─ PlayerLivesOrModeCount != 0 → UpdateTitleScreenAndStartSelection ($C773)
    │                                                                     + TickSoundTimers ($F1AF)
    │
    └─ zero ── lda GameMode ($46)
              │
              ├─ zero → UpdateAttractModeState ($C932)  (attract / inter-stage sequencer)
              │
              └─ non-zero ── lda GameplaySubmode ($4D)
                             ├─ zero → UpdateGameplayFrame ($CD6E)   (normal gameplay frame chain)
                             └─ non-zero → UpdateStageTransition ($CFC1)  (stage-intro / submode-6 path)

After the mode dispatch, ProcessQueuedTask ($EE3A) executes one queued gameplay task, then $2000 bit 7 is restored.


6. Subsystems

6.1 Deferred Task / VRAM Queue Systems ($0331, $0300)

Two cooperating RAM queues at $0300 fill during VBlank-adjacent time and are consumed during rendering:

  • Deferred gameplay tasks: EnqueueTask ($EE7F) appends a tagged task byte (address in $0301, data in $0302). ProcessQueuedTask ($EE3A) pops one per frame: the task's high nibble selects a handler from DeferredTaskHandlerTable ($C000), the low nibble becomes its final data byte, and the dispatcher performs an indirect jmp ($00):($02) with a synthetic $39EE return address. Queue positions wrap at offset $2E.

  • DeferredTaskHandlerTable ($C000) — five typed handlers:

    Task type Handler Address
    1 InitializeBoundingBox $F044
    2 UpdateExistingBoundingBox $F040
    3 TestObjectBoundingBoxOverlap $F138
    4 QueueSelectedHudFields $EF50
    5 FillNametableAndClearAttributes $EEBB

    Entry 0 (task type 0) is deliberately routed through the mirror at $BFFE to the IRQ-vector word — see §3.

  • VRAM command streams: QueueSingleVramByte ($F1C5), QueueRectangularVramTransfer ($EFCE), and QueuePlayerSelectVramStream ($CA33) build queued VRAM writes; FlushQueuedVramCommands ($EE9C) walks the $0331 handoff pointer into ProcessVramCommandStreamAndRestoreScroll ($EF3C), the generic $2006/$2007 writer used by every screen-draw path.

  • Sprite management: PerformOamDma ($EEB0) performs the shadow-OAM ($0200–$02FF) → $2003/$4014 DMA each frame; HideInactiveObjectSprites ($EC17)/HideObjectSpriteGroup ($CA08) park inactive slots.

6.2 Attract / Inter-Stage Sequencer ($C932–$C9FF)

UpdateAttractModeState ($C932) runs whenever GameMode == 0. It is a countdown-driven scene sequencer using GameStateTimer ($39) as a descending timer with fixed thresholds:

GameStateTimer value Action
$84 scoreboard/interlude phase (decrements per frame)
$6E–$6F stage transition setup (AdvanceAttractModeSequence, $C969)
$74 full PPU re-prep (HideObjectSpriteGroup $CA08 + ClearGameplayNametable $CA16 + QueuePlayerSelectVramStream $CA33)
$68 jsr InitializeGameplayRam ($CA82), GameMode = $01 — gameplay begins

At $C988 it disables rendering and uploads per-stage VRAM via StageBackgroundVramStreamPointerTable ($C3BD, one 2-byte stream pointer per stage index) plus the fixed stream at $F8B6, guarded by GameModeTimer ($48) — this draws the "How High Can You Get??"-style inter-stage screens before the stage demo starts, then queues rendering via QueueCollisionAndVramTasks ($ECA4) and QueueSingleVramByte ($F1C5).

6.3 Title / Game Start ($C773–$C896)

UpdateTitleScreenAndStartSelection ($C773) runs while PendingGameMode != 0 and PlayerLivesOrModeCount != 0:

  1. APU gate: audio enable $06A2-checked; a start-up delay in $05B4 raises APUCTRL = $0F (all channels on)
  2. First pass (TitleScreenInitialized ($52) == 0): disables rendering, uploads the title background via the TitleScreenVramCommandStream at $F266, then builds the cursor sprite ($0200/$0201/$0203 with Y from TitleCursorY ($ED) = $6F)
  3. SELECT ($20 mask): advances TitleCursorY by $10 (wraps $AF → $6F), sets TitleInputRepeatTimer ($7D) = $0A, latches StartButtonLatch ($6A)
  4. START ($10 mask): latches PreviousStartButtonState ($EC), clears the bounding-box work area ($24–$2E), derives SelectedPlayerMode ($EF) from TitleCursorY (lsr ×4 minus 6) and a matching GameModeTimer ($1C or $18), then jsr CollisionUpdateStep_EDFF ($EDFF)
  5. Transition to gameplay: reseeds the PRNG ($18 = $0F, $19 = $13), clears PendingGameMode, GameMode, and the $0580–$0587 record arrays, sets StageOrScreenIndex ($4A) = $01, derives DifficultyOrRoundIndex ($4B) from SelectedPlayerMode parity, sets PlayerLivesOrModeCount ($4C) per TwoPlayerModeFlag ($4E); final call HideObjectSpriteGroup and clears the $05B4 startup delay

UpdateGameModeTransition ($C897) is the alternate boot chain (PlayerLivesOrModeCount == 0): it ticks sound timers and compares GameStateTimer against $75 in the same phased style as the attract sequencer.

6.4 Gameplay Submodes ($CD6E–$D0FF)

With GameMode != 0, the NMI dispatches on GameplaySubmode ($4D):

  • Submode 0 — UpdateGameplayFrame ($CD6E): the full gameplay frame chain. Player state machine on PlayerState ($6B, states $03/$04/$05 special-cased):
    • PlayerUpdateStep_EE10 ($EE10), DecodePlayerDirectionalInput ($CE6C) — input decode and object ambient updates
    • UpdatePlayerAuxiliarySprite ($CEA2), InitializePlayerStateTransition ($D251), DispatchPlayerState ($D412), ProcessStageCollisions ($D2FC), UpdateStageEntities ($CEF4), DetectPlayerLoss ($CED4) — player state, movement, and collision phase
    • HideInactiveObjectSprites ($EC17), UpdateGameplaySubmode ($CF9D), RequestLowAltitudeSound ($CEC9) — object OAM build and sound requests
    • death/loss handling: UpdatePlayerDeathSequence ($D1AE), UpdateBonusCountdown ($CF37)
    • TickSoundTimers ($F1AF)
    • Player-state bookkeeping: PlayerStateReferenceY ($76) copied from shadow OAM $0210, PlayerStatePhase ($78), PlayerStateTransitionActive ($79), PlayerStateCounter ($7E), plus the vertical-physics groups PlayerVerticalSpeedInteger ($7A)/PlayerFallThreshold ($7B)/PlayerVerticalAcceleration* ($7C/$82/$83)/PlayerGravity* ($86/$87)
    • SELECT exits via the PendingGameMode = 1 path at $CDE4 (return to title)
    • PauseActive ($EB)/PauseDebounceTimer ($EE) gate the frame body
  • Submode != 0 — UpdateStageTransition ($CFC1): stage-intro / submode-6 path. In its early phase it gates on PlayerState and PlayerStatePhase; once the reference sprite at $0210 passes Y threshold $28, it issues a per-stage sound request into SecondarySoundRequestFlags ($FD) from StageOrScreenIndex ($02 for stage 1, $04 for stage 2, otherwise $08 — stage music), uploads a $50-byte VRAM stream (PPU address $1866 with a $00 fill value — pattern-table erase for the intro) via QueueRectangularVramTransfer, and sets PlayerInputDirection ($54) = $03. Submode 6 branches to BeginStageClearSequence ($D05F); entry also flows through InitializePlayerBoundingBox ($D04E) and BuildPlayerCollisionBounds ($D2BD).

6.5 Controllers ($F219–$F265)

Standard NES serial read through $4016 with the classic DPCM-corruption filter:

  • ReadControllers: strobe on/off, then ReadControllerPort for X = 0 and 2
  • ReadControllerPort: 8-bit shift loop in Y, then a two-sample consistency merge — a polarity-safe trick that keeps the record stable against mid-frame pad reads
  • Results in ControllerRawState ($14) / ControllerFilteredState ($15) and $16/$17 for port 2
  • The $0F-mask ladder implements "button pressed this frame" persistence for directional input vs start/select

6.6 Sound Engine ($F1AF–$FE55)

  • UpdateAudioFrame ($F91F): called from the NMI as the main audio service — it manages the APU channel frame state and audio effects
  • TickSoundTimers ($F1AF): per-frame channel timer service; SoundTimerDivider ($34) drives the frame-based SoundTimerArray ($35+)
  • Effect state and data: Pulse1Effect* ($F0–$F2), AudioEffectTimer/AudioPitchAccumulator ($F3/$F4), Triangle* ($F5/$F6), music sequencer state MusicDataPointer* ($F7/$F8)/MusicNoisePosition ($F9)/MusicPulsePosition ($FA)/AudioLfoPhase ($FB), and request flags SoundRequestFlags ($FC)/SecondarySoundRequestFlags ($FD)/ObjectSoundRequestFlags ($FE)
  • Sound-effect data at the end of the bank: SoundEffectDataPointer* ($FE52/$FE53), SoundEffectNoiseOffset ($FE54), SoundEffectSequenceData at $FE55
  • The title/APU chain programs APUCTRL = $0F (all channels on) at start

6.7 Pseudo-Random Generator ($F1F8–$F218)

An 8-byte LFSR at $18–$1F:

  • Taps: bit 1 of RandomLfsr0 XOR bit 1 of RandomLfsr1
  • Shifted bit through the full ror chain over all eight bytes
  • Stepped continuously in the reset idle loop (Label_C6B0)
  • Seeded to $0F $13 $00… at game start

7. Memory Map (Key Zero Page / RAM)

Address Name Use
$00–$0F Zp00–Zp0F Shared scratch / indirect pointers (routine-local by design)
$10 PPUControlShadow $2000 shadow
$11 PPUMaskShadow $2001 shadow
$12–$13 PPUScrollXShadow / PPUScrollYShadow $2005 shadows
$14/$16 ControllerRawState / Controller2RawState Raw serial pad bytes, ports 1/2
$15/$17 ControllerFilteredState / Controller2FilteredState Filtered pad bytes, ports 1/2
$18–$1F RandomLfsr0–RandomLfsr7 8-byte pseudo-random shift register
$20–$23 CollisionBounds0–CollisionBounds3 Active collision bounds
$24–$27 BoundingBoxFlags / BoundingBoxCoord1–3 Pending bounding box (queued tasks)
$2C/$2E PlayerVerticalFraction / PlayerVerticalPosition Player Y integration
$34–$38 SoundTimerDivider / SoundTimerArray / SharedActionTimer / PlayerSpriteVisibilityTimer Sound timers, shared timers
$39 GameStateTimer Attract/title countdown
$3A ObjectActiveArray Object active/hidden flags for sprite hiding
$3C ActionCooldown Object cooldown
$3E–$45 CollisionLeft/Top/Right/Bottom, CollisionMatchX/Y/Right/Bottom Collision results
$46 GameMode Current mode (0 = attract, 1 = gameplay)
$47 PendingGameMode Boot pending flag
$48 GameModeTimer Mode timer (attract/title countdowns)
$49 CurrentPlayerIndex Player selector
$4A StageOrScreenIndex Current stage (1-based)
$4B DifficultyOrRoundIndex Difficulty/round selector
$4C PlayerLivesOrModeCount Lives / mode count
$4D GameplaySubmode Gameplay submode selector
$4E TwoPlayerModeFlag 2-player selection
$4F–$51 DemoInputIndex / DemoInputTimer / DemoInputCode Attract-demo scripted input
$52–$53 TitleScreenInitialized / LevelObjectProgress Boot stage, level-object progress
$54–$57 PlayerInputDirection / PreviousInputDirection / PlayerPositionFraction / ObjectPositionFraction Input + fixed-point positions
$64–$6A BonusTickDivider / BonusCounter / DeathAnimation* ($66/$67) / PlayerCollisionResult ($68) / CollidingObjectOamOffset ($69) / StartButtonLatch ($6A) Bonus ticks, death animation, collision results
$6B–$70 PlayerState, PreviousPlayerState, PlayerFacing, PreviousPlayerFacing, PlayerClimbPhase, PlayerClimbDirection Player state machine + climbing
$71–$77 PlayerMovementSubstate, MovementCollisionCode, PlayerCollisionAnchor* ($73/$74), PlayerClimbOffset, PlayerStateReferenceY ($76), PlayerLandingTile Movement, collision anchors, landing
$78–$7E PlayerStatePhase ($78), PlayerStateTransitionActive ($79), PlayerVerticalSpeedInteger ($7A), PlayerFallThreshold ($7B), PlayerVerticalAcceleration ($7C), TitleInputRepeatTimer ($7D), PlayerStateCounter ($7E) State phase + vertical physics
$7F–$87 PlayerVerticalVelocityFraction ($7F), PlayerVerticalAcceleration* ($82/$83), PlayerGravity* ($86/$87) Vertical velocity/gravity fixed-point
$8C–$9F CarriedObjectActive ($8C), CollectibleSpriteTileArray ($8D), StageObjectDataPointer* ($91–$93), CollectedObject* ($94–$9F) Stage objects and collectibles
$A0–$BB StagePrimaryObjectCount, ObjectMotionStep ($A1), StageSecondaryObject*, EnemyStateArray ($A3), EnemyAlternateStateArray ($A6), EnemyDirectionArray ($B4), EnemySpawnDelayArray ($B9), Platform* state ($AC–$BE), StageSecondaryObjectCount ($B0), SpawnedEnemyCount ($B1), StageObjectMode ($B2) Stage-entity pools
$C3–$CF SpecialObjectIndex ($C3), SpecialObjectX ($C4), StageObjectAnimationMode ($CE), StageObjectOamOffset ($CF) Special objects
$D0–$E6 BonusSpriteActive ($D0), VramMetatileBuffer ($D1), StageRopeAddress* ($DA/$DB), PendingLevelObjectTask ($E7) VRAM buffers, ropes, level-object tasks
$E7–$EE DemoInputActive ($E8), GameplayInitializationPending ($E9), MovementFrameCounter ($EA), PauseActive ($EB), PreviousStartButtonState ($EC), TitleCursorY ($ED), PauseDebounceTimer ($EE) Flow/pause/title state
$EF SelectedPlayerMode Player-count selection from the title
$F0–$FE Pulse1Effect* ($F0–$F2), AudioEffectTimer ($F3), AudioPitchAccumulator ($F4), Triangle* ($F5/$F6), MusicDataPointer* ($F7/$F8), MusicNoisePosition ($F9), MusicPulsePosition ($FA), AudioLfoPhase ($FB), SoundRequestFlags ($FC), SecondarySoundRequestFlags ($FD), ObjectSoundRequestFlags ($FE) APU effects and music sequencer state
$FF GameplayEventFlags Shared gameplay event/request flags
$0100–$01FF Scratch and object arrays Object work area
$0200–$02FF Shadow OAM Cleared to $FF at reset; DMA'd every frame
$0300–$03FF Deferred task / VRAM queue Command-stream bytes and task dispatch slots ($0300 cursor, $0301 wrap index, $0302 entries×2, $0331 handoff pointer)
$0520+ Stage object RAM Object/config groups copied in via the ObjectConfig* tables
$0580–$05B5 Per-mode record arrays Attract/stage state records written by the title and attract sequences
$06A2, $05B4 APU gate / startup delay flags Sound enable gating used at title and gameplay entry

A handful of bytes still carry deliberately neutral names (Zp28–Zp5C, Zp58–Zp62, Zp80–ZpE6 members) where reuse across routines is suspected; the Pass 1 trace recorded 1,757 zero-page accesses across 186 distinct base addresses.


8. Code/Data Map

Pass-1 trace statistics (recorded during generation): 6,135 decoded instructions, 12,358 classified code bytes, 266 reachable routine entry points, 68 referenced PRG data labels, 0 overlapping-instruction conflicts. As of the current Pass 3 source, every data region is emitted as explicit .byte data with its ROM provenance documented — no .incbin blocks remain.

Region Class Size
$C000–$C66D data (DeferredTaskHandlerTable, HudDigitDescriptorTable, StagePlayerStartYTable, ObjectConfig* tables, StageBackgroundVramStreamPointerTable at $C3BD) 1,646 bytes
$C66E–$F137 code (reset, NMI, mode dispatch, attract, title, gameplay) 10,954 bytes
$F138–$F1AE data (bounding-box overlap data) 119 bytes
$F1AF–$F265 code (sound timers, LFSR, controllers) 183 bytes
$F266–$F91E data (TitleScreenVramCommandStream at $F266, screen VRAM streams and stage data — 1,721 bytes) 1,721 bytes
$F91F–$F957 code (UpdateAudioFrame) 57 bytes
$F958–$F959 data 2 bytes
$F95A–$F9F7 code 158 bytes
$F9F8–$FA63 data 108 bytes
$FA64–$FE50 code (deferred task / VRAM queue engine, $EE39–$FE50) 1,005 bytes
$FE51–$FFEF data (SoundEffectDataPointer* $FE52–$FE53, SoundEffectNoiseOffset $FE54, SoundEffectSequenceData $FE55…) 415 bytes
$FFF0–$FFFF IRQ byte (reused by DeferredTaskHandlerTable entry 0) + filler + vectors 16 bytes

9. Summary of Execution Flow

Power On / Reset
    │
    ▼
ResetHandler ($C66E) ──── sei, clear RAM, init PPU/shadow OAM, arm attract
    │
    ▼
enable $2000 bit 7 (NMI on)
    │
    ▼
Idle loop $C6B0 ────────► AdvanceRandomLfsr, forever   (reset path never returns)
                                                        (all action is in NMI)

NMI ($C6E6) [once per frame]
    ├─ push A/X/Y + $00–$07 save
    ├─ PerformOamDma ($EEB0) + FlushQueuedVramCommands ($EE9C)
    ├─ WriteInvertedRenderingMask ($CA7A) + UpdateAudioFrame ($F91F)
    ├─ ReadControllers ($F219)
    ├─ Mode dispatch ─┬─ PendingGameMode → UpdateTitleScreenAndStartSelection ($C773)
    │                 │                      / UpdateGameModeTransition ($C897)
    │                 ├─ GameMode == 0  → UpdateAttractModeState ($C932)  ─┐
    │                 └─ GameMode != 0  → UpdateGameplayFrame ($CD6E)      │
    │                                        / UpdateStageTransition ($CFC1) │
    ├─ ProcessQueuedTask ($EE3A) ──► DeferredTaskHandlerTable ($C000)      │
    └─ restore CPU/ram state, rti                                          │
                                                                           │
     InitializeGameplayRam ($CA82) at GameStateTimer ($39) = $68 ◄──────────┘
         │
         ▼
     GameMode = 1; NMI dispatches gameplay chains per GameplaySubmode
         (player state machine on $6B, collision bounds, stage entities,
          deferred task queue, DispatchPlayerState/ProcessStageCollisions)

10. Notes on Provisional Areas

  • Status: Pass 3 — all 1,035 top-level labels (ResetHandler, NmiHandler, UpdateGameplayFrame, UpdateStageTransition, ProcessQueuedTask, DeferredTaskHandlerTable, state tables, etc.) carry semantic names, and all data regions are explicit .byte blocks with Pass 3 structure annotations. Remaining ZpXX names are deliberate (routine-reused scratch), documented in §7.
  • IRQ / deferred-task quirk: The table entry 0 at mirrored $BFFE reuses the IRQ-vector word; the $FFF0 asl a byte is a table addressing artifact, not a real handler (see §3 and §6.1).
  • Remaining unknowns: the detailed semantics of the ObjectConfig* per-difficulty groups copied into $0520+ RAM, the exact music-sequence encoding inside the $F266/$F95A data streams, and a few still-neutral ZpXX scratch bytes.

Build verification:

cd "/home/tmerritt/Projects/disassembly/Donkey Kong Jr. (JU)/copilot"
make              # builds reconstructed.nes, then cmp against rom.nes → "Byte-identical ROM verified."

The rebuild is byte-identical to rom.nes (ROM SHA-256 at the top of this document). The Pass-1 source verifier (verify_pass1.py, which re-decoded every annotated instruction byte and re-hashed the full 24,592-byte ROM against rom.nes) passed during generation but was not carried into copilot/.