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

7.6 KiB
Raw Blame History

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 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)

  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.