- Decoded the VramWrite ($E1CC) stream format: <addr hi> <addr lo> <count> <bytes>, $00 terminator - Named all 9 payload streams ($E4CC-$E7C2, no gaps/overlaps) plus VramWriteSourcePointerTable at $F000 - Split PadBitTableLives into Player1TileTable ($E4B4) and Player2TileTable ($E4C0), 12 entries each (Y=0..11) - Decoded CHR font (digits $30-$39, letters $3A-$52, space $5F; 'W' missing from the A-Z run) and recovered on-screen text including 'PRESS A TO TEST SIDE-A' / 'SIDE-B' - Documented that the 16 KiB PRG is two byte-identical 8 KiB halves; $C000 copy is dead - Documented 7 unreached subroutines inside the live code region (kept as .byte per Pass-1 policy) - Fixed a stray duplicate sentence and stale _wip paths in root OVERVIEW.md Byte-identical ROM verified.
11 KiB
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 | Horizontal |
| PRG bank start | CPU $C000 |
| Source file | copilot/prg.s (~1,650 lines with headers) |
| Output | reconstructed_prg.bin (16,384 bytes) |
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_prg.bin matches prg.bin exactly (all 16,384 bytes), and prg.bin matches the PRG bank of NES PowerPad Test Cart (U) [!].nes.
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 | All data tables named: VramWriteSourcePointerTable ($F000, 9 streams), Player1TileTable/Player2TileTable ($E4B4/$E4C0), PaletteData ($E121), and all nine VramWrite payload streams (PowerPadSideAButtonGraphics, PanelFrameTiles, StatusMessageText, SideATestPromptText, …). Screen text decoded from CHR. Remaining 46 Label_Exxxx are internal loop/branch targets. |
4. Reset — CPU $FFFC → $E000
Reset ($E000)
On power-on or reset:
- Calls a chain of initialization subroutines in sequence:
ResetInit($E09B): sei/cld, disable PPU rendering, wait for two vblanksClearRAM($E0C9): clear 8 pages of zero-page RAMClearVRAM($E0EB): clear nametable memoryLoadPalette($E107): load 32 bytes of palette data into PPU palette RAMClearVRAM_Fill23($E142): fill VRAM with $23 (blank tile attribute)InitAPU($E16B): initialize APU pulse/triangle channelsClearVRAM_FillFF($E187): fill VRAM with $FF- Five
LoadPatternTableroutines ($E19A–$E1CB): load pattern tables from $F000 region SetPPUCTRL_NMI($E219): enable NMI bit in PPUCTRL
- Main loop (
Label_E058at $E058): callsClearScreen($E4FA),WaitForVBlank($E092), checks$07(frame counter high nibble), loops back to $E000 if zero, otherwise callsResetScroll($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:
- Save registers (
PHA / TXA / PHA / TYA / PHA). - Calls
DisableNMI($E1FB) — clears NMI bit in PPUCTRL shadow. - Increments
Zp03(frame counter). - Reads
PPUSTATUS($2002), stores toZp02(vblank flag). - Calls
ReadPad($E338) — reads controller pad data. - Calls
ShiftPads($E362) — shifts pad data into state variables. - Calls
ProcessPads($E398) — processes pad transitions, updates scores. - Calls
DrawScore($E3E1) — draws score digits. - Calls
DrawLives($E407) — draws lives indicators. - 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 |
EnableNMI |
$E1F1 | Enables NMI bit in PPUCTRL shadow |
DisableNMI |
$E1FB | Disables NMI bit in PPUCTRL shadow |
SetPPUCTRL_NMI |
$E219 | Sets NMI bit ($80) in PPUCTRL shadow |
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 |
ShiftVar1–4 |
$E433–$E47E | Shift pad bits into score accumulators |
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
8.1 Direct tables
| Table | Address | Size | Description |
|---|---|---|---|
PaletteData |
$E121 | 32 | Palette loaded into PPU $3F00-$3F1F by LoadPalette |
Player1TileTable |
$E4B4 | 12 | PPUADDR low bytes for player 1, indexed Y=0..11 |
Player2TileTable |
$E4C0 | 12 | Same for player 2; $00 = "skip this button" |
VramWriteSourcePointerTable |
$F000 | 18 | 9 little-endian stream pointers, one per LoadPatternTable* / draw call site |
8.2 VramWrite payload streams
VramWrite ($E1CC) reads records of <addr hi> <addr lo> <count> <count bytes>, terminated by a $00 at a record boundary. All nine streams tile $E4CC-$E7C2 with no gaps and no overlaps:
| Slot | Ptr | Stream label | Address | Records | Writes to |
|---|---|---|---|---|---|
| 0 | $F000 | PowerPadSideAButtonGraphics |
$E54F | 2 | $2083, $20A3 — 52 tile graphics in pattern table 0 |
| 1 | $F002 | PowerPadButtonLabelTiles |
$E58A | 4 | $2136, $2156, $2236, $2256 — button captions |
| 2 | $F004 | PanelFrameTiles |
$E5B3 | 8 | $2124…$22C4 — panel borders for six indicator boxes |
| 3 | $F006 | SideAPressedButtonRow |
$E64C | 6 | $2184…$22A4 — side-A button caps |
| 4 | $F008 | SideAPanelButtonRow |
$E6BF | 6 | same rows, alternate layout |
| 5 | $F00A | StatusMessageText |
$E732 | 3 | $2121, $2163, $2183 — status messages |
| 6 | $F00C | Player1ButtonIconStream |
$E4CC | 2 | $2196, $21B6 |
| 7 | $F00E | Player2ButtonIconStream |
$E4E3 | 2 | $2296, $22B6 |
| 8 | $F010 | SideATestPromptText |
$E790 | 2 | $2304, $2344 — the prompt rows |
8.3 Screen text (decoded from CHR)
Font mapping read directly out of chr.bin pattern table 1: digits $30-$39 = '0'-'9', letters $3A-$52 = 'A'-'Z' (note W is absent from the A-Z run), $57 = -, $58 = /, $59 = \, $5F = space.
| Address | Text |
|---|---|
| $2121 | CONNECT POWER PAD <$5D> CONTROLLER |
| $2163 | <$60>IF POWER PAD IS CONNECTED<$62> |
| $2183 | <$61> IC MAY BE BAD <$53> <$63> |
| $2304 | PRESS A TO TEST SIDE-A |
| $2344 | PRESS B TO TEST SIDE-B |
Tiles $53-$5E are truncated glyph fragments in CHR ROM — including the W of POWER, which sits at $5E and renders incomplete. The original cartridge therefore displays partial marks at those positions; this is a ROM authoring artifact, reproduced faithfully.
9. Build
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$E000copy; nothing references$C000-$DFFF, so the first copy is dead and is kept as classified.bytedata rather than disassembled. - 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.bytedata; each is decoded in theprg.sheader comment block. - The PRG is approximately 7.2% code / 92.8% data. Most data is
$FFpadding from$E7C3-$EFFFand$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
- Optional: refine the
Zp05–Zp0Faliases now thatProcessPads/DrawScore/DrawLivessemantics are known (pad-state bytes vs. shift counters). - Optional:
$E4CC-$E4F9holds 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.