7.6 KiB
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)
ldx #$FF / txs / sei / cld, clear PPUCTRL/PPUMASK.- Repeated
LDA #<val> / JSR $FF49—$FF49doesSTA $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$4100pokes are an mapper-unlock/protection sequence. Boot pokes$30,$10/$30/$10+$30(via$FF4Cdelay variant twice) or$20/$30/$20depending on a check. JSR $FF57— reads stack page bytes$0120/$0121and compares against$41/$56("A","V").SEC⇒ proceeds to the$4100poke sequence above;CLC⇒ alternate poke sequence, then falls into:JSR $FF69— seeds$0120/$0121with$41/$56.JMP $C800— real init: waits two$2002vblank phases, clears$0000-$07FF, builds ZP pointer$D7/$D8 = $C8xx, zeroes$00, writes$98to PPUCTRL /$1Ato 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 cmps 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
cd "_wip/Pyramid (U)/copilot"
make # → cmp reconstructed_prg.bin prg.bin → "BYTE-IDENTICAL"
Next Steps (per PROMPTS.md)
- Pass 1 — DONE. Code/data split,
; PRG:$xxxx AA BBaudit annotations on every line, byte-identical build. - Pass 2 — semantic names:
BootUnlock,StackByteCheck,QueueDispatch1/2, NMI/IRQ frame handlers, audio routines; filllocals.incwith zero-page aliases ($02/$03pointer,$45/$46X/Y state,$04EAdispatcher copy,$0120/$0121"AV" checker). - Pass 3 — name the data tables (level data at
$D7F3+, envelope pool at$E276+, frequency table at$DF3E, ramps). - Promotion — move to a top-level
<name>/copilot/folder, addheader.s/chr.s+ full-ROMmake, write the power-on flowOVERVIEW.mdin the established style; then it joins the root index's numbered inventory.