# Pyramid (U) (Sachen) — Game Flow from Power-On This document describes the execution flow of the disassembled **Pyramid (U) (Sachen)** PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `prg.s` in the repository. The rebuild is verified **byte-identical to the original ROM** — `make` compares the whole `reconstructed.nes` against `rom.nes`. **This is an unlicensed Sachen release.** The iNES header claims mapper 0, but the boot code writes to `$4100` — past the end of the NES register space. On the real cartridge those pokes are a mapper-unlock/protection sequence. That mismatch is recorded, not explained. **Status.** Pass 1, Pass 2 and Pass 3 are complete and the project is promoted. `make` prints **Byte-identical ROM verified.** Every one of the 31 data blocks the tracer left in `prg.s` has been **interpreted**, not merely labelled: section 5 gives each block's structure (record size, encoding, units) and the consuming instruction with its address, and the blocks that no instruction reads are recorded as unreferenced data rather than given a table name. The block names live in `data_region_names.tsv`, so `make regen` reproduces them. --- ## 1. Reset Vector — CPU $FFFC → $FF00 | Vector | Address | Handler | |--------|---------|---------| | NMI | $D72E | `NmiHandler` | | RESET | $FF00 | `Reset` — the boot chain, at the very end of the PRG | | IRQ | $FF00 | `Reset` — the same address; `sei` is never undone, so the IRQ never fires | **`Reset`** ($FF00) is the cold-start chain, and it is unusual in that the whole thing lives in the vector page: ``` Reset ($FF00): ldx #$FF / txs / sei / cld lda #$00 / sta PPUCTRL / sta PPUMASK lda #$30 / jsr BootPokeMapperUnlock jsr BootCheckStackBytes bcc Reset_Loop ; the check failed - take the other sequence lda #$10 / jsr BootPokeMapperUnlock lda #$30 / jsr BootPokeMapperUnlock jsr BootUnlockDelay / jsr BootUnlockDelay lda #$10 / jsr BootPokeMapperUnlock jmp BootChainEntry Reset_Loop ($FF2F): ; the alternate poke sequence lda #$20 / jsr BootPokeMapperUnlock lda #$30 / jsr BootPokeMapperUnlock lda #$20 / jsr BootPokeMapperUnlock ... ``` The two sequences differ in the values poked: `$30 $10 $30` on the carry-set path, `$20 $30 $20` on the carry-clear path. ### The mapper unlock **`BootPokeMapperUnlock`** ($FF49) is one instruction plus a delay: ``` BootPokeMapperUnlock: sta $4100 ; the unlock poke BootUnlockDelay: ldy #$04 BootUnlockDelay_Loop: ldx #$CE BootUnlockDelay_Inner: dex / bne BootUnlockDelay_Inner dey / bne BootUnlockDelay_Loop rts ``` The delay is the protection: the console latches `$4100` and the ROM waits for the mapper to settle. ### The "AV" check **`BootCheckStackBytes`** ($FF57) compares two RAM bytes against the letters **A** and **V**: ``` BootCheckStackBytes: lda #$41 / cmp $0120 / bne BootCheckStackBytes_Fail lda #$56 / cmp $0121 / bne BootCheckStackBytes_Fail sec / rts BootCheckStackBytes_Fail: clc / rts ``` `Carry` set means the check passed and the first poke sequence runs; `Carry` clear means the alternate sequence runs. **`BootSeedStackBytes`** ($FF69) writes `$41`/`$56` into `$0120`/`$0121` itself, so the check is satisfied on the next pass. **`ClearCheckerBytes`** ($FF74) writes `$00` into both again. ### The real entry **`BootChainEntry`** ($FF3E) is the hand-off into the game: ``` BootChainEntry: lda #$30 / jsr BootPokeMapperUnlock jsr BootSeedStackBytes ; seed the "AV" checker bytes for next time jmp BootColdStart ; the real init, at $C800 ``` **`BootColdStart`** ($C800) is the game's own entry: it sets up the stack, waits for two vblank phases, clears RAM, resets the sound engine and initialises the PPU. --- ## 2. The dispatch architecture The game dispatches through **two levels** of indirection. ### The task table — `jmp` trampolines The entry points live in a table of `jmp` instructions rather than of addresses. Four such tables sit inside the data regions: | Table | Address | Entries | Task | |-------|---------|---------|------| | `TaskDispatchTable1` | $C01E | 3 | $C1C9, $C1D0, $C1D7 | | `TaskDispatchTable2` | $C02A | 6 | $C1EF, $C205, $C201, $C225, $C23A, $C294 | | `TaskDispatchTable3` | $C042 | 2 | $C34A, $C43E | | `TaskDispatchTable4` | $C054 | 5 | $C585, $C5AB, $C614, $C662, $C6A3 | and the first ten entries of the table are code of their own, at $C000–$C01B (`Task_PlayVramScript`, `Task_CopyBlockToCommandBuffer`, `Task_UploadPalette1`, …). The tracer cannot see any of this: a `jmp` inside a data region is just four bytes to it, so **all sixteen targets had to be seeded by hand**. ### The queued-event dispatchers Two dispatchers jump through the zero page with `jmp (ScriptPtrLo)`: | Dispatcher | Site | Vector table | Entries | |------------|------|--------------|---------| | 1 | `DispatchTask1` ($C392) | `TaskVectorTable1` ($C3A6) | 9 entries — the ninth, $D820, is a **junk slot** and stays data | | 2 | `DispatchTask2` ($CCB8) | `TaskVectorTable2` ($CCD5) | 8 entries, preceded by the 8-byte `TaskIndexTable2` at $CCCD | Both build the pointer with the same idiom — read a 16-bit entry from the table into $02/$03, then `jmp ($0002)` — and both terminate the static trace. --- ## 3. The VRAM command list The engine never writes the PPU directly. It assembles a **byte stream** in the buffer at $0700 and flushes it: - `$FE` — the next two bytes are a VRAM address - `$FD` — end of list - anything else — a byte for $2007 `PlayVramScript` ($C063) walks a script and builds that list; `FlushVramCommandList` ($C127) plays it back. `FinishCommandList` ($C11B) writes the `$FD` marker and waits for `CommandListFlag` to clear. --- ## 4. The NMI handler `NmiHandler` ($D72E) is the per-frame entry: it advances the frame counter, runs `ActorUpdate` ($D50A) and `SoundTick` ($DF80), then restores the registers. Two byte counters at $D75C/$D75D accumulate the frame total, and `DrawNumber` ($D79D) turns that into the score digits on the PPU. --- ## 5. Pass 3 Data-Table Inventory One row per data block the tracer left in `prg.s` (31 in all). "Address" is the block's range, "Name" the label now attached to it (`-` = left unnamed on purpose), "Structure" what the bytes are, and "Read by" the consuming instruction(s) with their addresses. | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | `$C01E-$C026` | 9 | `TaskDispatchTable1` | Three 3-byte `jmp` trampolines: `jmp $C1C9` (`IncrementCounter16`), `jmp $C1D0` (`AdvanceScriptPointer`), `jmp $C1D7` (`ConvertNibbleToBcd`). The table holds code, not addresses, so the tracer cannot follow it. | Not referenced by any absolute operand - the engine reaches the trampolines through the task scheduler's computed dispatch. | | `$C02A-$C03B` | 18 | `TaskDispatchTable2` | Six `jmp` trampolines: `SetPpuCtrl` (`$C1EF`), `SetPpuCtrlAndMask_Alt` (`$C205`), `SetPpuCtrlAndMask` (`$C201`), `StoreOamEntry` (`$C225`), `DrawTwoDigitNumber` (`$C23A`), `AddDeltaToPosition` (`$C294`). | Not referenced by any absolute operand (computed dispatch). | | `$C042-$C047` | 6 | `TaskDispatchTable3` | Two `jmp` trampolines: `CopyBlockToCommandBufferTail` (`$C34A`), `Subtract16` (`$C43E`). | Not referenced by any absolute operand (computed dispatch). | | `$C054-$C062` | 15 | `TaskDispatchTable4` | Five `jmp` trampolines: `WriteDigitString` (`$C585`), `AddDelta24` (`$C5AB`), `AddDelta24B` (`$C614`), `SetPaletteEntry` (`$C662`), `SetPaletteEntry2` (`$C6A3`). | Not referenced by any absolute operand (computed dispatch). | | `$C3A6-$C3B5` | 16 | `TaskVectorTable1` | Eight 16-bit little-endian handler addresses: `MoveSpriteRight` (`$C3B6`), `MoveSpriteLeft` (`$C3C7`), `Task_ReturnStub` (`$C3FD`), `SetPaletteBlock` (`$C35F`), `MoveSpriteDown` (`$C3E3`), `MoveSpriteUp` (`$C3E7`), `MoveSpriteLeft2` (`$C3EB`), `MoveSpriteRight2` (`$C3EF`). A ninth slot (`$D820`) is a junk entry and stays data. | `lda $C3A6,x` at `$C395` and `lda $C3A7,x` at `$C39A` build the address; it is entered with `jmp ($0002)` at `$C3A3`. | | `$C52A-$C535` | 12 | `PaletteSlotRemapTable` | 12 bytes mapping a colour from the game's display script to its slot in the 16-byte palette RAM: `01 05 09 0D 02 06 0A 0E 03 07 0B 0F`. Slots 0, 4, 8 and 12 keep the universal background colours. | `ldy $C52A,x` at `$C50C`, `$C56F`, `$C685` and `$C6BC`; the value becomes the low byte of the palette address written at `$C50F`. | | `$C605-$C613` | 15 | `SoundFrequencyDeltaTable` | Five 24-bit little-endian frequency deltas: `$0186A0` (100000), `$002710` (10000), `$0003E8` (1000), `$000064` (100), `$00000A` (10) - a decade series used to scale a note's 24-bit frequency. | `sbc $C605,y` at `$C5C3`, `$C5CB`, `$C5D3` and `adc $C605,y` at `$C5E5`, `$C5ED`, applied to the 24-bit `Delta` at `$4D`/`$4E`/`$4F`. | | `$C6E7-$C7FF` | 281 | - | 265 bytes of `$00` fill (`$C6E7-$C7EF`) followed by a 16-byte counter table at `$C7F0-$C7FF` (`00 01 02 ... 0F`). The counter table's role is not established. | Not referenced by any instruction. | | `$C95A-$C9A6` | 77 | `ActorSpriteTileTable` | Four groups of CHR tile indices: `$C95A-$C966` (13 bytes) the player's tiles (`1C 1D 11 12 ... 1B`), `$C967-$C96E` (8 bytes) the second actor's tiles, `$C96F-$C98A` (28 bytes) the enemy tiles (`56 58 57 ... 71`), and `$C98B-$C9A6` (28 bytes) the two-frame animation pattern that alternates tiles `$6F` and `$77`. | `lda $C95A,y` at `$C90D`; `lda $C967,y` at `$C936` and `$C93E`; `lda $C96F,y` at `$C8D7`; `lda $C98B,y` at `$C8D0`. | | `$CAAA-$CAF2` | 73 | `ActorDefinitionTable` | Six 4-byte records at `$CAAA-$CAC1` (`0F 0E 00 00` / `0F 06 06 00` / `14 00 04 08` / `00 00 10 14` / `14 10 04 10` / `00 14 00 04`) holding each actor type's palette and spawn parameters; then 8 bytes at `$CAC2-$CAC9`; then a list of CHR tile indices at `$CACA-$CAF2` (`B1 C2 B0 E1 A1 E2 ... FF FF`) that ends with `$FF`. | `lda $CAAA,y` at `$CA80` copies the 5-byte record of actor type A into the VRAM command buffer at `$0702`; `lda $CAB2,x` / `lda $CAB3,x` at `$CB88` / `$CB8E` load the 16-bit spawn position of the record at `$CAB2`; `lda $CAC4,x` at `$CC4C`. | | `$CBC1-$CBDB` | 27 | `ActorSizeTable` / `ActorSpeedStepTable` | `$CBC1-$CBD0`: 16 size classes `00 00 01 01 02 02 04 04 05 05 00 01 02 04 05 00`, multiplied by 4 to give the actor's pixel hit box. `$CBD1-$CBDB`: 11 speed steps `14 0F 0A 06 05 04 03 02 02 02 02`, one per actor type. | `lda $CBC1,x` at `$CB7A` and `$CBA5` (size class to `$042F`); `lda $CBD1,x` at `$CBE5` (speed step to `$0432`). | | `$CCCD-$CCE4` | 24 | `TaskIndexTable2` / `TaskVectorTable2` | `$CCCD-$CCD4`: the eight per-slot task indices `0A 0A 00 00 01 03 06 06`. `$CCD5-$CCE4`: eight 16-bit little-endian handler addresses - `TaskScheduler_Select` (`$CCEE`), `TaskScheduler_Check` (`$CD27`), `TaskScheduler_Next` (`$CC90`), `TaskScheduler_SetPointer` (`$CD75`), `TaskScheduler_Check2` (`$CD43`), `TaskScheduler_Finish` (`$CDA4`), `TaskScheduler_Move` (`$CE1A`), `TaskScheduler_Move2` (`$CE5E`). | `lda $CCCD,x` at `$CCB1`; `lda $CCD5,x` / `lda $CCD6,x` at `$CCBA` / `$CCBF`, entered with `jmp ($0002)` at `$CCCA`. | | `$CD97-$CDA3` | 13 | - | 13 bytes `0A 0A 01 01 01 76 5B 82 83 11 01 01 FD`. The layout is **not established**: the small values could be a parameter list, and the rest are CHR tile indices with the same `$FD` marker the game's tile streams use. | Not referenced by any instruction. | | `$D021-$D029` | 9 | `BrightnessRampTable` / `SoundEffectIdTable` | `$D021-$D027`: the seven-step brightness ramp `0F 00 14 28 3C 50 64` - the NES colour numbers the game fades through. `$D028-$D029`: two sound-effect ids, `01` and `04`. | `lda $D021,y` at `$CF24` (the ramp); `lda $D028,x` at `$CB62`, `$D01A` and `$D454` - the effect id is passed to `SoundPlayEffect` at `$CB65`. | | `$D0C5-$D0D2` | 14 | `GridObjectTileTable` | `$D0C5-$D0C8`: the four tile codes `01 02 04 08` that the grid search compares against. `$D0C9-$D0D2`: ten more bytes (`00 03 01 03 00 02 00 02 01 03`) compared in pairs with the grid contents. | `cmp $D0C5,x` at `$D03D`; `cmp $D0C9,x` at `$D07B` and `cmp $D0CA,x` at `$D080`; `lda $D0CF,y` at `$D092` and `lda $D0D0,y` at `$D099`. | | `$D1AA-$D1B1` | 8 | `ActorDirectionOffsetTable` | Two 4-byte arrays giving the one-cell step in each direction: the X offsets `00 01 00 01` at `$D1AA` and the Y offsets `00 00 01 01` at `$D1AE`. | `lda $D1AA,x` at `$CEB4` and `$D057`, `adc $D1AA,x` at `$D0E6`; `lda $D1AE,x` at `$CEAC` and `$D050`, `adc $D1AE,x` at `$D0FB`. | | `$D2AB-$D2BE` | 20 | `ActorSpriteTable` | 20 CHR tile indices, one group per actor type: `01 21 22 3B 3A 01 01 01 23 01 01 01 57 01 01 5A 02 03 07 08`. | `lda $D2AB,y` at `$D1DD`; `lda $D2AB,x` at `$D284` and `lda $D2BB,x` at `$D28A`. | | `$D320-$D3A0` | 129 | `LevelLayoutTable` / `ActorTargetTileTable` | `$D320-$D32F`: 16 bytes (`01 02 01 02 ... 08 08`) with no reader. `$D330-$D38F`: 24 four-byte records - byte 0 of each record is the grid tile code that actor slot is hunting for, and the record stride is 4 (the index is `4 x actor number`). `$D390-$D3A0`: 17 bytes (`FF 73 75 7B 77 FF FF FF 79 FF FF FF 7D FF FF FF 7F`) with no reader. | `lda $D330,y` at `$CEC1`, `$D038`, `$D063`, `$D0ED`, `$D105` and `$D129`. At `$D066` the code XORs the code with the grid cell and tests the result against `$03` and `$0C`. | | `$D467-$D475` | 15 | `ActorSpriteOamTable` | The OAM bytes of a spawned actor's sprite: three groups of 5 bytes whose first byte is the tile index (`$D467`, `$D46C`, `$D471`), the attribute byte between them being the constant `$74`. | `adc $D467,x` at `$D3E6`, `adc $D46C,x` at `$D3C8` and `lda $D471,x` at `$D3D5` - all written into the OAM shadow at `$0200`. | | `$D4EE-$D509` | 28 | `ActorInitialStateTable` | Four parallel 7-byte arrays - the initial position, size, speed and state of the seven actor slots. | `lda $D4EE,x` at `$D4C5`, `lda $D4F5,x` at `$D4CB`, `lda $D4FC,x` at `$D4D1` and `lda $D503,x` at `$D4DC`. | | `$D556-$D56A` | 21 | `ActorSpeedTable` / `ActorSpeedModifierTable` / `ActorMoveIntervalTable` | `$D556-$D55C`: seven bytes of `$01`, the base speed. `$D55D-$D563`: seven bytes of `$00`, the speed modifier. `$D564-$D56A`: the doubling ramp `00 01 02 04 08 10 20` - the number of frames between an actor's moves. | `adc $D556,x` at `$D534`, `adc $D55D,x` at `$D53E` and `lda $D564,x` at `$D544`. | | `$D5D5-$D631` | 93 | `ActorSpriteOffsetTable` / `ActorSpriteYOffsetTable` / `ActorSpriteXOffsetTable` / `ActorSpriteBaseOffsetTable` | `$D5D5-$D5DC`: the OAM slot index each actor uses. `$D5DC-$D5E9`: two per-actor offset bytes (`$0409`, `$040A`). `$D5EA-$D601`: the per-sprite Y offsets, `$FF` marking a sprite to hide. `$D602-$D619`: the per-sprite X offsets, added to the actor's base X. `$D61A-$D631`: a second 24-byte offset table. | `lda $D5D5,x` at `$D596`; `lda $D5DC,x` at `$D58A`; `lda $D5E3,x` at `$D590`; `lda $D5EA,y` at `$D59A` (the `$FF` test at `$D59D` hides the sprite); `lda $D602,y` at `$D5BA`; `lda $D61A,y` at `$D5B0`. | | `$D69C-$D6AB` | 16 | `ActorSpriteYPositionTable` / `ActorSpriteXPositionTable` | `$D69C-$D6A3`: the eight sprite Y positions `27 29 2B 2D 3D 3F 2D 41`. `$D6A4-$D6AB`: the eight sprite X positions `50 59 62 6B 78 81 8A 93`, added to the actor's base X. | `lda $D69C,y` at `$D67F` (written to the OAM shadow at `$0200`); `lda $D6A4,y` at `$D68C`, added to the base X at `$D690`. | | `$D72D-$D72D` | 1 | - | The single byte `$3A`. Nothing in this dump names it. | Not referenced by any instruction. | | `$D7F3-$DE99` | 1703 | `LevelScriptData` | The game's display data in the VRAM-script format the interpreter at `$C090` implements: plain tile bytes, `$FE ` runs that repeat the previous tile, and `$FE $00` to end a list. `$D7F3-$D852` is the first script; `$D853-$D912` are twelve 16-byte blocks (six of them all zero, the others holding values that do not fit the NES palette format, so their role is not established); `$D913-$D952` is 64 bytes of `$AA`, the shape of a nametable attribute table; `$D953-$DE99` are two more scripts (the attract-mode and level screens). | Read through the script pointer: `lda (ScriptPtrLo),y` at `$C090`. The pointer is set at run time by the task dispatch, so no absolute operand in this dump names the block. | | `$DEBB-$DECA` | 16 | `SoundApuInitTable` | Four 4-byte blocks of initial APU register values - pulse 1 (`$8F $FA $FF $F0`), pulse 2 (`$3F $8B $00 $FF`), triangle (`$20 $CB $DE $A5`) and noise (`$33 $D0 $FC $60`). | `lda $DEBB,x` at `$DEA9`, copied to `$4000-$4003` (`APUPULSE1CTRL` and the pulse-1 sweep and period registers) at `$DEAC`. | | `$DF30-$DF7F` | 80 | `MusicPeriodTable` | 40 16-bit little-endian note periods, from `$E319` up to `$ED79`. | `lda $DF30,x` / `lda $DF31,x` at `$DED5` / `$DEDA`; the same pair of instructions reads the entries at `$DF44`/`$DF45` and `$DF58`/`$DF59`. | | `$E276-$ED6D` | 2808 | `SoundEnvelopeTable` | The sound engine's instrument data: `$E276-$E2A6` is a descending volume envelope (`58 FA A8 80 3C FC ...`), `$E2A7-$E2CF` a per-voice envelope-length table, `$E2D0-$E2F0` the note-length ramp `01 02 04 06 08 0A 0C 0E 10`, and `$E2F1-$ED6D` the per-voice parameter sets in groups of four. | `lda $E276,x` at `$E02A` and `$E0CF`; `lda $E2E8,y` at `$DFD5`; `lda $E2F1,x` at `$E017` and `$E0BC`; `ora $E2A7,x` at `$E033`. | | `$EDBF-$F890` | 2770 | `SoundSamplePool` | The sample and instrument pool. The first four bytes are the DMA descriptor the sample player uses (source address `$F200`, length `$F890`); the remaining 2,766 bytes are mostly `$00`/`$FF` fill with a handful of `$55`/`$AA`/`$F0` bytes. | `lda $EDBF,x` at `$ED70`, `lda $EDC0,x` at `$ED75`, `lda $EDC1,x` at `$ED88`, `sbc $EDBF,x` at `$ED8B`, `lda $EDC2,x` at `$ED90` and `sbc $EDC0,x` at `$ED93`. | | `$F95D-$FEFF` | 1443 | - | 1,443 bytes of `$00` fill. | Not referenced by any instruction. | | `$FF7D-$FFFF` | 131 | `InterruptVectorTable` | `$FF7D-$FFF9`: 125 bytes of `$00` fill. `$FFFA-$FFFF`: the three 6502 interrupt vectors - NMI = `$D72E` (`NmiHandler`), RESET = `$FF00` (`Reset`), IRQ = `$FF00` (`Reset`, the same address; `sei` is never undone). | The vectors are read by the CPU's interrupt logic, not by an instruction in the dump. | ### Open work * Four blocks are **unreferenced by any instruction in this dump** and are recorded above as such rather than being given a table name: `$C6E7-$C7FF` (zero fill plus an unexplained 16-byte counter table), `$CD97-$CDA3` (13 bytes of unknown layout), `$D72D-$D72D` (one byte) and `$F95D-$FEFF` (zero fill). Their labels were removed from `prg.s`. * `$D7F3-$DE99` (`LevelScriptData`) is read only through the script pointer, so the absolute operand that would name it is not in the dump. A re-trace that follows the task scheduler's computed dispatch would show which instruction sets the pointer and would let the three scripts inside the block be split and named individually. * The four `jmp` trampoline tables (`$C01E`, `$C02A`, `$C042`, `$C054`) are reached only by computed dispatch as well; their targets are named in the inventory above, but the code that jumps to them is still unreached. --- ## 6. Naming and Residue **Pass 1** — 8,253 lines. 3,130 instruction lines covering 6,493 code bytes (**39.6%**), 632 data lines covering 9,891 bytes, 3,762 canonical audit comments. The byte accounting is exact: 16,384 bytes, no duplicates, nothing missing. **Pass 1 correction (2026-10-20).** The committed split was a strict subset of the correct one: **828 bytes** of code were still marked as data, because the tracer cannot follow a `jmp` that sits inside a data region. Re-tracing with the sixteen `jmp`-table targets, the `$C800` boot mainline and six further code starts seeded by hand recovers them, and a five-byte stub at $DF89 (`lda $33 / jmp SoundPlayEffect`) that sits in a hole between two routines. The re-trace loses **no** committed code byte. **Pass 2 complete** — 463 label definitions, **zero `Label_` residue**: 431 routine entries and the 32 data blocks, named from this dump's own code. **No raw `$XXXX` operand** remains on any `jsr`, `jmp` or branch. The map is in `routinemap.tsv`; the internal branch-target names are derived mechanically from the branch that reaches them by `_tools/derive_branch_names.py`. **Pass 3** — 24 semantic zero-page aliases, each applied only where its role is proven (`locals.inc`, `zp_aliases.inc`). 57 slots keep their neutral `ZpXX` name, and **every one of them is listed with the reason** in `locals.inc`. `apply_symbols.py` reports **0 substitutions pending and 0 residue**; no hardware register is left as a raw literal. **Pass 3, data tables** — all 31 data blocks are now interpreted; see section 5. The names live in `data_region_names.tsv` (applied by `name_data_region.py` in `make pass3`), so the interpretation survives a re-trace. Names that were guesses or that no instruction backs were changed or dropped: `PaletteIndexTable` → `PaletteSlotRemapTable`, `SoundDeltaTable` → `SoundFrequencyDeltaTable` (five 24-bit decade multipliers), `SpriteTileTable` → `ActorSpriteTileTable`, `ActorAttributeTable` → `ActorDefinitionTable`, `ActorTypeTable` → `GridObjectTileTable`, `ActorOffsetTable` → `ActorDirectionOffsetTable`, `ActorBehaviourTable` → `ActorSpriteOamTable` (OAM bytes, not behaviour), `ValueRampTable` → `ActorSpriteYPositionTable` (sprite coordinates, not a ramp) and `SoundSampleTable` → `SoundApuInitTable` (initial APU register values, not samples). `ActorParameterTable` ($CD97) and `ActorFlagByte` ($D72D) were removed: nothing in the dump reads them and their layout is not established. **Residue (recorded, not guessed):** - The zero page is reused heavily — 410 references still carry a neutral name. The largest group is `$45`–`$49` (105, 69, 33, 22 and 11 references), which are the general-purpose scratch of the shift and arithmetic helpers; one name would not describe their other uses. - 482 absolute operands point at RAM ($0700, $0200, $0400, $0436, …). Those are data addresses, not hardware registers, and stay numeric. - `sta $4100` at $FF49 stays a raw operand. It is outside the `$2000-$4017` range that `PROMPTS.md` asks to be symbolic, and no `symbols.inc` name would describe a mapper unlock. --- ## 7. Promotion to `NROM/` (2026-10-20) Moved out of `_wip/NROM/` with `git mv`; the build now compares the **whole ROM**, not just the PRG: - `header.s` / `chr.s` added (one-line `.incbin`s of `header.bin` / `chr.bin`) - `linker.cfg` gained the `HEADER` ($0000, $0010) and `CHR` ($0000, $2000) areas ahead of `PRG` ($C000, $4000) - `rom.nes` — symlink to `Pyramid (U) (Sachen).nes` - `Makefile` builds `header.o` / `prg.o` / `chr.o` into `reconstructed.nes` and compares it against `rom.nes` - `make regen` reproduces `prg.s` and `locals.inc` from `prg.bin` and re-applies the naming, so the split stays reproducible `reconstructed.nes` is 24,592 bytes, MD5 `aa285ff1…6c59e2` — equal to the original ROM. The `header.bin` / `prg.bin` / `chr.bin` splits were checked against the original before promotion: they are the ROM's first 16 bytes, its next 16,384 bytes and its last 8,192 bytes, exactly. Build verification: `make` in `NROM/Pyramid (U) (Sachen)/` produces `reconstructed.nes`, compared against `rom.nes` (**whole-ROM byte-identical**). ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry. **RESET** is at `$FF00` (`Reset`). The CPU comes here out of reset with the PPU and APU in their power-on state and the zero page and RAM uninitialised. The reset path calls: | Address | Routine | |---------|---------| | `$FF49` | `BootPokeMapperUnlock` | | `$FF57` | `BootCheckStackBytes` | | `$FF49` | `BootPokeMapperUnlock` | | `$FF49` | `BootPokeMapperUnlock` | | `$FF4C` | `BootUnlockDelay` | | `$FF4C` | `BootUnlockDelay` | | `$FF49` | `BootPokeMapperUnlock` | | `$FF3E` | `BootChainEntry` | | `$FF4C` | `BootUnlockDelay` (falls through) | | `$FF4E` | `BootUnlockDelay_OnNonzero` (falls through) | | `$FF49` | `BootPokeMapperUnlock` | | `$FF69` | `BootSeedStackBytes` | | `$C800` | `BootColdStart` | | `$FF50` | `BootUnlockDelay_OnNonzero2` (falls through) | | `$C805` | `BootWaitForVblank` (falls through) | | `$C80A` | `BootWaitForVblank2` (falls through) | | `$DF0E` | `SoundStopAll` | **NMI** is at `$D72E` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$F894` | `SampleTableRead2` | | `$F897` | `SampleTableRead3` | | `$C027` | `Task_FlushVramCommandList` | | `$F891` | `SampleTableRead` | | `$C00F` | `Task_StrobeControllers` | | `$D7E3` | `IrqHandler` | **IRQ** is at `$FF00` (`Reset`) - the same address as RESET, so the IRQ vector reaches the reset path.