Adds remaining NROM as _wip Update gitignore to not keep object files Adds AGENTS.md as this is an AI helped project
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, horizontal 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/`.
|