# Joypad Test Cartridge (U) — Game Flow from Power-On This document describes the execution flow of the disassembled Joypad Test Cartridge (U) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `copilot/prg.s` in the repository. The rebuild is verified **byte-identical** to the original `prg.bin`. --- ## 1. ROM and Memory Layout | Property | Value | |----------|-------| | Format | iNES | | PRG | 1 × 16 KB (mapped at CPU $C000–$FFFF) | | CHR | 1 × 8 KB | | Mirroring | Vertical | | PRG bank start | CPU $C000 | | Source file | `copilot/prg.s` (4,516 lines with headers) | | Output | `reconstructed.nes` (24,592 bytes) | ### Interrupt vectors (CPU $FFFA–$FFFF) | Vector | Address | Handler | |--------|---------|---------| | NMI | $CCF7 | `NmiHandler` | | RESET | $C000 | `Reset` | | IRQ / BRK | $C000 | Reuses `Reset` | --- ## 2. Build Status `make` in `copilot/` assembles `prg.s` with `ca65`, links with `ld65`, and verifies against `prg.bin`. **Verification result: BYTE-IDENTICAL** — `make` compares the whole reconstructed ROM against `copilot/rom.nes` and prints `Byte-identical ROM verified.` Byte accounting is exact: 3,836 code + 12,548 data = 16,384 of 16,384 bytes. --- ## 3. Pass Status | Pass | Status | Description | |------|--------|-------------| | Pass 1 | **Redone** | Was badly truncated: 282 instructions / 580 bytes (3.5%). Now **1,773 instructions / 3,836 bytes (23.4%)** — a 6.6x increase. Root cause: the tracer only knew the 151 official opcodes and halted at `$C09E` on `DCP $3C20` (`$CF`), filing 3,256 bytes of real code as `.byte`. Verified against a fresh trace: 1,769 emitted instructions + 4 undocumented opcodes written as explicit bytes = 1,773, an exact match | | Pass 2 | **Complete** | 202 labels, **zero `Label_Cxxxx` remaining**; 202 routine headers, all describing real behaviour (no "undetermined in Pass 1" boilerplate); `locals.inc` carries 99 aliases, half of them proven semantic names sitting beside the generated `ZpXX` entries | | Pass 3 | **Complete** | Every data block in the PRG is interpreted and listed in the **Pass 3 data-table inventory** (section 8): the four inline VRAM-stream pointers, the master-palette command stream, the three joypad-panel tile-map command streams, the marker tile-burst stream, the two 32-byte marker sprite tables, the two 32-byte button sprite templates, the `$FF` ROM padding and the vectors. The command-stream record format is decoded from the consuming code: a record is `[PPU address high][PPU address low][count][count bytes]`, count bit 7 selects the PPU address increment (1 = horizontal, 32 = vertical) and count bit 6 selects "repeat one byte"; a byte with bit 7 set terminates the stream. | ### Why Pass 1 was truncated The tracer descended from the three vectors, following direct `jsr`/`jmp`/branch targets. It stopped at `$C09E`, where the byte is `$CF` = `DCP $3C20` — an undocumented read-modify-write op used here as ordinary code. Everything after that point was written out as data until a later run promoted it. This cartridge is an unusually clear demonstration: nothing exotic is involved, just one undocumented opcode in the middle of the main loop. `nesdis.py` now takes `--undocumented` (default **off**, so other projects' `make regen` stays byte-stable) to decode all 105 undocumented NMOS opcodes. ca65 V2.19 implements none of them, so the 4 affected instructions (`DCP $3C20`, `LAX $20D4`, `NOP $C1,X`, `RRA ($D0),X`) are emitted as explicit `.byte` with the disassembly in the comment. They stay in the code region, so the split and the audit trail are both preserved. --- ## 4. Reset — CPU $FFFC → $C000 **`Reset`** ($C000) On power-on or reset: 1. **Wait for three PPU warm-up vblanks** — `WaitVblank2` ($C005) and `WaitVblank3` ($C00A) each poll `$2002` bit 7, following an initial poll at $C000. 2. **Clear RAM** — `ClearRAM1_Loop` ($C013) clears pages $0000–$07FF via indirect indexed store (`STA VramPtrLo,X` / `STA $0100,X` etc.). 3. **Initialize PPU** — `InitPPU_1` ($C031): `STA $2000` with $10 (PPUCTRL, NMI disabled), shadow in `PpuCtrlShadow` ($EF); `STA $2001` with $00 (PPUMASK off), shadow in `PpuMaskValue` ($EE). 4. **OAM DMA** — `OamDma` ($C03F): `STA $4014` with $02 (DMA from $0200–$02FF). 5. **Clear controller state** — `ClearController` ($C044): clears $2003 (OAMADDR) and state variables. 6. `JSR PpuScrollLatch` ($CE3C) — latch scroll and control registers. 7. `LDX #$FF / TXS` — initialize stack pointer. 8. `JSR PpuMaskSetup` ($CD2F) — set PPUMASK from shadow. 9. **Clear RAM again** — `ClearRAM2_Loop` ($C05D) clears pages $0000–$06FF. 10. **Re-initialize PPU** — `InitPPU_2` ($C078): same as InitPPU_1. 11. **VRAM write** — `VramWriteSetup1` ($C086): set up address/length for a VRAM write via `VramWrite` ($CDEE). 12. `JSR ClearOAM_Routine` ($C174) — clear OAM with sprite flush value $F4. 13. `JSR VramPtrWrite` ($CD3F) — write VRAM data via pointer table. 14. `ToggleFlag` ($C095) — toggle `VramToggleFlag` ($CF). 15. `JSR PpuScrollLatch` ($CE3C) and `JSR VramPtrWrite` ($CD3F) again. 16. **Main loop** — spins, toggling $CF each iteration and calling PPU/VRAM update routines. --- ## 5. NMI / Frame Heartbeat — `NmiHandler` ($CCF7) The NMI fires once per frame: 1. **Save registers** (`PHA / TXA / PHA / TYA / PHA`). 2. `JSR PpuCtrlSetup` ($CD17) — latch PPUSTATUS, clear NMI bit in `PpuCtrlShadow` ($EF), write to PPUCTRL. 3. `JSR OamDmaSetup` ($CD24) — set OAMADDR=0, trigger OAM DMA ($4014=$02). 4. Check `$0300` (vblank flag) — if nonzero, `JSR VramBurstWrite` ($CE4F) to burst-write VRAM data from table at $0302. 5. `JSR PpuScrollLatch` ($CE3C) — latch scroll and control. 6. Clear `NmiFlagByte` ($E0) to 0. 7. **Restore registers** (`PLA / TAX / PLA / TAY / PLA / RTI`). --- ## 6. Key Subroutines | Label | Address | Description | |-------|---------|-------------| | `Reset` | $C000 | Cold-start initialization: vblank waits, RAM clear, PPU init, OAM DMA | | `WaitVblank2` | $C005 | Wait for second PPU warm-up vblank | | `WaitVblank3` | $C00A | Wait for third PPU warm-up vblank | | `ClearRAM1_Loop` | $C013 | Clear RAM pages $00–$07 | | `InitPPU_1` | $C031 | Initialize PPUCTRL and PPUMASK | | `OamDma` | $C03F | Trigger OAM DMA via $4014 | | `ClearController` | $C044 | Clear OAMADDR and controller state | | `PpuScrollLatch` | $CE3C | Latch PPU scroll X/Y and control register | | `InitStack` | $C052 | Initialize stack pointer to $FF | | `PpuMaskSetup` | $CD2F | Set PPUMASK from shadow register | | `ClearRAM2_Loop` | $C05D | Clear RAM pages $00–$06 | | `InitPPU_2` | $C078 | Re-initialize PPU control and mask | | `VramWriteSetup1` | $C086 | Set up VRAM write parameters | | `ClearOAM_Call` | $C08F | Call OAM clear routine | | `VramPtrWrite1` | $C092 | Write VRAM data via pointer table | | `ToggleFlag` | $C095 | Toggle VramToggleFlag ($CF) | | `SetupScroll2` | $C097 | Latch PPU scroll and control | | `VramPtrWrite2` | $C09A | Write VRAM data via pointer table again | | `ClearOAM_Routine` | $C174 | Clear OAM ($0200–$02FF) with $F4 | | `NmiHandler` | $CCF7 | Per-frame NMI: PPU updates, OAM DMA, VRAM burst write | | `NmiContinue` | $CD0A | Continue NMI: latch PPU scroll, clear flag | | `PpuCtrlSetup` | $CD17 | Latch PPUSTATUS, write PPUCTRL from shadow | | `OamDmaSetup` | $CD24 | Set OAMADDR=0, trigger OAM DMA | | `PpuMaskSetup` | $CD2F | Set PPUMASK from shadow | | `VramPtrWrite` | $CD3F | Write VRAM via pointer table (uses RetAddrUpdate) | | `VramWriteLoop` | $CD45 | Loop writing bytes from indirect pointer to PPUDATA | | `VramPtrTableRead` | $CD99 | Read from pointer table at $0000 | | `RetAddrUpdate` | $CDC8 | Get return address from stack, advance by 2 | | `VramWrite` | $CDEE | Set PPU address, write data bytes to PPUDATA | | `VramBurstWrite` | $CE4F | Burst-write VRAM from table at $0302 | --- ## 7. Zero-Page Aliases (`locals.inc`) | Alias | Address | Role | |-------|---------|------| | `VramPtrLo` | $00 | VRAM write pointer low byte | | `VramPtrHi` | $01 | VRAM write pointer high byte | | `VramLen` | $02 | VRAM write data length | | `PpuScrollX` | $03 | PPU horizontal scroll | | `RetAddrTempLo` | $05 | Return address temp lo | | `RetAddrTempHi` | $06 | Return address temp hi | | `PpuMaskShadow` | $0E | PPUMASK shadow | | `PpuCtrlShadow` | $EF | PPUCTRL shadow (NMI bit cleared) | | `VramToggleFlag` | $CF | Frame toggle flag | | `NmiFlagByte` | $E0 | NMI flag byte | | `JoyBtnStateBase` | $4E | Base for indexed button states | | `FrameCounterLo` | $53 | Frame counter low byte | | `FrameCounterHi` | $54 | Frame counter high byte | Full list in `copilot/locals.inc`. --- ## 8. Data Regions and Pass 3 data-table inventory The PRG is **23.4% code / 76.6% data** (3,836 / 12,546 bytes). Every data block in the PRG is listed below. The names are the labels now in `prg.s`; a `-` means the block is left **unnamed in the source** because no instruction in this dump reads it. ### The VRAM command-stream format The on-screen picture is not stored as a nametable image. It is stored as a **command stream** that the CPU replays into the PPU at run time. * The stream pointer lives in the zero-page pair `$00`/`$01` (`VramPtrLo`/`VramPtrHi`). * `SubCCD99` (`$CD99`) reads the next byte of the stream with `lda ($00),y` at `$CD9E`. **A byte with bit 7 set ends the stream** (`bpl` at `$CDA0` falls through to the writer, `rts` at `$CDA2` returns to the caller). * Otherwise the byte is the **high byte of the PPU address**; `SubCCD45` writes it to `$2006` at `$CD46`, then the next stream byte is the low byte of the address (`lda ($00),y` at `$CD4A`, `sta $2006` at `$CD4C`). * The third byte is the **record length** `lda ($00),y` at `$CD50`. Its bit 7 (carried out of `asl a` at `$CD52`) selects the PPU address increment: set = 32 (vertical, `ora #$04` at `$CD56`), clear = 1 (horizontal, `and #$FB` at `$CD5A`). Its bit 6 (carried out of `asl a` at `$CD62`) selects "repeat a single byte": with bit 6 clear the loop at `$CD71`–`$CD7A` advances `Y` once per byte and writes `length` distinct bytes; with bit 6 set the `iny` at `$CD68` runs once and the loop re-reads the same byte `length` times. * The record is therefore `[addr high][addr low][length][length data bytes]`, and the loop leaves `Y` at `length + 2` so that `SubCCD8D` (`$CD8D`) advances the stream pointer by `length + 3` — to the next record. Two opcodes are special inside a stream: `$4C` (`cmp #$4C` at `$CDB1`) makes the dispatcher load a new 16-bit stream address from the two bytes that follow, and `$60` (`cmp #$60` at `$CDA3`) makes it pop a return address off the stack and use that as the new stream pointer. The stream address itself is passed **inline in ROM**: a `jsr SubCCD3F` (`$CD3F`) or a `jsr SubCCE98` (`$CE98`) is immediately followed by the two bytes of the stream address, and the return-address trick in `SubCCDC8` (`$CDC8`) skips them when the call returns. Those two-byte inline pointers are therefore data, and are the first four rows of the inventory. ### The marker tile-burst stream `SubCCE98` (`$CE98`) reads a *different* stream format: one command byte followed by data. `and #$0F` at `$CEA3` gives the number of data bytes, the four `lsr a` at `$CEAA`–`$CEAD` give the number of repeats. The routine then builds a byte buffer at `$0302` in the format `[PPU addr high][PPU addr low][length][length data bytes]` and advances the PPU address by `$20` (one nametable row) per repeat. `SubCCE4F` (`$CE4F`) replays that buffer into `$2006`/`$2007`. ### The OAM record format The sprite tables are plain NES OAM: **8 records of 4 bytes each, in the order Y, tile, attribute, X**. `SubCC2A7` (`$C2A7`) and `SubCCB5D` (`$CB5D`) copy 32 bytes (`$DF` down to `$00`) from ROM into the OAM shadow at `$0200`, and `SubCC1A0` (`$C1A0`) / `SubCC1EE` (`$C1EE`) copy the two templates into `$02E0`. ### Pass 3 data-table inventory | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | `$C095–$C096` | 2 B | `MasterPaletteStreamPtr` | Inline little-endian 16-bit pointer to a VRAM command stream; value `$CF06` (the master palette). Placed immediately after the `jsr SubCCD3F` at `$C092`. | `lda (Zp05),y` at `$CDD5` and `$CDDA` in `SubCCDC8`, reading the return address of that `jsr`; the resulting pointer is walked by `lda ($00),y` at `$CD9E` | | `$C09D–$C09E` | 2 B | `JoypadPanelScreenOnePtr` | Inline little-endian 16-bit pointer to a VRAM command stream; value `$CF2A` (joypad panel screen one). Follows the `jsr SubCCD3F` at `$C09A`. | `lda (Zp05),y` at `$CDD5`/`$CDDA` in `SubCCDC8`; stream walked by `lda ($00),y` at `$CD9E` | | `$CB4B–$CB4C` | 2 B | `MasterPaletteStreamPtrAlt` | Inline little-endian 16-bit pointer; value `$CF06` again — the re-initialisation path re-writes the master palette. Follows the `jsr SubCCD3F` at `$CB48`. | `lda (Zp05),y` at `$CDD5`/`$CDDA` in `SubCCDC8`; stream walked by `lda ($00),y` at `$CD9E` | | `$CB53–$CB54` | 2 B | `JoypadPanelScreenTwoPtr` | Inline little-endian 16-bit pointer; value `$D063` (joypad panel screen two). Follows the `jsr SubCCD3F` at `$CB50`. | `lda (Zp05),y` at `$CDD5`/`$CDDA` in `SubCCDC8`; stream walked by `lda ($00),y` at `$CD9E` | | `$CF06–$CF29` | 36 B | `MasterPaletteStream` | One VRAM-write record — PPU address `$3F00`, length `$20` — followed by the `$FF` terminator. The 32 data bytes are the complete 2C02 master palette (one backdrop byte plus four colour triples for background and for sprites, at `$3F00`–`$3F1F`). | `lda ($00),y` at `$CD9E`; the data bytes are written to `$2007` by `sta PPUDATA` at `$CD76` | | `$CF2A–$D062` | 313 B | `JoypadPanelScreenOneStream` | 12 VRAM-write records plus the `$FF` terminator. Each record paints a horizontal run of nametable-0 tiles: `$20A2`/`$20C2`/`$20E2`/`$2102`/`$2122` (28 tiles each), `$21C5`/`$2225`/`$2285` (24 tiles each) and `$23C0`/`$23D0`/`$23E0`/`$23F0` (16 tiles each). The tile indices address the background pattern table at CHR `$1000`. | `lda ($00),y` at `$CD9E` (record header), `$CD4A` (address low byte), `$CD50` (length) and `$CD74` (each data byte) | | `$D063–$D1F4` | 402 B | `JoypadPanelScreenTwoStream` | 31 VRAM-write records plus the `$FF` terminator. Paints the left-hand half of the panel: a 26-tile run at `$2062`, 13-tile runs at `$208F`…`$21CF`, single tiles at `$20A6`/`$2106`/`$2166`/`$21C6`, 7-tile runs at `$2087`…`$21E7` and the bottom-left 4×16 block at `$23C0`–`$23FF`. | `lda ($00),y` at `$CD9E`, `$CD4A`, `$CD50`, `$CD74` | | `$D1F5–$D4AE` | 698 B | `JoypadPanelScreenThreeStream` | 57 VRAM-write records plus the `$FF` terminator. The full panel: every record of screen two plus the right-hand half (`$220F`…`$234F` 13-tile runs, `$2226`…`$2346` single tiles and `$2207`…`$2367` 7-tile runs). | `lda ($00),y` at `$CD9E`, `$CD4A`, `$CD50`, `$CD74` | | `$D4AF–$D4E2` | 52 B | `MarkerTileBurstStream` | A `SubCCE98` burst stream: the command byte `$A5` (low nibble `$5` = 5 data bytes, high nibble `$A` = 10 repeats) followed by 10 groups of 5 tile indices = 50 bytes. The routine replays them to PPU addresses `$2228`, `$2248`, `$2268`, `$2288`, `$22A8`, `$22C8`, `$22E8`, `$2308`, `$2328`, `$2348` — one run every 4 nametable rows. | `lda ($00),y` at `$CEA1` (command byte) and `$CECD` (each data byte) in `SubCCE98` | | `$D4E3–$D502` | 32 B | `MarkerSpriteTable` | 8 OAM records of 4 bytes (Y, tile, attribute, X), all with attribute `$03`: `(Y=$37,tile=$4D,X=$D0)`, `(Y=$37,tile=$4D,X=$C0)`, `(Y=$37,tile=$4E,X=$B0)`, `(Y=$37,tile=$4E,X=$A0)`, `(Y=$27,tile=$48,X=$88)`, `(Y=$37,tile=$4C,X=$88)`, `(Y=$2F,tile=$49,X=$80)`, `(Y=$2F,tile=$4B,X=$90)`. Copied into the OAM shadow at `$0200`–`$021F`. | `lda $D4E3,x` at `$C2A7` and `$C2B0` | | `$D503–$D562` | 96 B | - | Three more 32-byte OAM blocks, identical to `MarkerSpriteTable` except for the Y byte (`$67`, `$97`, `$C7`). | not referenced by any instruction | | `$D563–$D5C2` | 96 B | - | Three 32-byte OAM blocks whose records are Y=`$27`/`$57`/`$87`, tile `$30`, attribute `$00`, X=`$40`/`$50` — a vertical pair of 4-tile columns. | not referenced by any instruction | | `$D5C3` | 1 B | - | `$FF` sentinel separating the two marker sprite sets. | not referenced by any instruction | | `$D5C4–$D5E3` | 32 B | `MarkerSpriteTableAlt` | 8 OAM records of 4 bytes, byte-for-byte the same layout as `MarkerSpriteTable`. This is the marker strip of the two-row panel screen rather than the four-row one. Copied into the OAM shadow at `$0200`–`$021F`. | `lda $D5C4,x` at `$CB5D` and `$CB66` | | `$D5E4–$D603` | 32 B | - | 8 OAM records, the Y=`$67` companion of `MarkerSpriteTableAlt`. | not referenced by any instruction | | `$D604–$D643` | 64 B | - | 16 blank OAM records: every byte `$F4`, the sprite Y value the game uses to park a sprite off screen (`SubCC174` at `$C174` fills the whole OAM shadow with `$F4`). | not referenced by any instruction | | `$D644–$D6A3` | 96 B | - | Three 32-byte OAM blocks, identical to the `$D563`–`$D5C2` group (Y=`$27`/`$57`/`$87`, tile `$30`). | not referenced by any instruction | | `$D6A4` | 1 B | - | `$FF` sentinel separating the marker sprite sets from the button templates. | not referenced by any instruction | | `$D6A5–$D6C4` | 32 B | `ButtonLabelSpriteTemplate` | 8 OAM records of 4 bytes. All have Y=`$79` and attribute `$00`; the tiles are `$1D`,`$0E`,`$1C`,`$1D`,`$18`,`$1F`,`$0E`,`$1B` at X = `$18`,`$20`,`$28`,`$30`,`$40`,`$48`,`$50`,`$58` — a row of letter tiles from the sprite pattern table, i.e. the button labels. Copied into the OAM shadow at `$02E0`–`$02FF`. | `lda $D6A5,x` at `$C1A2` | | `$D6C5–$D6E4` | 32 B | `ButtonStateSpriteTemplate` | 8 OAM records of 4 bytes. All have Y=`$79` and attribute `$00`; the tiles are `$0C`,`$18`,`$17`,`$1D`,`$0B`,`$0A`,`$0D`,`$24` at X = `$18`,`$20`,`$28`,`$30`,`$40`,`$48`,`$50`,`$58` — the second row of button labels. Copied into the OAM shadow at `$02E0`–`$02FF`. | `lda $D6C5,x` at `$C1F4` | | `$D6E5–$FFF9` | 10,517 B | `RomPadding` | `$FF` fill to the end of the PRG image. | not referenced by any instruction | | `$FFFA–$FFFF` | 6 B | `CpuVectors` | The three interrupt vectors: NMI = `$CCF7` (`NmiHandler`), RESET = `$C000` (`Reset`), IRQ/BRK = `$C000` (the reset path again). | the CPU fetches them from `$FFFA`–`$FFFF` when an interrupt is taken | Three further inline stream pointers live **inside the code region** and are therefore still emitted as instructions by the Pass 1 tracer, so they carry no label: | Address | Size | Structure | Read by | |---------|------|-----------|---------| | `$C287–$C288` | 2 B | inline pointer `$CF06` (master palette), after the `jsr SubCCD3F` at `$C284` | `lda (Zp05),y` at `$CDD5`/`$CDDA` | | `$C28F–$C290` | 2 B | inline pointer `$D1F5` (panel screen three), after the `jsr SubCCD3F` at `$C28C` | `lda (Zp05),y` at `$CDD5`/`$CDDA` | | `$C2A0–$C2A1` | 2 B | inline pointer `$D4AF` (marker tile burst), after the `jsr SubCCE98` at `$C29D` | `lda (Zp05),y` at `$CDD5`/`$CDDA` | The Pass 1 tracer mis-decoded those three pairs as instructions (`asl ZpCF` at `$C287`, `sbc ZpD1,x` at `$C28F`, `lax $20D4` at `$C2A0`); they are the pointer bytes, not code. The bytes are left untouched so the rebuild stays byte-identical. ### Notable data structures in RAM (not ROM tables) - VRAM pointer at `$00`/`$01`, read indirectly by `SubCCD99` ($CD99), `SubCCD74` ($CD74) and `SubCCE98` ($CE98) - VRAM burst command buffer at `$0302`, read by `SubCCE4F` ($CE4F) and written by `SubCCEB2` ($CEB2); the limit at `$0300` is set to `$51` (81) at `$C294`, exactly the size of the 10×8-byte burst built from `MarkerTileBurstStream` - The OAM shadow at `$0200`–`$02FF`, filled with `$F4` by `SubCC174` ($C174) and then overwritten from the ROM sprite tables - The marker sprite Y positions computed at run time into `$0281`, `$0285`, `$0289`, `$028D`, `$0291`, `$0295`, `$0299`, `$029D` by `SubCC524` ($C524) and `SubCC597` ($C597) --- ## 9. Build ```bash cd "Joypad Test Cartridge (U)/copilot" make # → ca65 + ld65 + cmp reconstructed.nes rom.nes → "Byte-identical ROM verified." ``` --- ## 10. Notes - The NMI handler drives per-frame updates; the main loop spins waiting for NMI phase changes. - `RetAddrUpdate` ($CDC8) implements a return-address modification trick: it reads the return address from the stack, advances it by 2, and stores it back. This is used with `VramPtrWrite` ($CD3F) to implement runtime-computed control flow (effectively a coroutine/threaded dispatch). - The IRQ vector points to $C000 (Reset) and is effectively unused. - Large portions of the PRG are test pattern and button mapping data, not executable code. - `PollExpansionPortLoop` reads `$4017` and compares bits 0-1, but nothing consumes the result — the branch is always taken the same way. The read is present but inert. - `MarkerVar01`–`MarkerVar12` (zero page `$5A`, `$5B`, `$56`–$5F`, `$60`, `$61`, `$63`, `$64`) drive the twelve `MarkerUpdateN` indicator groups. Which player and button each belongs to is **not** established, so they are named by position rather than by button. - A draft Pass 2 `locals.inc` called `$55` a "P1 Down timer". The instructions do not support that: `ApplyMarkerValueEnableAudio` stores `$70`/`$88`/`$A0` there and `DispatchOnMarkerValue` compares it against `$70` and `$88`. It is `MarkerValue`. --- ## 11. Next Steps - **Pass 3 is complete.** Every data block in the PRG is interpreted and listed in the **Pass 3 data-table inventory** (section 8), the VRAM command stream record format is decoded from the consuming code, and the OAM record layout is documented. The blocks that no instruction in this dump reads are recorded as unreferenced data. - **MarkerVar mapping** (from `locals.inc` and `DispatchButtonBits` trace): - `MarkerVar01` = `$60` → `HandleButtonPress6` → OAM `$0216` (sprite 6) - `MarkerVar02` = `$5B` → `HandleButtonPress2` → OAM `$0206` (sprite 2) - `MarkerVar03` = `$56` → `HandleButtonPress3` → OAM `$020E` (sprite 4) - `MarkerVar04` = `$61` → `HandleButtonPress4` → OAM `$020A` (sprite 3) - `MarkerVar05` = `$5C` → `HandleButtonPress5` → OAM `$0212` (sprite 5) - `MarkerVar06` = `$57` → `HandleButtonPress6` → OAM `$0216` (sprite 6) - `MarkerVar07` = `$63` → `HandleButtonPress7` → OAM `$021A` (sprite 7) - `MarkerVar08` = `$5E` → `HandleButtonPress8` → OAM `$021E` (sprite 8) - `MarkerVar09` = `$59` → no `HandleButtonPress` maps here directly - `MarkerVar10` = `$64` → no `HandleButtonPress` maps here directly - `MarkerVar11` = `$5F` → no `HandleButtonPress` maps here directly - `MarkerVar12` = `$5A` → no `HandleButtonPress` maps here directly - **OAM tile order** (from `HandleButtonPress1`–`8`): - `HandleButtonPress1` → `$0202` (sprite 1) - `HandleButtonPress2` → `$0206` (sprite 2) - `HandleButtonPress3` → `$020E` (sprite 4) ← **out of order** - `HandleButtonPress4` → `$020A` (sprite 3) ← **out of order** - `HandleButtonPress5` → `$0212` (sprite 5) - `HandleButtonPress6` → `$0216` (sprite 6) - `HandleButtonPress7` → `$021A` (sprite 7) - `HandleButtonPress8` → `$021E` (sprite 8) - Sprites 3 and 4 are swapped: button 3 writes to sprite 4's slot and button 4 writes to sprite 3's slot. This is a genuine quirk of the game's OAM layout, not a disassembly error. ## 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 `$C000` (`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 | |---------|---------| | `$C005` | `WaitVblank2` (falls through) | | `$C00A` | `WaitVblank3` (falls through) | | `$C013` | `ClearRamPass1Loop` (falls through) | | `$CE3C` | `PpuScrollLatch` | | `$C055` | `InitTestScreen` (falls through) | | `$CD2F` | `PpuMaskSetup` | **NMI** is at `$CCF7` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$CD17` | `PpuCtrlSetup` | | `$CD24` | `OamDmaSetup` | | `$CE4F` | `VramBurstWrite` | **IRQ** is at `$C000` (`Reset`) - the same address as RESET, so the IRQ vector reaches the reset path. --- ## 9. Naming residue — address-derived entry points Every `jsr`/`jmp` target in this project now carries a semantic name except **15**, which keep the honest address-derived `SubCCXXXX` form. This is a diagnostic cartridge: the ROM is a small screen of button indicators and a controller shift register, and the code does not say which controller button each of the twelve indicator routines belongs to. | Label | Address | Why the dump does not settle the name | |--------|---------|-------------------------------------| | `SubCC523` | `$C523` | A bare `rts` — the tail of the marker-sprite copy routine. The cartridge gives it no code of its own. | | `SubCC524` | `$C524` | The first of twelve chained **button-indicator** routines. Each one owns a four-sprite block (the indicator's four marker cells) and one press counter in `$56`–`$65`, and each writes the tile values `$F4`, `$D4` and `$E0|counter`. The dump does not say which of the controller's eight buttons a given block stands for, so naming it after a button would be a guess. | | `SubCC597` | `$C597` | The second of the twelve chained button-indicator routines (counter `$5B`). | | `SubCC60A` | `$C60A` | The third of the twelve chained button-indicator routines (counter `$56`). | | `SubCC67D` | `$C67D` | The fourth of the twelve chained button-indicator routines (counter `$61`). | | `SubCC6F0` | `$C6F0` | The fifth of the twelve chained button-indicator routines (counter `$5C`). | | `SubCC763` | `$C763` | The sixth of the twelve chained button-indicator routines (counter `$57`). | | `SubCC7D6` | `$C7D6` | A bare `rts` — the tail of one of the twelve chained indicator routines. | | `SubCC7D7` | `$C7D7` | The seventh of the twelve chained button-indicator routines (counter `$63`). | | `SubCC84A` | `$C84A` | The eighth of the twelve chained button-indicator routines (counter `$5E`). | | `SubCC8BD` | `$C8BD` | The ninth of the twelve chained button-indicator routines (counter `$59`). | | `SubCC930` | `$C930` | The tenth of the twelve chained button-indicator routines (counter `$64`). | | `SubCC9A3` | `$C9A3` | The eleventh of the twelve chained button-indicator routines (counter `$5F`). | | `SubCCA16` | `$CA16` | The twelfth of the twelve chained button-indicator routines (counter `$5A`). | | `SubCCA89` | `$CA89` | A bare `rts` — the tail of the last of the twelve chained indicator routines. | The other 33 entry points are named: `ClearRamAndOam`, `ResetInit` (the `Reset` path in the source), `NmiHandler`, `EnableNmiAndStrobeControllers`, `ScanControllerButtonBits`, `PlayTestTone`, `ClearOamRoutine`, `CopyMarkerSpriteTable1`, `CopyMarkerSpriteTable2`, `FindPressedButton`, `ClearMarkerRam`, `UpdateButtonSprites`, `CheckAnyButtonPressed`, `CountPressedButtons`, `ResetFrameCounter`, `ResetButtonCounters`, `ShiftMarkerBitToOam`, `PollControllerAndSilenceApu`, `SilenceApuWhenNoButtonPressed`, `StrobeController1`, `ReadController1Bit`, `StrobeController2`, `EnablePpuNmi`, `PpuCtrlSetup`, `OamDmaSetup`, `PpuMaskSetup`, `SetPpuMaskBits`, `VramPtrWrite`, `RetAddrUpdate`, `VramWrite`, `PpuScrollLatch`, `VramBurstWrite`, `PlayVramStream` and `EndVramStream`.