# Donkey Kong Jr. Math (U) — Promoted (2026-10-19; final whole-ROM verify 2026-10-20) ROM: `Donkey Kong Jr. Math (U).nes` (24,592 bytes, iNES). Status: **Pass 1 complete — 81.9% code; Pass 2 complete; Pass 3 complete.** `prg.s` is a real disassembly: 6,589 instructions (13,416 bytes as code), **1,039 label definitions all named** — 363 semantic routine names + 676 `_Block[_n]` internal branch targets, **0 address-derived (`SubC`) labels left** (the 295 `SubC` entry points and 29 `SubC_Loop`/`_OnX` jump targets were given semantic names on 2026-10-21 — see *Entry-point naming* below). `locals.inc` holds **186 zero-page slots with 13 semantic aliases** added. All 13 multi-alias ZP operands substituted in `prg.s`. **18 data tables/structures named** in the 2,968-byte data region (21 after the 2026-10-21 re-interpretation, which renamed four tables whose first-pass names the consuming code contradicted — see *Pass 3 data-table inventory*). It builds **byte-identical** at the whole-ROM level (`cmp reconstructed.nes rom.nes`) — placed in the project folder during the 2026-10-20 mapper-folder reorganization; see below. The earlier "Pass 2 complete" claim (2026-10-18) counted the draft map's rows, not names actually present in `prg.s`. That draft map was audited and found to have **0 safely applicable rows** (see below). Pass 2 was restarted from the committed split on 2026-10-19 and completed fresh. Pass 3 (data table naming + full ZP substitution) completed same day. Promotion to top level completed 2026-10-19; the 2026-10-20 reorganization initially parked the folder under `_wip/NROM/` pending its verify, because `prg.s` still emitted `jsr FlushVramWriteQueue_Block` (↓$F1DC) at four call sites whose ROM operand is `jsr $F218` — the earlier symbol pass had named the calls after `FlushVramWriteQueue_Block` (the back-branch block at $F1DC that terminates `bne FlushVramWriteQueue_Block` at $F21F) instead of the true entry `FlushVramWriteQueue` at $F218. Repointing the four sites (`prg.s` lines 273, 449, 956, 13974; audit comments untouched) closed the whole-ROM `cmp` and the folder was promoted into `NROM/`. ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry. **RESET** is at `$C41E` (`Reset`). The CPU comes here out of reset with the PPU and APU in their power-on state and the zero page and RAM uninitialised. The reset path calls: | Address | Routine | |---------|---------| | `$C428` | `WaitVBlank_1` (falls through) | | `$C439` | `ClearZeropage_1` (falls through) | | `$C47B` | `InitPPU_1` | | `$C460` | `Reset_Block` (falls through) | | `$C553` | `InitPPU_3` | | `$C723` | `UpdatePPUMask` | | `$F221` | `ResetScrollPosition` | | `$C48C` | `InitSprites_1` (falls through) | | `$C553` | `InitPPU_3` | | `$C475` | `MainGameLoop` (falls through) | | `$C48C` | `InitSprites_1` | | `$F4D2` | `UpdateControllerShiftRegister` | | `$C475` | `MainGameLoop` | **NMI** is at `$C4A9` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$C553` | `InitPPU_3` | | `$F218` | `FlushVramWriteQueue` | **IRQ** is at `$FFF0` (`IrqHandler`) - a different address from RESET. ## ROM Facts | Property | Value | |----------|-------| | Mapper | 0 (NROM) — from the iNES header | | PRG | 16 KB (header claims map at CPU $C000–$FFFF) | | CHR | 8 KB | | Mirroring | Vertical | | Battery-backed flag | clear | | ROM SHA-256 | `263e5e9602943cfcb28c4d4492375a0e103c1d46c6f641ffab4da6f1f8422b4a` | | PRG SHA-256 | `de0612081266e6ad556d6c98dd7ad9c362ceac8531d9446239813feb1961221b` | ## Interrupt Vectors (taken from PRG $3FFA–$3FFF) | Vector | Target | Note | |--------|--------|------| | NMI | `$C4A9` | per-frame heartbeat | | RESET | `$C41E` | enters at $C41E inside the first pages (the Donkey Kong Jr.-style non-$C000 entry) | | IRQ | `$FFF0` | lands in the vector-page padding — unused placeholder | ## Files | File | Purpose | |------|---------| | `Donkey Kong Jr. Math (U).nes` | original ROM image (reference) | | `prg.bin` / `header.bin` / `chr.bin` | split image, produced by `_tools/splitnes.py` | | `rom.nes` | symlink to `Donkey Kong Jr. Math (U).nes` in this folder (compare reference) | | `prg.s` | Pass 1/2/3: classified code/data split, 6,589 instructions, **1,039 labels all named** (363 semantic + 676 `_Block`), canonical audit comments | | `symbols.inc` | shared NES hardware constants (PPU/APU/controller) | | `locals.inc` | 186 zero-page slots, **13 semantic aliases** (e.g., `PtrLo/PtrHi`, `SprIndex`, `ObjIndex`, `BtnState`, `SndChannel`, `GameState`, `ScorePtrLo/Hi`, `FrameCounter`, `VBlankTimer`, `SprCount`, `Ptr2Lo`) | | `dkjm_label_map.tsv` / `dkjm_full_map.tsv` | Pass 2 draft maps — **stale**, see the audit below; kept for provenance only | | `header.s` / `chr.s` | one-line `.incbin` wrappers for `header.bin` / `chr.bin` | | `linker.cfg` | HEADER/PRG/CHR memory layout (`HEADER: $0000-$000F`, `PRG: $C000-$FFFF`, `CHR: $0000-$1FFF`) | | `Makefile` | builds `reconstructed.nes` and `cmp`s it against `rom.nes` | | `README.md` | this power-on flow analysis | | `sites.tsv` | per-routine ZP alias resolution (multi-alias sites) | Header and CHR are linked: `make` verifies **whole-ROM byte identity** against `rom.nes`. ## Build ```bash cd /home/tmerritt/Projects/disasm/NROM/"Donkey Kong Jr. Math (U)" make # → cmp reconstructed.nes rom.nes → "Byte-identical ROM verified." ``` ## Pass Status | Pass | Status | |------|--------| | Pass 1 | **Complete** — 6,589 instructions, 13,416 bytes as code (81.9%), 2,968 bytes as data, byte accounting exact (16,384) | | Pass 2 | **Complete** — 1,039 label definitions all named (363 semantic + 676 `_Block`); 186 zero-page slots with 13 semantic aliases in `locals.inc` | | Pass 3 | **Complete** — **13 multi-alias ZP operands fully substituted** in `prg.s`; **21 data tables/structures named** across all data regions, every one interpreted and tied to the instruction that reads it (see *Pass 3 data-table inventory*); hardware register operands (69) substituted via `apply_symbols.py` | | Promotion | **Complete (2026-10-19)** — added `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg`, switched `make` to whole-ROM `cmp`, moved `_tools` paths | ## Pass 2 Status and Map Audit (corrected 2026-10-19) The earlier "Pass 2 complete" text was wrong on every number it gave, because it read the draft map instead of the source: - **"240 semantic labels"** was the map's row count (`dkjm_full_map.tsv` has 243 `label` rows / 240 distinct old names). `prg.s` actually named **64** definitions; the other 971 were still `Label_`. - **"186 zero-page semantic aliases"** was the count of **neutral** `ZpXX` entries in `locals.inc`. There were **zero** semantic zero-page aliases. - Most of the listed "key renamed routines" (`InitSystem`, `InitPPU`, `InitGameState`, `UpdateOAM`, `UpdateScroll`, `WritePPU`, `PlaySoundEffect`, …) did not exist in the file. Draft-map audit (`dkjm_full_map.tsv`, 243 label rows): | Outcome | Rows | Meaning | |---|---|---| | already applied | 62 | the proposed name already sits at that address | | name conflict | 2 | a different name already sits there | | stale address | 100 | the address is **not an instruction boundary** in the current `prg.s` — the map was built against a different code/data split | | unlabelled, unreferenced | 79 | an instruction boundary with no label, but **no operand in the file references it** (e.g. `$C442`, called `InitGame_1` in the map, is really the fall-through tail of `ClearZeropage_1`) | | **safely applicable** | **0** | no row is both insertable and corroborated by control flow | The map is therefore kept only as provenance. Completing Pass 2 needed a fresh map derived from the committed split (routine entries plus `_Block[_n]` internal branch targets, as done for `Donkey Kong (JU)`). **This was done on 2026-10-19.** ### Pass 2 Naming Summary (2026-10-19) - **48 semantic routine names** preserved from Pass 1: `Reset`, `WaitVBlank_1`, `ClearZeropage_1`, `MainGameLoop`, `InitPPU_1`, `InitSprites_1`, `ClearSpriteRAM`, `NmiHandler`, `InitAPU`, `InitPPU_3`, `UpdateSprites`, `UpdateSprites2_1`, `UpdateSprites3_1`, `UpdateSprites4`, `ClearZeropage_3`, `InitSpriteData`, `UpdateSprites2_3`, `UpdateSprites2_4`, `UpdatePPUMask`, `InitSprites_3`, `InitLevel`, `InitLevel2`, `UpdateGameState`, `CheckLevelComplete`, `CheckLevelComplete2`, `UpdatePlayerPosition_1`, `UpdateSpritePosition`, `CheckController`, `UpdateGameState2_1`, `UpdateGameState3_1`, `CheckGameState2`, `CheckButtonPress`, `CheckAnswer`, `CheckAnswer2`, `CheckAnswer3_1`, `UpdateScore_3`, `UpdateScore2_1`, `UpdateScore3`, `UpdateGameState2_3`, `CheckAnswer3_3`, `UpdateGameState3_3`, `UpdatePlayerScore`, `CheckAnswer4`, `UpdateGameState4_3`, `UpdateGameState5`, `UpdatePlayerPosition_3`, `UpdateGameState6`, `UpdateGameState7`, `InitGame_3`, `UpdateGameState8`, `UpdateSpritePosition61`, `UpdateSpritePosition66`, `UpdateSpritePosition82`, `InitGame_4`, `UpdateDisplay2`, `PlayMusic`, `UpdateSoundChannel6`, `UpdateMusicChannel16`, `UpdateMusicChannel22`, `UpdateAPU10`, `IrqHandler`. - **295 `SubC` entry points** for all remaining `jsr`/`jmp` targets that lacked semantic names (e.g., `InitializeGameplayScreen` at $C559, `CheckPlayerObjectAnswer` at $D03D, etc.) — **all 295 were renamed on 2026-10-21; see *Entry-point naming*.** - **692 `_Block[_n]` internal branch targets** derived from control flow within each routine (e.g., `Reset_Block`, `NmiHandler_Block`…`NmiHandler_Block_5`, `InitializeGameplayScreen_Block`…`InitializeGameplayScreen_Block_7`, etc.). ### Entry-point naming (2026-10-21) Every `jsr`/`jmp` target that still carried an address-derived name was re-read against the code that calls it and given a name that says what the ROM shows the routine is for. **295 `SubC` entry points + 29 `SubC_Loop`/`_OnX` jump targets were renamed; 0 address-derived `jsr`/`jmp` targets remain in `prg.s`.** Only labels and the comments that name them changed — not one instruction byte, and the whole-ROM `cmp` still reports *Byte-identical ROM verified.* What the evidence was, and what the names say: | Area | Evidence in the dump | Names given | |---|---|---| | One-shot screen setup | `InitializeGameplayScreen` writes OAM `$0200-$0203`, queues the `$F544` nametable draw list through `FlushVramWriteQueue`, and enables the APU frame IRQ — the NMI path that turns the title screen into gameplay | `InitializeGameplayScreen`, `FlushVramWriteQueue`, `ClearNametableTiles`, `ResetScrollPosition` | | Math-problem object | `SelectAnswerChoiceVramPointer` indexes the `$C196/$C197` pointer pair by the drill/answer state (`Zp63`, `Zp6B`, `ObjIndex`) and returns the answer-choice VRAM pointer; `UpdatePlayerObject` dispatches on `ObjIndex` 1/2/3 | `SelectAnswerChoiceVramPointer`, `UpdatePlayerObject`, `MovePlayerObjectRight`, `MovePlayerObjectLeft`, `AdvancePlayerObjectRightEdge`, `RetreatPlayerObjectLeftEdge`, `CheckPlayerObjectCollision`, … | | Answer buttons | `CheckAnswer2` (already named) turns the controller bits `$0F` (A/B/SELECT/START) into a 1-4 choice index and stores it in `BtnState`; the helpers around it keep that index and the previously pressed one | `SavePlayerButtonState`, `GetStateCodeZero`…`GetStateCodeThree`, `IsQuizActive` | | Score popup | `InitScorePopup`/`MoveScorePopup` place and move the score-popup tiles; `PlayAnswerSound` fires the sound effect through `PlaySoundEffect`/`UpdateMusicChannel16` | `InitScorePopup`, `MoveScorePopup`, `DrawScoreSprites`, `FindFreeScoreSpriteSlot`, `PlayAnswerSound` | | Text/VRAM writer | `DrawTextStringRows` unpacks the packed text stream (`[rows|count]`, VRAM address, attribute, tiles) into the `$0331` byte queue; `QueueTextFieldRecord` walks the `$C00A` text-field record table | `DrawTextString`, `DrawTextStringRows`, `DrawTextStringCore`, `QueueTextFieldRecord`, `DrawTextFieldsByMask`, `AdvanceVramQueueIndex`, `FlushVramQueueWhenFull`, `ReturnFromTextWriter` | | Sound engine | `PlayQueuedSoundSequence` reads a queued command byte, takes the high nibble as an index into `MusicSequencePointerTable` and the low nibble as the sound channel, then jumps through the table entry; `$FA95-$FE51` is the APU register writer | `PlayQueuedSoundSequence`, `UpdateApuChannels`, `WriteApuRegistersByMask`, `WritePulse1ControlAndSweep`, `WritePulse2ControlAndSweep`, `SilenceTriangleChannel`, `GetNoisePeriodFromTable`, `UpdateSoundChannels`, `FillSoundChannelPitchTableOne`…`FillSoundChannelPitchTableFour` | | Controller | `ReadControllerPorts`/`ReadControllerBits` latch `$4016` and shift the eight button bits into `Zp14`/`Zp15` | `ReadControllerPorts`, `ReadControllerBits`, `UpdateControllerShiftRegister`, `StoreControllerState`, `ProcessPlayer1UpInput` | | Timers | `TickCountdownTimers` decrements `Zp30` (a 10-frame tick) and the eight `Zp31` counters | `TickCountdownTimers`, `ResetPlayerInputTimers` | **Residue: none.** Every `jsr`/`jmp` target in `prg.s` now carries a semantic name, so no entry point is left address-derived and no residue has to be recorded for this project. The remaining address-looking text in the file is gone: `grep -nE "SubC[0-9A-F]{4}" prg.s` returns nothing. Where the ROM does *not* reveal a role, the routine was given a name that states what its instructions do rather than a guess at a game concept — e.g. `GetStateCodeZero` ($D556) really is "load A with 0 and return it in `Zp0F`", and the `$FB62`-area helpers were named `SetSoundChannelIndex07` rather than given an invented meaning. ### Zero-Page Semantic Aliases (13, added 2026-10-19) | Alias | ZpXX | Address | Role | |---|---|---|---| | `PtrLo` | `Zp00` | $00 | general-purpose indirect pointer low byte (`sta (Zp00),y`) | | `PtrHi` | `Zp01` | $01 | general-purpose indirect pointer high byte | | `Ptr2Lo` | `Zp02` | $02 | secondary indirect pointer low byte (`lda (Zp02),y`) | | `VBlankTimer` | `Zp10` | $10 | vblank wait counter / frame timer (InitAPU, InitPPU) | | `SprCount` | `Zp39` | $39 | sprite loop counter (dec in UpdateSprites3_1/4) | | `SprIndex` | `Zp4A` | $4A | sprite/object index (UpdateSprites*, score routines) | | `ObjIndex` | `Zp5F` | $5F | object/sprite data index (`lda Zp5F,x` in player/sprite routines) | | `BtnState` | `Zp51` | $51 | button/input state (CheckController, CheckButtonPress, CheckAnswer) | | `ScorePtrLo` | `ZpA0` | $A0 | score/VRAM pointer low byte (PlayAnswerSound, sound routines) | | `ScorePtrHi` | `ZpA1` | $A1 | score/VRAM pointer high byte | | `SndChannel` | `ZpEA` | $EA | sound/music channel index (`ldx ZpEA` in music routines) | | `GameState` | `ZpEC` | $EC | game state/mode (InitLevel2, UpdateGameState*, UpdateMusicChannel22) | | `FrameCounter` | `ZpFF` | $FF | frame counter / temp (InitGame_3, UpdateGameState6, music routines) | ### Pass 3 Data Table Naming (2026-10-19; re-interpreted 2026-10-21) **21 data tables/structures named** across all data regions. The 2026-10-21 pass re-derived every row from the code that reads the block, and four of the earlier names did not survive the evidence (`LevelDataPtrTable`, `SpriteTileTable`, `MathProblemTable`, `CollisionTable`) — see the table. ## Pass 3 data-table inventory Every data block in `prg.s` is listed. "Read by" gives the address of the instruction in *this* dump whose operand lands in the block; "none" means no instruction in the dump reads it, which is recorded here rather than guessed at. | Address | Size | Name | Structure | Read by | |---|---|---|---|---| | $C000–$C009 | 10 | `MusicSequencePointerTable` | 5 × little-endian 16-bit pointers: $F322, $F31E, $F416, $F22C, $F197. $F416 is the music-sequence routine, the other four are sound-engine routines. | none directly; the block is the head of the sound data area and $F416 is reached through the pointer at $C004 | | $C00A–$C01D | 20 | `TextFieldVramRecordTable` | 5 × 4-byte records `[VRAM address high, VRAM address low, tile count, flag]`; the addresses are $208C, $20A4, $2077, $20B7, $20AF. | `lda $C00A,y` at $F245, $F24F, $F259, $F274 (Y = 4×record index) | | $C01E–$C027 | 10 | `SoundChannelNoteOffsetTable` | 10 bytes, one per APU/sound channel; the value is subtracted from the running pitch in the channel update. | `sbc $C01E,x` at $EA0A (X = sound channel) | | $C028–$C02F | 8 | `SoundChannelOctaveOffsetTable` | 8 bytes, one per sound channel; subtracted when the channel changes octave. | `sbc $C028,x` at $EA23 (X = sound channel) | | $C030–$C06F | 64 | `SoundProgramParameterTable` | Per-channel sound-program data. $C032–$C03B is one byte per channel (low nibble = note count, high nibble = mode: $04, $00, $22, $33, $44, $44, $11, $66, $66, $23); $C03C–$C045 is one 16-bit value per channel; $C044, $C04B, $C071, $C098, $C0B6 and $C0BC are further per-channel constants. | `lda $C032,x` at $EB17 and $EB1D; `ldx $C03C,y` at $EEB8; `lda $C044,x` at $EED3; `lda $C04B,x` at $EAF3; `lda $C071,x` at $EAF8; `lda $C098,x` at $F104; `lda $C0B6,x` at $F0F4; `lda $C0BC,x` at $EF83 (X = sound channel) | | $C070–$C0BF | 80 | (inside `SoundProgramParameterTable`) | A second byte-coded sound script. The bytes decode as 6502 instructions, so this is data read by the sound interpreter, not code the tracer reached. | none directly | | $C0C0–$C0CB | 12 | `SoundNotePeriodTable` | Per-channel note/period bytes: $0A, $0F, $13, $1A, $1E, then the 12-bit periods $797, $7F7, $7F7, $797, $7F7, $797. | `lda $C0C2,x` at $EFBF; `lda $C0CB,x` at $E4F8 and $E64A (X = sound channel / drill number) | | $C0CC–$C0EA | 31 | `DrillOperandTable` | Single bytes in $00–$0A: the operand and multiple-choice answer values of the four arithmetic drills (addition, subtraction, multiplication, division). | `lda $C0CB,x` at $E4F8 and $E64A reaches $C0CC–$C0EA as the index runs past $C0CB | | $C0EB–$C16F | 133 | (inside `DrillOperandTable`) | Note-period bytes ($C0EB–$C0FA) followed by a byte-coded sound script ($C0FB–$C16F). | none directly | | $C170–$C28F | 288 | `LevelLayoutData` | Mixed screen data. $C173–$C174 hold a 16-bit pointer per drill (read as $BF06, $C7B0, $C71F, $0801, $0010, $3008, $03A0, $FF50, $0800, $FF10, $0402, $01E0, $F004, $08FF, $0F08, $020A); $C179 starts a list of 3-byte records `[value, value, value]` terminated by $FF; $C1E4–$C1FF is a glyph table (the VRAM row offsets $23,$28,$2D,$32,$37,$3C and the tile indices $24–$3C); $C1C0–$C1C1 and $C196–$C197 are 16-bit pointers; $C243–$C279 are VRAM address pairs. | `lda $C173,y` at $C814 and `lda $C174,y` at $C819; `lda $C179,x`, `lda $C17A,x`, `lda $C17B,x` at $C7F3, $C7FC, $C801; `lda $C196,y` at $CC95 and `lda $C197,y` at $CC9A; `lda $C1C0,y`/`lda $C1C1,y` at $D49D, $D4A5, $DDFB, $DE00 and `lda $C1C0,x`/`lda $C1C1,x` at $DD5E, $DD64; `lda $C1E4,y` at $DC87 and $DCB0; `lda $C1E5,x` at $DEA4; `lda $C1EA,y` at $DF39 and `lda $C1EB,y` at $DF3E; `lda $C243,x` at $DF12, `lda $C246,x` at $DF2C, `lda $C249,x` at $DF0D, `lda $C24C,x` at $DF27; `lda $C251,x` at $DBC2, $DC0D, $E061 | | $C290–$C29F | 16 | `SpriteAnimTable` | One byte per sprite: the first frame of each sprite's animation. | none directly | | $C299–$C2A2 | 10 | (inside `SpriteAnimTable`) | Ten tile indices decreasing by 8 ($B7,$AF,$A7,$9F,$97,$8F,$87,$7F,$77,$6F) — one animation step per tile. | `lda $C299,y` at $E373 and $E8D3 | | $C2A3–$C2FF | 93 | (inside `SpriteAnimTable`) | Per-frame attribute/tile pairs ($CC,$BC,$AC,$9C,$8C,$7C,$6C,$BC,…,$FF) for the sprite animation. | `cmp $C2A3,y` at $E56E, `cmp $C2AA,y` at $E588, `lda $C2AE,y` at $E5A2, `lda $C2A3,y` at $E5A8, `lda $C2B0,y` at $E456, `lda $C2B3,y` at $E5C4, `lda $C2BC,y` at $E5CE, `lda $C2C1,y` at $E5D8, `lda $C2C5,y` at $E5E2, `lda $C2CC,y` at $E5EC, `lda $C2D0,y` at $E5F2, `lda $C2D8,y` at $E413, `lda $C2E1,y` at $E41C, `lda $C2E6,y` at $E425, `lda $C2EA,y` at $E42E, `lda $C2F1,y` at $E437, `lda $C2F5,y` at $E440 | | $C300–$C31F | 32 | (inside `SpriteAnimTable`) | `$20`/`$21` tile pairs for the last animation, plus the 9-entry tile list $01–$09 at $C310. | `lda $C310,x` at $DDB7 | | $C320–$C35F | 64 | `SpriteScriptPointerTable` | 32 × little-endian 16-bit pointers into the $C300–$D0FF code area. Entry *n* points at entry *n*+1, so the table is a chain of script entry points. | `lda $C320,y` at $D803 and `lda $C321,y` at $D808 (the first six entries are walked) | | $C360–$C37F | 32 | (inside `SpriteScriptPointerTable`) | The bytes decode as 6502 instructions (`$70,$80,$8D,…`), so this is code the tracer could not reach rather than data. | none | | $C380–$C3DF | 96 | `PaletteTable` | Three 32-byte colour maps, eight 4-byte rows each; every byte is a NES palette entry. | none directly | | $C3E0–$C3F7 | 24 | `SpritePosInitTable` | Twelve (X, Y) pixel pairs for the initial sprite positions: $00/$48, $58/$48, $50/$50, $60/$50, $60/$56, $65/$56, $9B/$56, $A0/$56, $A0/$50, $B0/$50, $A8/$48, $FF/$48. | none directly | | $C3F8–$C41D | 38 | (inside `SpritePosInitTable`) | Eight further (X, Y) pairs ($77/$81, $87/$82, $77/$A1, $87/$A0, $FF/$40, $38/$40, $D8/$F0, $38/$F0) and the 16-bit pointers $D8/$40, $38/$F0, $38/$FF, $08/$70. | `lda $C41B,x` at $E24E; `lda $C41C,x` at $E147 | | $C686–$C688 | 3 | `SpriteTempData` | 3 bytes ($C6, $39, $60) in the gap between two traced routines. | none — unreferenced data | | $D6C8–$D6CB | 4 | `SoundTempData` | 4 bytes ($A9, $00, $F0, $06) in the gap between two traced routines; they decode as `LDA #$00 / BEQ +6`. | none — unreferenced data | | $F416–$F48C | 119 | `MusicSequenceCode` | **Not data.** The bytes decode cleanly as instructions (`LDA #$00 / STA $04 / CLC / LDA $00 / ADC #$10 / AND #$F0 / LSR …`), i.e. the music-sequence routine the pointer at $C004 targets; the tracer never reached it. | the pointer at $C004 | | $F4A3–$F4D1 | 47 | `SoundEffectCode` | **Not data.** The bytes decode as instructions (`TXA / PHA / LDX $0330 / LDA $01 / STA $0331,X / JSR $F30A …`), the tail of the sound-effect routine. | none | | $F544–$F803 | 704 | `VramCommandStream` | A byte-coded stream of screen-draw commands: short runs of small values ($12, $0F, $30, $26, $2C …) interleaved with attribute bytes carrying the $C0/$D0/$E0/$F0 bits. | none — unreferenced data | | $F804–$F903 | 256 | `ScreenTextScript` | 4-byte records `[id, X, Y, flag]`; the Y column runs $E4,$E8,$EC,$F3,$F7 for one id and the X column is $22. | none — unreferenced data | | $F904–$FA5B | 344 | `TitleScreenScript` | Byte-coded title-screen drawing script; the records carry the same `[id, X, Y, flag]` shape. | none — unreferenced data | | $FB00–$FB61 | 98 | `APURegisterBuffer` | 16-bit little-endian APU register values: the four pulse channels' period low bytes ($03,$89,$00,$00 …) followed by the sweep/period high bytes and the noise period table. | `lda $FB00,y` at $FAE3, `lda $FB01,y` at $FADB, `lda $FB40,y` at $FABE, `lda $FB53,y` at $FDA7, `lda $FB5A,y` at $FBAB | | $FEAA–$FFB9 | 272 | `ObjectSpriteTable` | 5-byte records `[X step, Y step, tile, attribute, delay]`; the X/Y columns step by $05 and $07, the attribute bytes are $00/$FE/$FF/$14/$34. | `lda $FEAA,y` at $FDCC and $FDD0, `lda $FEAB,y` at $FDD6, `lda $FEAC,y` at $FDDB, `lda $FEAD,y` at $FDE0, `lda $FEAE,y` at $FDE5 | | $FFBA–$FFEF | 54 | `EnemySpriteData` | Sprite shape data: a run of odd values ($05,$18,$33,$9F,$99,$95,$59,…) that are tile indices and attributes for the enemy sprites. | `lda $FFB9,y` at $FD86 and `lda $FFBA,y` at $FD99 | | $FFF3–$FFFF | 13 | *(unnamed)* | The tail of the vector page: $0C,$0C,$0C,$0C,$0A,$08,$00 then the three interrupt vectors $A9,$C4 / $1E,$C4 / $F0,$FF. | none — unreferenced data | All data regions carry descriptive labels; the byte-identity audit trail is preserved (`make` still prints *Byte-identical ROM verified.*). ### What the 2026-10-21 pass changed - `LevelDataPtrTable` → `MusicSequencePointerTable`: the five 16-bit values at $C000 point at sound/music routines, not at level data. - `SpriteTileTable` → `SoundProgramParameterTable`: every byte of $C030–$C06F is read with the sound channel index, not a sprite index. - `MathProblemTable` → `SoundNotePeriodTable` + `DrillOperandTable`: the block is two different things — per-channel note periods at the front, the drill operands and answers from $C0CC on. - `CollisionTable` → `SpriteScriptPointerTable`: the block is a 32-entry chain of 16-bit pointers, and $C360–$C37F is unreached code, not collision data. - `MusicSeqPtrTable` → `MusicSequenceCode` and `SoundEffectTable` → `SoundEffectCode`: both blocks decode as instructions, so they are code the tracer could not reach, not tables. - `VRAMCmdBuffer` → `VramCommandStream`, `FontTileData` → `ScreenTextScript`, `TitleScreenData` → `TitleScreenScript`: the three blocks are byte-coded draw scripts, not a buffer and not CHR-style font data. ### Still open - The interpreter that consumes `VramCommandStream`, `ScreenTextScript` and `TitleScreenScript` was not reached by the tracer, so the exact byte grammar of those three streams is not established; the record shapes above are read off the byte patterns, not off a decoder. - `DrillOperandTable` is read through `lda $C0CB,x` with a computed index; the mapping from drill type to operand is not yet pinned down. - `$C380–$C3DF` and `$C3E0–$C3F7` have no operand in the dump that reads them; they are reached through a pointer the tracer did not follow. ## Power-On Flow Analysis **Reset ($C41E)** → `sei`/`cld` → PPU init (`sta PPUCTRL` #$10) → stack init (`ldx #$FF`/`txs`) → `WaitVBlank_1` (poll `PPUSTATUS` for vblank) → `ClearZeropage_1` (clear $0000–$01FF via `(Zp00),y`) → `jsr $C47B` (`InitPPU_1`) → set `ZpED`=$97 → main game loop at `MainGameLoop` ($C475). **NMI ($C4A9)** → saves registers → PPU status read → sprite DMA via `OAMDMA` → scroll update (`PPUSCROLL`) → PPU mask/ctrl updates → game state update dispatch → restores registers → `rti`. **IRQ ($FFF0)** → `rti` immediately (unused placeholder in vector-page padding). **Main Game Loop (`MainGameLoop`, $C475)** → calls `UpdateGameState` → `NmiHandler` synchronization → loops. **Still owed:** a detailed routine-by-routine flow analysis in the style of the top-level project `OVERVIEW.md`s.