Files
disasm/_wip/Pyramid (U)/OVERVIEW.md
T
tmerritt 3877214637 update of Circus WIP
start of Pyramid wip
2026-10-16 16:17:08 -04:00

113 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.