113 lines
7.6 KiB
Markdown
113 lines
7.6 KiB
Markdown
# 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 #<val> / 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 `<name>/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.
|