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.
325 lines
24 KiB
Markdown
325 lines
24 KiB
Markdown
# 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:
|
||
|
||
```bash
|
||
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/`.
|