# NES PowerPad Test Card (U) — Game Flow from Power-On This document describes the execution flow of the disassembled NES PowerPad Test Card (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` (~1,650 lines with headers) | | Output | `reconstructed.nes` (whole ROM: 16-byte header + 16,384-byte PRG + 8,192-byte CHR) | ### Interrupt vectors (CPU $FFFA–$FFFF) | Vector | Address | Handler | |--------|---------|---------| | NMI | $E06E | `NmiHandler` | | RESET | $E000 | `Reset` | | IRQ / BRK | $E000 | 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** — `reconstructed.nes` matches `rom.nes` exactly (header + PRG + CHR). --- ## 3. Pass Status | Pass | Status | Description | |------|--------|-------------| | Pass 1 | Complete | Code/data split — entire PRG bank classified as traced code (~7.2%) or explicit `.byte` data (~92.8%) with `; CPU:$xxxx PRG:$xxxx` annotations | | Pass 2 | Complete | Routine headers added for all labeled positions; key routines semantically named (`Reset`, `NmiHandler`, `VramWrite`, `ReadPad`, `ProcessPads`, etc.); zero-page aliases in `locals.inc` | | Pass 3 | Complete | Every data block in the PRG is interpreted and listed in the **Pass 3 data-table inventory** (section 8). Named: the 32-byte master palette, the two 12-entry controller-indicator position tables, the nine `VramWrite` payload streams (the pad-button graphic strips, the indicator-box frames, the button captions, the two player button-row layouts, the three status messages and the two side-test prompts), the nine-entry stream pointer table, the `$FF` ROM fill and the vectors. The `VramWrite` record format is decoded from the consuming code: `[PPU address high][PPU address low][count][count bytes]`, terminated by a `$00` byte at a record boundary. The CHR font was used as the oracle to decode the screen text. | --- ## 4. Reset — CPU $FFFC → $E000 **`Reset`** ($E000) On power-on or reset: 1. Calls a chain of initialization subroutines in sequence: - `ResetInit` ($E09B): sei/cld, disable PPU rendering, wait for two vblanks - `ClearRAM` ($E0C9): clear 8 pages of zero-page RAM - `ClearVRAM` ($E0EB): clear nametable memory - `LoadPalette` ($E107): load 32 bytes of palette data into PPU palette RAM - `ClearVRAM_Fill23` ($E142): fill VRAM with $23 (blank tile attribute) - `InitAPU` ($E16B): initialize APU pulse/triangle channels - `ClearVRAM_FillFF` ($E187): fill VRAM with $FF - Five `LoadPatternTable` routines ($E19A–$E1CB): load pattern tables from $F000 region - `EnableNmi` ($E219): enable NMI bit in PPUCTRL 2. **Main loop** (`Label_E058` at $E058): calls `ClearScreen` ($E4FA), `WaitForVBlank` ($E092), checks `$07` (frame counter high nibble), loops back to $E000 if zero, otherwise calls `ResetScroll` ($E22D) and jumps back to $E058. **Key point:** The main loop is driven by vblank waits and frame counter checks. All per-frame work (controller reading, pad processing, screen drawing) happens inside the NMI handler. --- ## 5. NMI / Frame Heartbeat — `NmiHandler` ($E06E) The NMI fires once per frame: 1. **Save registers** (`PHA / TXA / PHA / TYA / PHA`). 2. Calls `HideBackground` ($E1FB) — clears the background-enable bit ($08) of the PPUMASK shadow, so the VRAM updates below do not tear the picture. 3. Increments `Zp03` (frame counter). 4. Reads `PPUSTATUS` ($2002), stores to `Zp02` (vblank flag). 5. Calls `ReadPad` ($E338) — reads controller pad data. 6. Calls `ShiftPads` ($E362) — shifts pad data into state variables. 7. Calls `ProcessPads` ($E398) — processes pad transitions, updates scores. 8. Calls `DrawScore` ($E3E1) — draws score digits. 9. Calls `DrawLives` ($E407) — draws lives indicators. 10. **Restore registers** (`PLA / TAX / PLA / TAY / PLA / RTI`). --- ## 6. Key Subroutines | Label | Address | Description | |-------|---------|-------------| | `Reset` | $E000 | Cold-start initialization and main loop entry | | `NmiHandler` | $E06E | Per-frame NMI: pad read, process, score/lives draw | | `WaitForVBlank` | $E092 | Waits for vblank flag, clears it | | `ResetInit` | $E09B | Cold-start hardware init (PPU, APU, vblank waits) | | `ClearRAM` | $E0C9 | Clears 8 pages of zero-page RAM | | `ClearVRAM` | $E0EB | Clears nametable memory with zeros | | `LoadPalette` | $E107 | Loads 32-byte palette into PPU palette RAM | | `ClearVRAM_Fill23` | $E142 | Fills VRAM with blank tile value $23 | | `InitAPU` | $E16B | Initializes APU pulse/triangle channels | | `ClearVRAM_FillFF` | $E187 | Fills VRAM with $FF | | `LoadPatternTable1`–`5` | $E19A–$E1CB | Load pattern tables from $F000 region | | `VramWrite` | $E1CC | Generic VRAM write via indirect pointer | | `ShowBackground` | $E1F1 | Sets the background-enable bit ($08) of the PPUMASK shadow ($2001) | | `HideBackground` | $E1FB | Clears the background-enable bit of the PPUMASK shadow ($2001) | | `EnableNmi` | $E219 | Sets NMI bit ($80) in PPUCTRL shadow ($2000) | | `ResetScroll` | $E22D | Resets PPU scroll X/Y to zero | | `InitRoundParams` | $E236 | Initializes round parameters | | `DrawPlayer1Pad` | $E270 | Draws player 1 pad indicators | | `DrawPlayer2Pad` | $E2B0 | Draws player 2 pad indicators | | `ClearRows` | $E2F0 | Clears screen rows with blank tiles | | `ClearGameVars` | $E32D | Clears game variables $06–$09 | | `ReadPad` | $E338 | Reads controller pad via shift register | | `ShiftPads` | $E362 | Shifts pad data into state variables | | `ProcessPads` | $E398 | Processes pad transitions, updates scores | | `DrawScore` | $E3E1 | Draws score digits on screen | | `DrawLives` | $E407 | Draws lives indicators on screen | | `BeginScoreShift`/`ShiftScoreWithCarry`/`ShiftScoreNoCarry` | $E433–$E44B | Shift the score digit into the score accumulator | | `BeginLivesShift`/`ShiftLivesWithCarry`/`ShiftLivesNoCarry` | $E459–$E471 | The same shift for the lives counter | | `UpdatePads` | $E47F | Updates pad state, checks button transitions | | `LoadPatternTable6`–`7` | $E4A0–$E4B3 | Load additional pattern tables | | `ProcessPadBits` | $E4FA | Process pad bit data for transitions | | `SetAPUNoise` | $E52E | Sets APU noise channel | | `ClearAPUNoise` | $E534 | Clears APU noise channel | | `ClearScreen` | $E53A | Clears screen with blank tile $5F | --- ## 7. Zero-Page Aliases (`locals.inc`) | Alias | Address | Role | |-------|---------|------| | `Zp00` | $00 | PPUCTRL shadow register | | `Zp01` | $01 | PPUMASK shadow register | | `Zp02` | $02 | VBlank flag | | `Zp03` | $03 | Frame counter / NMI tick | | `Zp05` | $05 | Score/digit temp | | `Zp06` | $06 | Pad 1 state byte 1 | | `Zp07` | $07 | Pad 1 state byte 2 | | `Zp08` | $08 | Pad 1 state byte 3 | | `Zp09` | $09 | Pad 1 state byte 4 | | `Zp0A` | $0A | Pad 2 state byte 1 | | `Zp0B` | $0B | Pad 2 state byte 2 | | `Zp0C` | $0C | Pad temp / swap variable | | `Zp0D` | $0D | Pad mode / draw flag | | `Zp0E` | $0E | Shift counter / temp | | `Zp0F` | $0F | Delay counter | | `Zp10` | $10 | VRAM pointer low byte | | `Zp11` | $11 | VRAM pointer high byte | | `Zp12` | $12 | Score digit 1 accumulator | | `Zp13` | $13 | Score digit 2 accumulator | | `Zp14` | $14 | Score digit 3 accumulator | | `Zp15` | $15 | Score digit 4 accumulator | Full list in `copilot/locals.inc`. --- ## 8. Data Tables and Pass 3 data-table inventory The PRG is **8.3% code / 91.7% data** (1,353 / 15,031 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 `VramWrite` record format `VramWrite` (`$E1CC`) replays a byte stream into the PPU: * `stx Zp10 / sty Zp11` (`$E1CC`) take the stream address; the nine stream addresses live in the pointer table at `$F000`. * `lda (Zp10),y` at `$E1D2` reads the record's **PPU address high byte**. `beq SubCE1F0` at `$E1D4` ends the stream when that byte is `$00`. * `iny` / `lda (Zp10),y` at `$E1D9`/`$E1DA` read the **address low byte**, which is written to `$2006` at `$E1DC`. * `iny` / `lda (Zp10),y` / `tax` at `$E1DF`–`$E1E2` read the **byte count**. * The loop at `$E1E3`–`$E1EA` writes that many bytes to `$2007` (`$E1E6`), each read with `lda (Zp10),y` at `$E1E4` and each preceded by the `iny` at `$E1E3`. So a record is `[addr high][addr low][count][count bytes]`, and a `$00` byte at a record boundary terminates the stream. ### The two 12-entry position tables `SubCE3E1` (`$E3E1`) draws the "pressed" marker and `SubCE407` (`$E407`) draws the "released" marker. Both set the PPU address to `$23xx` and then take the **low byte** from one of the two 12-entry tables, indexed by `Y`. `Y` runs 0-7 over the eight bits of the first controller port (`iny` at `$E3B5`) and 8-11 over the upper nibble of the second (`iny` at `$E3D8`), so the tables have one entry per reported bit. A `$00` entry means "this bit has no indicator for this port" and the draw is skipped (`beq` at `$E3F0` / `$E416`). ### Pass 3 data-table inventory | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | `$C06E–$DFFF` | 8,082 B | - | The **dead first copy** of the PRG image. The 16 KiB image is two byte-identical 8 KiB halves (`prg[0x0000:0x2000] == prg[0x2000:0x4000]`), so this block is a byte-for-byte mirror of the live half's `$E06E–$FFFF`: the same code, the same tables, the same `$FF` fill and the same vectors (NMI `$E06E`, RESET `$E000`, IRQ `$E000`). The tracer reached `$C000–$C06D` because it was seeded there; the rest of the copy was left as data. | not referenced by any instruction — the RESET vector at `$FFFC` points at `$E000`, so the CPU never fetches from this copy | | `$E0C5–$E0C8` | 4 B | - | `ldx #$80 / txs / rts` — a stack-pointer initialiser. It decodes cleanly as three instructions and no jump or call in the dump reaches it, so this is **code the tracer could not reach**, not data. | not referenced by any instruction | | `$E121–$E140` | 32 B | `MasterPalette` | The complete 2C02 master palette: the backdrop byte followed by the three colour bytes of each of the four background palettes and each of the four sprite palettes, i.e. the image that occupies `$3F00–$3F1F`. | `lda $E121,x` at `$E113` (32 iterations, `cpx #$20` at `$E11A`), stored to `$2007` at `$E116`; the PPU address is set to `$3F00` by `sta PPUADDR` at `$E109`/`$E10E` | | `$E20F–$E218` | 10 B | - | `lda $01 / and #$EF / sta $2001 / sta $01 / rts` — a "hide the background" routine (bit 4 of the PPUMASK shadow cleared, the mirror image of `SubCE205` at `$E205`). It decodes cleanly as five instructions and no jump or call reaches it, so this is **code the tracer could not reach**. | not referenced by any instruction | | `$E4B4–$E4BF` | 12 B | `Player1IndicatorPositionTable` | 12 bytes, one per reported controller bit: the **low byte of the PPU address** (`$23xx`) at which that bit's on-screen indicator is drawn — `$E3`, `$EB`, `$EA`, `$E2`, `$E9`, `$E1`, `$D9`, `$DA`, `$DC`, `$DB`, `$EC`, `$E4`. No entry is `$00`, so all twelve bits of the first port have an indicator. | `lda $E4B4,y` at `$E3F5` and `$E421` | | `$E4C0–$E4CB` | 12 B | `Player2IndicatorPositionTable` | The same 12-entry layout for the second port: `$E2`, `$EA`, `$EB`, `$E3`, `$00`, `$E4`, `$00`, `$DB`, `$00`, `$DA`, `$00`, `$E1`. Four entries are `$00`, meaning bits 4, 6, 8 and 10 have no indicator for that port. | `lda $E4C0,y` at `$E3ED` and `$E416` | | `$E4CC–$E4E2` | 23 B | `Player1ButtonIconStream` | A `VramWrite` stream: two records — PPU address `$2196` with 8 tiles and `$21B6` with 8 tiles — then the `$00` terminator. The tiles are the rounded button-cap shapes `$B0`,`$B1`,`$BC`,`$BD`,`$D4`,`$D5`,`$C0`,`$C1`,`$CC`,`$CD`,`$E4`,`$E5`. | `lda (Zp10),y` at `$E1D2`, `$E1DA`, `$E1E0` and `$E1E4` inside `VramWrite`, entered with the pointer from `ldx $F00C / ldy $F00D` at `$E4A0` | | `$E4E3–$E4F9` | 23 B | `Player2ButtonIconStream` | The same two records at `$2296` and `$22B6`, with the same tiles. | `VramWrite` entered with the pointer from `ldx $F00E / ldy $F00F` at `$E4AA` | | `$E54F–$E589` | 59 B | `PadButtonGraphicStripStream` | Two records of 26 tiles at `$2083` and `$20A3` — the two rows of pad-button artwork at the top of the screen. Each row is three groups of shapes separated by the blank tile `$04`: 10 tiles, blank, 6 tiles, blank, 8 tiles. | `VramWrite` entered with the pointer from `ldx $F000 / ldy $F001` at `$E19A` | | `$E58A–$E5B2` | 41 B | `ButtonCaptionTileStream` | Four records of 7 tiles at `$2136`, `$2156`, `$2236` and `$2256` — the captions under the four indicator boxes. | `VramWrite` entered with the pointer from `ldx $F002 / ldy $F003` at `$E1A4` | | `$E5B3–$E64B` | 153 B | `IndicatorBoxFrameTileStream` | Eight records of 16 tiles at `$2124`, `$2144`, `$2164`, `$21C4`, `$21E4`, `$2244`, `$2264` and `$22C4` — the borders of the eight indicator boxes, built from the corner and edge tiles `$07`–`$1C`. | `VramWrite` entered with the pointer from `ldx $F004 / ldy $F005` at `$E1AE` | | `$E64C–$E6BE` | 115 B | `Player2ButtonRowStream` | Six records of 16 tiles at `$2184`, `$21A4`, `$2204`, `$2224`, `$2284` and `$22A4` — the button caps of the six indicator boxes in the **second** player's layout. The solid tiles `$00`/`$01` and the diagonal shape tiles `$10`/`$11` sit in different columns here than in the first player's layout. | `VramWrite` entered with the pointer from `ldx $F006 / ldy $F007` at `$E2B3`, inside `SubCE2B0` (the player-2 pad routine) | | `$E6BF–$E731` | 115 B | `Player1ButtonRowStream` | The same six addresses in the **first** player's layout. | `VramWrite` entered with the pointer from `ldx $F008 / ldy $F009` at `$E273`, inside `SubCE270` (the player-1 pad routine) | | `$E732–$E78F` | 94 B | `StatusMessageTextStream` | Three records of text: 30 tiles at `$2121`, 27 tiles at `$2163` and 27 tiles at `$2183`. Decoded in 8.3 below. | `VramWrite` entered with the pointer from `ldx $F00A / ldy $F00B` at `$E1B8` | | `$E790–$E7C2` | 51 B | `SideTestPromptTextStream` | Two records of 22 tiles: `$2304` = "PRESS A TO TEST SIDE-A" and `$2344` = "PRESS B TO TEST SIDE-B". | `VramWrite` entered with the pointer from `ldx $F010 / ldy $F011` at `$E1C2` | | `$E7C3–$EFFF` | 2,109 B | `RomPadding` | `$FF` fill between the last stream and the pointer table. | not referenced by any instruction | | `$F000–$F011` | 18 B | `VramStreamPointerTable` | Nine little-endian 16-bit pointers to the nine `VramWrite` streams, in the order `$F000`→`$E54F`, `$F002`→`$E58A`, `$F004`→`$E5B3`, `$F006`→`$E64C`, `$F008`→`$E6BF`, `$F00A`→`$E732`, `$F00C`→`$E4CC`, `$F00E`→`$E4E3`, `$F010`→`$E790`. | `ldx $F000 / ldy $F001` at `$E19A`; `$F002/$F003` at `$E1A4`; `$F004/$F005` at `$E1AE`; `$F006/$F007` at `$E2B3`; `$F008/$F009` at `$E273`; `$F00A/$F00B` at `$E1B8`; `$F00C/$F00D` at `$E4A0`; `$F00E/$F00F` at `$E4AA`; `$F010/$F011` at `$E1C2` | | `$F012–$FFF9` | 4,072 B | `RomPaddingTail` | `$FF` fill between the pointer table and the vectors. | not referenced by any instruction | | `$FFFA–$FFFF` | 6 B | `CpuVectors` | The three interrupt vectors: NMI = `$E06E` (`NmiHandler`), RESET = `$E000` (`Reset`), IRQ/BRK = `$E000` (the reset path again). | the CPU fetches them from `$FFFA`–`$FFFF` when an interrupt is taken | ### 8.1 The screen text, decoded from the CHR font The background pattern table is CHR `$1000` (PPUCTRL is `$10`, set at `$E0A6`). Reading the CHR as the oracle gives: | Tile | Character | |------|-----------| | `$30`–`$39` | `0`–`9` | | `$3A`–`$4F` | `A`–`V` | | `$50` | `X` | | `$51` | `Y` | | `$52` | `Z` | | `$53` | blank — the 'W' that should follow 'V' is **not** in the A–Z run | | `$54`–`$56` | blank | | `$57` | `-` (a dash drawn in the upper half of the cell) | | `$58` | `\` | | `$59` | `/` | | `$5A` | `?` | | `$5B`–`$5D` | non-alphabetic fragments (the CHR holds broken glyphs here) | | `$5E` | `W` — the 'W' of the alphabet, placed **out of order** after the fragments | | `$5F` | space (blank cell) | | `$60`–`$63` | four diagonal line fragments, used as the message brackets | With that mapping the three status lines decode as: | Address | Tiles | Text | |---------|-------|------| | `$2121` | 30 | `CONNECT POWER PAD <$5D> CONTROLLER` — the 'W' of "POWER" is tile `$5E`, and the `$5D` fragment stands where a connective character belongs | | `$2163` | 27 | `<$60>IF POWER PAD IS CONNECTED<$62>` | | `$2183` | 27 | `<$61> IC MA<$59> BE BAD <$53> <$63>` — the intended word is "MAY", but tile `$59` holds a diagonal slash in this ROM's CHR, so the screen shows "IC MA/ BE BAD" | | `$2304` | 22 | `PRESS A TO TEST SIDE-A` | | `$2344` | 22 | `PRESS B TO TEST SIDE-B` | The incomplete letters are a ROM authoring artifact and are reproduced faithfully. ### 8.2 Data structures in RAM (not ROM tables) - The stream pointer in `$10`/`$11`, written by `stx Zp10 / sty Zp11` at `$E1CC`/`$E1CE` and read indirectly by `VramWrite` - The indicator tables' counterparts in RAM: the score accumulators `$12`–`$15`, initialised by `SubCE236` (`$E236`) - The player-1 / player-2 indicator positions are **not** in RAM — they are the two ROM tables above --- ## 9. Build ```bash cd "NES PowerPad Test Card (U)/copilot" make # → ca65 + ld65 + cmp reconstructed.nes rom.nes → "Byte-identical ROM verified." ``` --- ## 10. Notes - **The 16 KiB PRG is two byte-identical 8 KiB halves.** `prg[0x0000:0x2000] == prg[0x2000:0x4000]` is exactly true. Both halves hold the same program, pointer table, and vector set. The vectors point at `$E06E`/`$E000`/`$E000`, so the CPU always enters the `$E000` copy; nothing references `$C000-$DFFF`, so the first copy is dead and is kept as classified `.byte` data rather than disassembled. The tracer was seeded at `$C000` as well, so `$C000-$C06D` is classified as code — it is the duplicate of `$E000-$E06D` and is never fetched. - Unreached gaps inside the live code region decode as valid subroutines (PPUMASK show/hide background, PPUCTRL NMI-disable, frame-counter digit tiles, `ldx #$80 / txs`). Per Pass-1 policy they stay `.byte` data; each is decoded in the `prg.s` header comment block and in the inventory above. - The PRG is approximately 8.3% code / 91.7% data. Most data is `$FF` padding from `$E7C3-$EFFF` and `$F012-$FFF9`. - The NMI handler drives per-frame updates; the main loop spins waiting for vblank and frame-counter changes. - IRQ vector points to $E000 (`Reset`) and is effectively unused. --- ## 11. Next Steps - **Pass 3 data tables are done.** Every data block in the PRG is interpreted and listed in section 8. - Optional: refine the `Zp05`–`Zp0F` aliases now that `ProcessPads`/`DrawScore`/`DrawLives` semantics are known (pad-state bytes vs. shift counters). - Optional: `$E4CC-$E4F9` holds two near-identical icon streams differing only in two bytes; could be collapsed to a parameterized table in a future reconstruction, but the byte form is preserved as-is. - Optional: the two unreachable subroutines at `$E0C5` and `$E20F` could be re-classified as code if the tracer were re-run with them as extra seeds; the bytes are left untouched so the rebuild stays byte-identical. ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry. --- ## Naming residue — address-derived entry points Every `jsr`/`jmp` target in this project now carries a semantic name except **1**, which keeps the honest address-derived form: | Label | Address | Why the dump does not settle the name | |--------|---------|-------------------------------------| | `SubCE141` | `$E141` | A bare `rts` with no code of its own, reached only as the fall-through tail of the palette loader. The cartridge gives it nothing to be named after. | The other 47 entry points are named after what they do: `MainLoop`, `ResetInit`, `ClearRam`, `ClearVram`, `LoadPalette`, `ClearVramWithBlankTile`, `InitApu`, `ClearVramWithTileFF`, `LoadPatternTable1`–`LoadPatternTable7`, `VramWrite`, `VramWriteLoop`, `ShowBackground`, `HideBackground`, `EnableNmi`, `ResetScroll`, `InitRoundParams`, `WriteScoreDigit`, `DrawPlayer1Pad`, `DrawPlayer2Pad`, `ClearRows`, `ClearGameVars`, `ReadPad`, `ShiftPads`, `ProcessPads`, `DrawScore`, `DrawScoreTile`, `DrawLives`, `DrawLivesTile`, `BeginScoreShift`, `ShiftScoreWithCarry`, `ShiftScoreNoCarry`, `BeginLivesShift`, `ShiftLivesWithCarry`, `ShiftLivesNoCarry`, `UpdatePads`, `ProcessPadBits`, `RedrawPlayer1Pad`, `RedrawPlayer2Pad`, `SetApuNoise`, `ClearApuNoise` and `ClearScreen`. Note on two names that the earlier draft of this document got wrong: the routines at `$E1F1` and `$E1FB` write **$2001 (PPUMASK)**, not $2000, so they turn the background on and off — they are named `ShowBackground` and `HideBackground` here, not `EnableNMI`/`DisableNMI`. Only `$E219`, which writes $2000 with bit 7 set, enables the PPU's NMI.