# Pyramid (U) — Pass 1 ROM: `_wip/Pyramid (U)/Pyramid (U) (Sachen).nes` (24,592 bytes, iNES). Status: **PASS 1 — code/data split complete, byte-identical.** 5,834 of 16,384 PRG bytes (35.6%) are classified as traced 6502 code (396 labels, 70 distinct `jsr` targets); everything else is explicit `.byte` data with per-line PRG-offset annotations. `make` → `cmp reconstructed_prg.bin prg.bin` → **BYTE-IDENTICAL**. ## 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 | Horizontal | | Battery-backed flag | clear | | ROM SHA-256 | `8e042dc55093e47547ff9fa26e7fec2c3fb94ebac0d0686cc8bb6c2749a6066d` | | PRG SHA-256 | `251688d69009091c4a37e831e0fd6114bb1dc9d1bace98032c1b9a3030b5e378` | ## Interrupt Vectors (taken from PRG $3FFA–$3FFF) | Vector | Target | Note | |--------|--------|------| | NMI | `$D72E` | normal per-frame engine entry (waits PPU, runs frame state, drives APU silence/queue) | | RESET | `$FF00` | real cold-start code — see boot chain below | | IRQ | `$FF00` | redirects to RESET target; `sei` at boot, no `cli` seen on the traced path — IRQ likely never fires | ## Boot chain (all statically traced from `$FF00`) 1. `ldx #$FF / txs / sei / cld`, clear PPUCTRL/PPUMASK. 2. Repeated `LDA # / JSR $FF49` — `$FF49` does **`STA $4100`** (write beyond the NES register space, $4000–$4017) followed by a long delay loop ($CE×256-ish iterations ×4 outer). The header claims mapper 0, but on the real Sachen cart these `$4100` pokes are an mapper-unlock/protection sequence. Boot pokes `$30`, `$10/$30/$10+$30` (via `$FF4C` delay variant twice) or `$20/$30/$20` depending on a check. 3. `JSR $FF57` — reads stack page bytes `$0120`/`$0121` and compares against `$41`/`$56` ("A","V"). `SEC` ⇒ proceeds to the `$4100` poke sequence above; `CLC` ⇒ alternate poke sequence, then falls into: 4. `JSR $FF69` — seeds `$0120`/`$0121` with `$41`/`$56`. 5. `JMP $C800` — real init: waits two `$2002` vblank phases, clears `$0000-$07FF`, builds ZP pointer `$D7/$D8 = $C8xx`, zeroes `$00`, writes `$98` to PPUCTRL / `$1A` to PPUMASK. `$FF74-$FF7C` (`LDA #$00 / STA $0120 / STA $0121 / RTS`) clears the checker bytes; never called from static paths — emitted as data in the tail block. ## Dispatcher architecture (two queue + `jmp ($0002)` dispatchers) The engine is the classic *queued-task* pattern (same family as the repo's Burger Time / DK Jr. analysis): | Dispatcher | Site | Pointer | Vector table | Entries | |-----------|------|---------|--------------|---------| | 1 | `$C3A3` | `$0002/$0003` | `$C3A6` | 9: `$C3B6 $C3C7 $C3FD $C35F $C3E3 $C3E7 $C3EB $C3EF $D820` | | 2 | `$CCCA` | `$0002/$0003` | `$CCD5` | 8: `$CCEE $CD27 $CC90 $CD75 $CD43 $CDA4 $CE1A $CE5E` | Both build the pointer with the `TXA / ASL / TAX / LDA $tbl,X / STA $02 / LDA $tbl+1,X / STA $03 / LDX $45 / LDY $46 / jmp ($0002)` idiom; dispatcher 2 additionally copies an 8-byte low-value table from `$CCCD,X` into `$04EA,X` and syncs `$0430/$0431` (X/Y "cursor" state) before the jump. The `jmp ($0002)` sites terminate static traces; the tail at `$CCEB` (`jsr $D2BF / jsr $D476 / jmp $CC90`) re-queues into dispatcher 2. **Pass-1 candidates not statically resolvable** (emitted as code where entered only via runtime dispatch, documented for Pass 2 review): `$D7EB` (`EA EA A2 FF 9A / jmp $D655` — alternate cold-start stub, caller unknown), `$C201` (PPUCTRL/PPUMASK setup helper + OAM-buffer splitters, fall-through of the `$2002` wait loop at `$C1F6-$C200`), `$C43E` (division/multiply-style arithmetic helper entered after `rts` at `$C43D`). ## Code coverage summary (final Pass-1 state) | Metric | Value | |--------|-------| | Code bytes traced | 5,834 / 16,384 (35.6%) | | Labels | 396 | | Distinct `jsr` targets | 70 | | Data/gap bytes | 10,550 (64.4%) | | Indirect `jmp ($0002)` sites | 2 (both queue dispatchers) | Regions entered only via runtime dispatch (queue-table targets) are traced; remaining unreached bytes sit in the data map below. ## Data map (unreached bytes, all provenance-checked) | Region | Size | Content evidence | |--------|------|------------------| | `$C201-$C2FF` tail ($C225-$C2FF) | 219 | zero padding after the PPU-setup helpers | | `$C34A-$C35E` | 21 | data after the dispatcher-1 `jmp ($0002)` tail | | `$C3A6-$C3B7` | 18 | dispatcher-1 vector table (9 words) | | `$C585-$C7FF` | 635 | text/marker tables (tile-index values `$88 $20 $88 $C1`; zero-padded from `$C714`) | | `$C95A-$C9A6`, `$CAAA-$CAF2`, `$CBC1-$CBDB` | 177 | tile-index/lookup tables read via `lda $tbl,X` | | `$CCCD-$CCE4` | 24 | dispatcher-2 low-byte table (8) + vector table (8 words) | | `$D021-$D029` | 9 | brightness ramp `$0F $14 $28 $3C $50 $64` | | `$D1AA-$D1B1`, `$D2AB-$D2BE` | 28 | small pointer/value tables | | `$D320-$D3A0` | 129 | per-level byte tables (`$01 $02 $01 $02 ... $00 $08`) | | `$D467-$D475`, `$D4EE-$D509`, `$D556-$D56A`, `$D5D5-$D631` | 146 | pointer/value pools read via `lda $tbl,X` / `adc` arithmetic | | `$D69C-$D6AB` | 16 | value ramp `$27 $29 $2B $2D ...` | | `$D79D-$D7E2` | 37 | small tables flanking the NMI handler | | `$D7F3-$DE99` | 1,703 | packed level data (`$22 $2F`-patterned runs after the `$D7EB` stub) | | `$DEBB-$DECA` | 16 | table | | `$DF3E-$DF7F` | 80 | music frequency table (`$C3 $EC $6A $ED ...`) | | `$E276-$ED6D` | 2,808 | audio pool: instrument/envelope ramps (`$FA $A8 $80 $3C ...` declining) + APU-register data | | `$EDBF-$F890` | 2,770 | audio/graphics bulk tables + large `$00` runs (incl. `$F07F-$F890` padding) | | `$F942-$FEFF` | 1,470 | mostly zeros — padding, save for small tables near `$FA00` | | `$FF74-$FFF9` | 134 | tail: clear-checker bytes + pure zeros up to the vector page | ## `copilot/` Contents | File | Purpose | |------|---------| | `Pyramid (U) (Sachen).nes` | original ROM image (reference) | | `prg.bin` / `header.bin` / `chr.bin` | split image, produced by `_tools/splitnes.py` | | `prg.s` | Pass-1 classified source: traced code + explicit `.byte` data, `; PRG:$xxxx AA BB` per line | | `symbols.inc` | shared NES hardware constants (PPU/APU/controller) | | `locals.inc` | project-local zero-page aliases — still empty (filled in Pass 2) | | `linker.cfg` | PRG at $C000, 16 KB, `fill = yes` | | `Makefile` | links `reconstructed_prg.bin` and `cmp`s it against `prg.bin` | | `reconstructed_prg.bin` | build output from the last verified build | | `prg.s.skeleton` | backup of the pre-classification skeleton (`.incbin`-only) source | Header/CHR are reproduced verbatim from `header.bin`/`chr.bin` (not linked): `make` verifies **PRG bytes only** at this stage, exactly like every other `_wip/` skeleton. When this project is promoted to a top-level full disassembly, `header.s` / `chr.s` and a full-ROM `make` target (cf. `Burger Time (U)`) get added. ## Build ```bash cd "_wip/Pyramid (U)/copilot" make # → cmp reconstructed_prg.bin prg.bin → "BYTE-IDENTICAL" ``` ## Next Steps (per `PROMPTS.md`) 1. **Pass 1 — DONE.** Code/data split, `; PRG:$xxxx AA BB` audit annotations on every line, byte-identical build. 2. **Pass 2** — semantic names: `BootUnlock`, `StackByteCheck`, `QueueDispatch1/2`, NMI/IRQ frame handlers, audio routines; fill `locals.inc` with zero-page aliases (`$02/$03` pointer, `$45/$46` X/Y state, `$04EA` dispatcher copy, `$0120/$0121` "AV" checker). 3. **Pass 3** — name the data tables (level data at `$D7F3+`, envelope pool at `$E276+`, frequency table at `$DF3E`, ramps). 4. **Promotion** — move to a top-level `/copilot/` folder, add `header.s`/`chr.s` + full-ROM `make`, write the power-on flow `OVERVIEW.md` in the established style; then it joins the root index's numbered inventory.