# MUSCLE (U) — Pass 3 (data names and structures) ROM: `MUSCLE (U)/M.U.S.C.L.E. (U).nes` (24,592 bytes, iNES). Status: **PASS 3 COMPLETE — byte-identical, including the bytecode VM layer.** All three passes are done: Pass 1 split code from data, Pass 2 replaced every `Label_XXXX` and 87 zero-page aliases, Pass 3 named and structured every data region, finished the routine descriptions, and resolved the reused zero page and the unreachable stubs. The 69 data blocks the tracer left are now accounted for one by one in **Pass 3 data-table inventory** below: every block carries a name, a structure and the address of the instruction that reads it, or the evidence that nothing reads it. Two blocks that had been given bytecode-program names turned out not to be bytecode at all and were renamed - `DeathSequenceVramStream` and `LevelOamTemplateTable`. `make regen` in `copilot/` replays Pass 1 → Pass 3 from `prg.bin` and is byte-for-byte reproducible. Every instruction still carries its `; CPU:$xxxx PRG:xxxx AA BB` audit comment, so none of this touched a byte. | | Result | |---|---| | Code | 10,634 bytes (5,253 instructions) across 705 routines — 0 `Label_XXXX` remaining | | Data | 5,750 bytes across 69 named regions — 0 unlabelled blocks, 69 rows in the inventory | | Structured tables | 127 `.word` entries emitted one per line with their resolved target | | Routine descriptions | 0 boilerplate `; Global:` lines remaining (94 hand-written, 606 generated from call sites) | | Zero page | 163 aliases: 89 named, 1 reused byte carrying two names, 73 `ZpXX` | | Build | **Byte-identical ROM verified.** | ## 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 `$8000` (`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 | |---------|---------| | `$800E` | `WaitForFirstPpuWarmup` (falls through) | | `$8019` | `WaitForSecondPpuWarmup` (falls through) | | `$802C` | `InitializeGameState` (falls through) | | `$803B` | `InitializeGameState_OnNonzero_803B` (falls through) | **NMI** is at `$86CB` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$8759` | `FinishNmiRegisters` | **IRQ** is at `$8764` (`IrqHandler`) - a different address from RESET. ## ROM Facts | Property | Value | |----------|-------| | Mapper | 0 (NROM) | | PRG | 16 KB, mapped **low** at $8000–$BFFF (mirrored to $C000–$FFFF) | | CHR | 8 KB | | Mirroring | Vertical | | Battery / trainer | none | | ROM SHA-256 | `d3da17f2b67d517e4d519659407d6f0e3cd7a2a4d89d31da6bb2727afce798dd` | | PRG SHA-256 | `c9061b6d3d06f56e3c2f89337d8e59c159f494ff9efad50852fcd19621f0b03e` | **The skeleton's `$C000` assumption was wrong.** A 16 KB NROM PRG is normally mirrored from `$C000`, but the RESET vector is `$8000`, so this image is wired to `$8000` and every address in the source (and in `linker.cfg`) is `$8000`-based. The CPU still fetches the vectors from the mirror at `$FFFA`, which is why `InterruptVectors` is the last six bytes of the PRG. ## Interrupt Vectors (PRG $3FFA–$3FFF) | Vector | Target | Note | |--------|--------|------| | NMI | `$86CB` (`NmiHandler`) | the entire per-frame update; see flow below | | RESET | `$8000` (`Reset`) | confirms the low mapping | | IRQ | `$8764` (`IrqHandler`) | a single `RTI`, shared with the NMI epilogue; see below | ## Power-On Flow `Reset` ($8000): 1. `sei`, `ldx #$FF`, `stx $0320`, `txs` — X points the stack at the top of page 1 and `$0320` is set to $FF as the "an NMI already fired during init" latch. 2. Two `lda $2002 / bpl` spin loops blank the PPU and confirm vblank. Each write is followed by a `nop` before its poll ($800D, $8018) — the align-before-read habit of the original assembler. 3. `$4017 = $40` (APU frame counter IRQ inhibit), `$4010 = 0`, and zero the DMC address/length pair at zero page `$FA`/`$FB` (the same two bytes the controller reader later reuses). 4. Clear `$0326`, `$032B`, `$032C`, zero page `$FF`, then fill `$0300-$032C` with zero. 5. Set `SourcePointerLo/Hi = $9E/$B8` and `DestinationPointerLo/Hi = $00/$04`, then run `UpdateNametableFromStream`, which decodes the compressed nametable into $0400. 6. If the stream actually changed VRAM, latch the tighter scroll bound into `$0328-$032A`, reset the frame counters to 0/1/2/3, and fall into `StartStageSequence`. `NmiHandler` ($86CB): 1. Push A, X, Y in that order. 2. `lda $0320 / beq $86D8 / jmp $8759`. Reset leaves `$0320 = $FF`, so the **first** NMI skips the frame update entirely; from then on `$0320` is cleared each frame (see 6) and this is a fall-through. 3. Set `$0320 = $FF` and bump the 16-bit `FrameCounterLo/Hi` in zero page `$24`/`$25`. 4. `$2001 = 0` (blank rendering), `$2003 = 0`, `$4014 = $02` — OAM DMA from `$0200`. 5. Dispatch on the game state at `$0322`: zero goes to the `SubModeFlag` test, negative jumps to the object-list housekeeping at `$86AD`, positive falls into `UpdateRoundLogic`. The branches call `PollControllerAndAdvance`, `UpdateStageActors`, `UpdateObjectCollisions`, `RunGameStateDispatcher`, `BuildOamList` and `PlayDeathSequence` in turn. 6. `FinishNmiUpdates` ($8751): `lda $0321 / bne $8759` — the `$0320` latch is cleared **only** when `$0321` is zero, so a frame that requested continued work stays latched. Then `lda $2002` to release the PPU address latch, `jsr UpdateSoundFrame`, and `pla/tay/pla/tax/pla`. **The IRQ vector word is the NMI epilogue's `RTI`.** After the three `pla`s the next byte is `$8764`, which is both `IrqHandler` and the `rti` that ends every NMI — one `RTI` serving two vectors. The game never re-enables interrupts, so the IRQ is unreachable in practice. ## Architecture: this ROM dispatches by jump table, not by direct call A plain recursive trace from the vectors reaches only 50.6% of the PRG. The gap is **not** dead code — it is reached through four dispatch tables and two zero-page indirect jumps that a static descender cannot follow. Resolving them took code coverage to **64.9%**. | Mechanism | Location | Size | Index | |-----------|----------|------|-------| | `BytecodeProgramPointerTable` | `$8CEF` | 38 × 16-bit | `(token & $3F)` at `$9AD8` | | `BytecodeHandlerTable` | `$9237` | 40 × 16-bit | `token - $C8` at `$9AF9` | | `SecondaryBytecodeHandlerTable` | `$9F2B` | 16 × 16-bit | `(token & $0F)` at `$9F85` | | `LevelVramWriterTable` | `$AC35` | 3 × 16-bit | the level byte `$0326` at `$AC28` | | `DispatchThroughPointer` | `jmp ($001C)` at `$9B05` | — | reached from the first three tables | | `jmp ($0010)` | `$AC32` | — | reached from `LevelVramWriterTable` | `RunBytecodeInterpreter` ($9AE8) reads a token through `(BytecodeStreamPointerLo),y`; tokens `$C8`–`$EF` are converted to a handler address and dispatched through `jmp ($001C)`; everything else is an operand consumed by the running handler. **The handler tables have holes.** The interpreter masks the program index with `$3F`, so `BytecodeProgramPointerTable` is indexed 0–63 — but entry 38 already reads the first byte of `BytecodeProgram_00`, so the table is exactly **38** entries and the caller must guarantee a smaller index. `BytecodeHandlerTable` entries 0 and 2 hold `$928D` and `$9287`, which are the addresses of the code immediately after the table: slots the assembler never filled. ## Pass 3 — Data Structure All 69 data regions are labelled. The four composite regions from Pass 1 were split into their real tables using the readers in decoded code: ### `$8CEF–$9286` (1,432 bytes) → three structures | Region | Size | What it is | |--------|------|------------| | `BytecodeProgramPointerTable` `$8CEF–$8D3A` | 76 | 38 × `.word`, targets printed per entry | | `BytecodeProgram_00` … `BytecodeProgram_25` `$8D3B–$9236` | 1,276 | the 38 programs the table reaches, each labelled with the table index that reaches it | | `BytecodeHandlerTable` `$9237–$9286` | 80 | 40 × `.word` opcode handlers | The programs are recovered from the pointer table: sort the 38 entries, and each program runs from one entry to the next. All 38 entries are distinct, so the table reaches 38 distinct programs - one for every entry, and every label `BytecodeProgram_00` … `BytecodeProgram_25` is reached by exactly one of them. ### `$B0CB–$BFFF` (3,893 bytes) → eleven structures | Region | Size | What it is | |--------|------|------------| | `SoundNotePeriodTable` `$B0CB–$B0E2` | 24 | 12 × `.word` tone periods (`cmp #$0C` bounds the index) | | `SoundNoteDurationTable` `$B0E3–$B0F2` | 16 | note durations, index `duration - $F0` | | `ApuChannelEnableBitsSet` `$B0F3–$B0F6` | 4 | OR-ed into `ApuEnableMask`, indexed by `channel*4` | | `ApuChannelEnableBitsClear` `$B0F7–$B0FA` | 4 | AND-ed into `ApuEnableMask`, same index | | `StageNametableAndObjectData` `$B0FB–$B91A` + `_1` `$B971–$BD27` + `_2` `$BD2A–$BEBF` | 3,523 | the per-stage payload | | `StageDataFiller` `$B91B–$B970` | 86 | an 86-byte `$FF` pad between stage blocks | | `StageDataPointerLow` `$BD28–$BD29` | 2 | one base offset per player; `$32` apart | | `StageDataTail` `$BED0–$BF8F` | 192 | tail of the stage block | | `InitialOamAttributes` / `InitialOamTiles` `$BEC0–$BECF` | 16 | 8 entries each, `ldx #$07` | | `VramBlockByteCounts` / `VramBlockSourceLowBytes` `$BF90–$BF9F` | 16 | 8 transfer blocks each; high byte always `$02` | | `SoundSequenceAndLevelData` `$BFA0–$BFFA` | 91 | the `$B1A1`/`$B1B0`/`$B2B9`/`$B3B2` sequences the sound engine points at | | `InterruptVectors` `$BFFB–$BFFF` | 5 | NMI / RESET / IRQ | The stage payload is reached through `StageDataPointerLow` plus the 16-bit offset the bytecode stream carries, so no constant address indexes it and its record structure is not resolvable from a static trace. That is recorded in the source rather than guessed at. ### `$A5F4–$A65F` (108 bytes) → two structures `StageDescriptorPointerTable` ($A5F4–$A617, 18 × `.word`) and `StageDescriptorData` ($A618–$A65F). Two readers share the table: one reads `(A & $1E)` from entry 0, the other starts at entry 9, and entry 18 already reads descriptor text — so it is exactly 18 entries. The targets sit twelve bytes apart ($A618, $A624, $A630, $A63C, $A648) with a final ten-byte record at $A654; each is a short header followed by a descending run of tile indices. ### Remaining tables | Region | Size | What it is | |--------|------|------------| | `AttributeBlockDeltaTable` `$8B7D–$8B91` | 21 | 7 × 3-byte `{source lo, source hi, destination lo}` deltas | | `OamShadowTemplate` `$9EF4–$9F13` | 32 | the initial OAM block copied into $0300 | | `VramRunFillValues` `$ACBD–$ACC8` | 12 | the 12 fill values the VRAM run tokens `$B8`–`$C3` select | | `AnimationFrameCollisionBits` `$AE35–$AE44` | 16 | animation frame → collision flag bits | | `StandaloneBytecodeProgram` … `_2` | 110 | three token programs **not** in `BytecodeProgramPointerTable` | ## Pass 3 data-table inventory Every one of the 69 data blocks the tracer left in `prg.s` is listed here, once. `Size` is the byte count of the block; `Name` is the label in `prg.s` (a dash means the block has no name); `Structure` says what the bytes are; `Read by` gives the address of the instruction that reads them. Where nothing in this dump reads a block, the row says so and says what the bytes are instead. The bytecode VM dominates the PRG, so most of the rows are token programs: a program is **not** a table. A byte in `$C8`–`$EF` is an opcode, handled by `BytecodeHandlerTable` (`$9237`) and dispatched by `RunBytecodeInterpreter` (`$9AE8`); every other byte is a literal operand. The listing above each program's `.byte` data in `prg.s` spells the tokens out. | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | $85ED–$8638 | 76 | `StandaloneBytecodeProgram` | bytecode program: 4 opcodes (`$E8`, `$EE`, `$EC`, `$EE`) and 72 literal operands - a token stream, not a table | interpreted by `BytecodeHandlerTable` (`$9237`) through `RunBytecodeInterpreter` (`$9AE8`); it is **not** in `BytecodeProgramPointerTable`, and no instruction in this dump points at it | | $881F–$8836 | 24 | `DeathSequenceVramStream` | compressed VRAM (nametable RLE) stream, **not** bytecode: it contains no byte in `$C8`–`$EF` | the source pointer is set by `PlayDeathSequence` (`$8839`/`$883D`) and the block is decoded by `WriteCompressedVramBlocks` (`$ACF5`) with the RLE decoder at `$AC82`/`$AC88` | | $8A1E–$8A27 | 10 | `StandaloneBytecodeProgram_2` | bytecode program: 1 opcode (`$CD`) and 9 literal operands | interpreted through `BytecodeHandlerTable` (`$9237`); not referenced by any instruction or pointer in this dump | | $8B7D–$8B91 | 21 | `AttributeBlockDeltaTable` | 7 × 3-byte `{source lo, source hi, destination lo}` deltas | `sbc $8B7D,x` at `$8B3E`, `sbc $8B7E,x` at `$8B45` and `sbc $8B7F,x` at `$8B4C` (`ClearAttributeRegion`) | | $8CEF–$8D3A | 76 | `BytecodeProgramPointerTable` | 38 × 16-bit pointers to the bytecode programs that follow | `lda $8CEF,y` at `$9ADC` and `lda $8CF0,y` at `$9AE1` (`SpawnActorPairAlt_OnNonzero_9AD8`) | | $8D3B–$8D3F | 5 | `BytecodeProgram_00` | bytecode program: 4 opcodes, 0 literal operands; table index 13 | through `BytecodeProgramPointerTable` (`$9ADC`) and `RunBytecodeInterpreter` (`$9AE8`) | | $8D40–$8D5B | 28 | `BytecodeProgram_01` | bytecode program: 16 opcodes, 4 literal operands; table index 36 | as above | | $8D5C–$8D8C | 49 | `BytecodeProgram_02` | bytecode program: 9 opcodes, 34 literal operands; table index 37 | as above | | $8D8D–$8D95 | 9 | `BytecodeProgram_03` | bytecode program: 6 opcodes, 0 literal operands; table index 22 | as above | | $8D96–$8D9E | 9 | `BytecodeProgram_04` | bytecode program: 6 opcodes, 0 literal operands; table index 23 | as above | | $8D9F–$8DB5 | 23 | `BytecodeProgram_05` | bytecode program: 5 opcodes, 16 literal operands; table index 33 | as above | | $8DB6–$8DE1 | 44 | `BytecodeProgram_06` | bytecode program: 9 opcodes, 30 literal operands; table index 34 | as above | | $8DE2–$8E2C | 75 | `BytecodeProgram_07` | bytecode program: 9 opcodes, 60 literal operands; table index 35 | as above | | $8E2D–$8E78 | 76 | `BytecodeProgram_08` | bytecode program: 10 opcodes, 60 literal operands; table index 14 | as above | | $8E79–$8EA8 | 48 | `BytecodeProgram_09` | bytecode program: 12 opcodes, 30 literal operands; table index 11 | as above | | $8EA9–$8EF3 | 75 | `BytecodeProgram_0A` | bytecode program: 26 opcodes, 32 literal operands; table index 24 | as above | | $8EF4–$8F19 | 38 | `BytecodeProgram_0B` | bytecode program: 4 opcodes, 32 literal operands; table index 25 | as above | | $8F1A–$8F1F | 6 | `BytecodeProgram_0C` | bytecode program: 4 opcodes, 0 literal operands; table index 28 | as above | | $8F20–$8F47 | 40 | `BytecodeProgram_0D` | bytecode program: 3 opcodes, 36 literal operands; table index 31 | as above | | $8F48–$8F70 | 41 | `BytecodeProgram_0E` | bytecode program: 3 opcodes, 36 literal operands; table index 32 | as above | | $8F71–$8F93 | 35 | `BytecodeProgram_0F` | bytecode program: 34 opcodes, 0 literal operands; table index 29 | as above | | $8F94–$8FFC | 105 | `BytecodeProgram_10` | bytecode program: 3 opcodes, 100 literal operands; table index 16 | as above | | $8FFD–$9007 | 11 | `BytecodeProgram_11` | bytecode program: 7 opcodes, 0 literal operands; table index 15 | as above | | $9008–$9070 | 105 | `BytecodeProgram_12` | bytecode program: 4 opcodes, 98 literal operands; table index 17 | as above | | $9071–$907C | 12 | `BytecodeProgram_13` | bytecode program: 7 opcodes, 2 literal operands; table index 27 | as above | | $907D–$9080 | 4 | `BytecodeProgram_14` | bytecode program: 3 opcodes, 0 literal operands; table index 0 | as above | | $9081–$90A5 | 37 | `BytecodeProgram_15` | bytecode program: 11 opcodes, 20 literal operands; table index 1 | as above | | $90A6–$90B8 | 19 | `BytecodeProgram_16` | bytecode program: 2 opcodes, 16 literal operands; table index 26 | as above | | $90B9–$90C9 | 17 | `BytecodeProgram_17` | bytecode program: 2 opcodes, 14 literal operands; table index 5 | as above | | $90CA–$90D6 | 13 | `BytecodeProgram_18` | bytecode program: 9 opcodes, 0 literal operands; table index 2 | as above | | $90D7–$90DD | 7 | `BytecodeProgram_19` | bytecode program: 5 opcodes, 0 literal operands; table index 12 | as above | | $90DE–$9118 | 59 | `BytecodeProgram_1A` | bytecode program: 58 opcodes, 0 literal operands; table index 3 | as above | | $9119–$9130 | 24 | `BytecodeProgram_1B` | bytecode program: 16 opcodes, 0 literal operands; table index 4 | as above | | $9131–$9135 | 5 | `BytecodeProgram_1C` | bytecode program: 4 opcodes, 0 literal operands; table index 6 | as above | | $9136–$9140 | 11 | `BytecodeProgram_1D` | bytecode program: 7 opcodes, 0 literal operands; table index 7 | as above | | $9141–$9174 | 52 | `BytecodeProgram_1E` | bytecode program: 18 opcodes, 24 literal operands; table index 8 | as above | | $9175–$918A | 22 | `BytecodeProgram_1F` | bytecode program: 14 opcodes, 0 literal operands; table index 18 | as above | | $918B–$91DF | 85 | `BytecodeProgram_20` | bytecode program: 16 opcodes, 56 literal operands; table index 19 | as above | | $91E0–$91EB | 12 | `BytecodeProgram_21` | bytecode program: 8 opcodes, 0 literal operands; table index 20 | as above | | $91EC–$922B | 64 | `BytecodeProgram_22` | bytecode program: 10 opcodes, 48 literal operands; table index 21 | as above | | $922C–$922E | 3 | `BytecodeProgram_23` | bytecode program: 2 opcodes, 0 literal operands; table index 9 | as above | | $922F–$9231 | 3 | `BytecodeProgram_24` | bytecode program: 2 opcodes, 0 literal operands; table index 10 | as above | | $9232–$9236 | 5 | `BytecodeProgram_25` | bytecode program: 4 opcodes, 0 literal operands; table index 30 | as above | | $9237–$9286 | 80 | `BytecodeHandlerTable` | 40 × 16-bit opcode handlers, index = (token − `$C8`) | `lda $9237,x` at `$9AF9` and `lda $9238,x` at `$9AFE` (`RunBytecodeInterpreter`) | | $9EF4–$9F13 | 32 | `OamShadowTemplate` | the 32-byte initial OAM block copied into the shadow at $0300 | `lda $9EF4,y` at `$839B` (`LoadOamShadowFromTable`) | | $9F2B–$9F4A | 32 | `SecondaryBytecodeHandlerTable` | 16 × 16-bit handlers, index = (token & `$0F`) × 2 | `lda $9F2B,y` at `$9F85` and `lda $9F2C,y` at `$9F8A` (`MoveActorByOpcode`) | | $A5F4–$A617 | 36 | `StageDescriptorPointerTable` | 18 × 16-bit pointers into `StageDescriptorData` | `lda $A5F4,y` at `$A539` and `lda $A5F5,y` at `$A53E`; the writer reads `lda $A606,y` at `$A593` and `lda $A607,y` at `$A598` | | $A618–$A65F | 72 | `StageDescriptorData` | six 12-byte descriptor records (tile/attribute runs) plus a final 10-byte record; the last record ends with the two `$30` terminators | read through `StageDescriptorPointerTable` (`$A539`/`$A53E`/`$A593`/`$A598`) | | $AB14–$AB67 | 84 | `LevelOamTemplateTable` | **not** bytecode (no byte in `$C8`–`$EF`): ten 8-byte per-level OAM records, the level byte at `$0326` selecting the record; the last four bytes, `$AB64`–`$AB67`, are the OAM slot copied to `$031C` | the source pointer is built at `$AB85`–`$AB96` from `$0326` and the block is copied by `LoadShadowOamBlock` (`$AB98`); `$AB64` is read at `$AB70`/`$AB74` and copied by the routine at `$AB68` | | $AC35–$AC3A | 6 | `LevelVramWriterTable` | 3 × 16-bit VRAM writers, index = the level byte at `$0326` | `lda $AC35,x` at `$AC28` and `lda $AC36,x` at `$AC2D` (`SelectLevelVramWriter`) | | $ACBD–$ACC8 | 12 | `VramRunFillValues` | the 12 fill values the compressed-VRAM run tokens `$B8`–`$C3` select, index = token − `$B8` | `lda $ACBD,x` at `$ACAA` (`WriteVramRunToken`) | | $AE35–$AE44 | 16 | `AnimationFrameCollisionBits` | 16 animation-frame → collision-flag bit entries, index = frame & `$0F` | `lda $AE35,x` at `$AE2F` | | $AF37–$AF3A | 4 | `UnreachableSoundInitIsland` | four bytes of orphaned code (`lda $E9 / bne $AF34`) that follow the `rts` ending `InitializeSoundEngine` | not referenced by any instruction | | $B0CB–$B0E2 | 24 | `SoundNotePeriodTable` | 12 × 16-bit tone periods, index = note number (guarded by `cmp #$0C`) | `lda $B0CC,y` at `$AFEA` and `lda $B0CB,y` at `$AFEF` (`ComputeTonePeriod`) | | $B0E3–$B0F2 | 16 | `SoundNoteDurationTable` | 16 note durations, index = (duration byte − `$F0`) | `lda $B0E3,y` at `$B046` (`ReadNoteDuration`) | | $B0F3–$B0F6 | 4 | `ApuChannelEnableBitsSet` | four 1-byte masks OR-ed into the APU enable mask, index = channel × 4 | `ora $B0F3,x` at `$B05F` (`BuildApuChannelMask`) | | $B0F7–$B0FA | 4 | `ApuChannelEnableBitsClear` | four 1-byte masks AND-ed into the APU enable mask, index = channel × 4 | `and $B0F7,x` at `$AF6D` (`RebuildApuEnableMask`) | | $B0FB–$B91A | 2080 | `StageNametableAndObjectData` | a shared data pool: compressed-VRAM RLE streams, per-actor bytecode and object lists mixed together | no constant address indexes it: `adc $BD28,x` at `$A063` forms the address from a runtime value, and the RLE reader at `$AC82` walks it sequentially | | $B91B–$B970 | 86 | `StageDataFiller` | an 86-byte `$FF` pad inside the pool | not referenced by any instruction | | $B971–$BD27 | 951 | `StageNametableAndObjectData_1` | continuation of the shared pool | as above | | $BD28–$BD29 | 2 | `StageDataPointerLow` | one base offset per player; the second player's copy is `$32` further on | `adc $BD28,x` at `$A063` (`ProcessBytecodeOpcode`) | | $BD2A–$BEBF | 406 | `StageNametableAndObjectData_2` | continuation of the shared pool | as above | | $BEC0–$BEC7 | 8 | `InitialOamAttributes` | 8 sprite-attribute bytes (`ldx #$07`) | `lda $BEC0,x` at `$89B5` (`CompactOamList`) | | $BEC8–$BECF | 8 | `InitialOamTiles` | 8 sprite-tile bytes | `lda $BEC8,x` at `$89C0` (`CompactOamList`) | | $BED0–$BF8F | 192 | `StageDataTail` | the tail of the shared pool, ahead of the VRAM block descriptors | not referenced by any instruction | | $BF90–$BF97 | 8 | `VramBlockByteCounts` | 8 block sizes fed to `ExecuteVramTransfer` | `lda $BF90,x` at `$89EE` (`ReadControllerAndInit`) | | $BF98–$BF9F | 8 | `VramBlockSourceLowBytes` | 8 block source low bytes; the high byte is always `$02` | `lda $BF98,x` at `$89F7` (`ReadControllerAndInit`) | | $BFA0–$BFFA | 91 | `SoundSequenceAndLevelData` | the `$B1A1`/`$B1B0`/`$B2B9`/`$B3B2` sound sequences plus the level tables, ending just before the vectors | not read by a constant address: the sound engine copies a sequence address from `SoundSequencePtrLo1`/`Hi1` into `SoundReadPointerLo`/`Hi` (`$AF76`/`$AF7A`) and reads through it (`$AF80`, `$B03C`) | | $BFFB–$BFFF | 5 | `InterruptVectors` | the three CPU vectors (NMI `$86CB`, RESET `$8000`, IRQ `$8764`), mirrored into the ROM at `$FFFB`–`$FFFF` | read by the CPU's reset logic, not by an instruction | ### What the inventory settled * **Two blocks that looked like bytecode programs are not bytecode at all.** `$881F` is the compressed VRAM stream for the death sequence - `PlayDeathSequence` points the source pointer at it and `WriteCompressedVramBlocks` decodes it - and `$AB14` is the per-level OAM template table that `LoadShadowOamBlock` copies into the shadow at `$0300`. Both contain no byte in `$C8`–`$EF`, which is the test the interpreter itself applies, so the interpreter could never have run them. * **The four handler tables are the whole dispatch mechanism**, and they are the only ROM data the VM touches directly: `BytecodeProgramPointerTable` picks the program, `BytecodeHandlerTable` picks the opcode handler, and the two secondary tables serve the movement opcodes. * **The stage payload is reached only through a runtime value.** Exactly one operand in `$B0FB`–`$BEBF` exists in the whole image - the `adc $BD28,x` at `$A063` - so the record structure of that pool cannot be enumerated statically. It is recorded as a shared pool rather than as per-stage data. ## What Pass 3 settled **The `$14`/`$15` reuse.** These are both the 16-bit block counter for the compressed-VRAM transfer *and* the accumulator for the collision bit flags. `locals.inc` carries both names, and `zero_page_ranges.tsv` applies `StreamBlockCountHi` only inside `AdvanceCountWord`, so the same byte is not mislabelled at all 53 of its other uses. This is the alias pattern `AGENTS.md` asks for — `RandomStateAlt = $E4 beside RandomState0 = $54` — done with a range instead of a blind rename. **The four hand-seeded entries are provably dead.** `$8644`, `$8653`, `$9CAD` and `$ABF7` have no 16-bit reference anywhere in the image except `jsr $8644` at `$8654`, and none of them appears in either bytecode handler table. They are kept as code — `rts`-terminated and register-plausible — but their headers now say so explicitly rather than leaving a reader to wonder. **`$AF37–$AF3A` stays data.** `lda $E9 / bne $AF34` sits right after the `rts` that ends `InitializeSoundEngine` and branches back into the middle of it. Unreachable, incoherent as a routine, and referenced by nothing. It is labelled `UnreachableSoundInitIsland` so the decision is visible. **Every routine header now says something.** The 94 subsystem routines keep hand-written descriptions; the other 606 get a generated line naming the routine and its visible call sites, which is factual. Nothing still reads "Contextual control-flow entry identified from its parent routine". ## `copilot/` Contents | File | Purpose | |------|---------| | `rom.nes` | symlink to `../M.U.S.C.L.E. (U).nes` | | `prg.bin` / `header.bin` / `chr.bin` | split image (`_tools/splitnes.py`) | | `prg.s` | fully classified and named PRG source (12,922 lines) | | `header.s` / `chr.s` | `incbin` of the untouched header and CHR | | `symbols.inc` | shared NES hardware constants | | `locals.inc` | 163 zero-page aliases with read/write counts and referencing addresses | | `routine_names.tsv` | Pass 2 routine names (address → name) | | `label_names.tsv` | generated: the full `Label_XXXX` → final name map | | `zero_page_names.tsv` | Pass 2 zero-page names with usage notes | | `zero_page_ranges.tsv` | Pass 3 range-scoped names for reused bytes | | `data_regions.tsv` | Pass 3 data-region labels, record formats and notes | | `routine_purposes.tsv` | the 94 hand-written `; Global:` descriptions | | `linker.cfg` | HEADER at $0000, PRG at **$8000**, CHR at $0000 | | `Makefile` | `all` builds and byte-compares; `pass1` / `pass2` / `pass3` / `regen` regenerate | ## Build ```bash cd "MUSCLE (U)/copilot" make # header.o + prg.o + chr.o -> reconstructed.nes, cmp vs rom.nes # "Byte-identical ROM verified." make pass1 # re-run _tools/nesdis.py with the recorded classification flags make pass2 # pass1 + naming + descriptions + data structuring + locals.inc make pass3 # pass2 + _tools/bytecode_dis.py token listings over the 42 VM programs make regen # same as pass3; the whole pipeline is byte-for-byte reproducible ``` ## Pass 3b — the bytecode VM, decoded The 42 token programs (`BytecodeProgram_00`–`_25` plus `StandaloneBytecodeProgram`–`_3`) are the game's scripting layer. Each now carries a byte-for-byte token listing above its unchanged `.byte` data, produced by `_tools/bytecode_dis.py` (wired in as `make pass3`). ### The encoding `RunBytecodeInterpreter` (`$9AE8`) is the whole dispatch: ``` $9AEA lda ($1A),y ; fetch one byte $9AEC iny $9AED cmp #$F0 / bcs $9B08 ; >= $F0 -> literal $9AF1 cmp #$C8 / bcc $9B08 ; < $C8 -> literal $9AF5 sbc #$C8 / asl a / tax $9AF9 lda $9237,x / sta $1C ; BytecodeHandlerTable $9AFE lda $9238,x / sta $1D $9B05 jmp ($001C) ; run the handler ``` and the literal path (`$9B08`) pushes the byte onto the operand list: ``` $9B08 pha $9B09 lda ($1A),y / iny ; the operand byte itself $9B0C jsr $94C8 ; save stream position, restore the operand index ... $9B22 sta $0033,Y ; operand list $9B25 jsr $9CBB ; block/stream bookkeeping ``` So **the encoding is one byte per element**: a byte in `$C8`–`$EF` selects one of the 40 `BytecodeHandlerTable` entries, and every other byte is a literal operand appended to `$0033[Y]` (and accumulated into `$003C[Y]`). There are no variable-length opcodes *except* four. **Four opcodes take one inline operand byte.** Their handler's first instructions are `lda ($1A),Y / iny`, so the byte after the token is consumed by the opcode itself: | Opcode | Handler | Inline byte stored in | Meaning | |--------|---------|------------------------|---------| | `$CB` | `$946D` | `$7C,X` (count) and `$7E,X` (body address) | `LOOP_BEGIN` | | `$D1` | `$950B` | `$43,X` | `SET_RATE` | | `$D7` | `$937C` | used as a count for the `$A660` blit | `DRAW_SPRITE` | | `$E1` | `$9465` | `$7A,X` | `SET_WAIT` | `$CB`/`$C9` are the loop pair: `LOOP_BEGIN` marks the stream position and loads the counter, and `LOOP_END` (`$9477`) decrements `$7C,X` and, if still non-zero, reloads `Y` from `$7E,X` — which rewinds the interpreter to the top of the loop body. ### What was decoded | | | |---|---| | Programs listed | **42** blocks: 38 reached by `BytecodeProgramPointerTable`, plus `StandaloneBytecodeProgram`, `StandaloneBytecodeProgram_2`, `DeathSequenceVramStream` and `LevelOamTemplateTable` | | Opcodes decoded | **377** (372 in the 38 table programs, 4 in `StandaloneBytecodeProgram`, 1 in `StandaloneBytecodeProgram_2`) | | Literal operands | **937** in total, but only **829** of them are in real bytecode programs - the other 108 belong to the two blocks that turned out not to be bytecode at all | | Opcodes defined / used | 40 / 40 — the opcode set is complete, nothing unused | Two of the four blocks outside the pointer table contain **no byte in `$C8`–`$EF`**, so the interpreter could never have run them, and their listing says so explicitly rather than implying they are code. They are not bytecode: `DeathSequenceVramStream` (`$881F`, 24 bytes) is the compressed VRAM stream of the death sequence, and `LevelOamTemplateTable` (`$AB14`, 84 bytes) is the per-level OAM template table. Both were renamed once that was established - see **Pass 3 data-table inventory** below. ### Confidence Each mnemonic was read off its handler's own straight-line path, stopping at the handler's first `jmp $9AE8` / `jmp $9B25`. Mnemonics that describe register-level behaviour (`ACC_ADD_1_STEP`, `STEP_ACCUM`, `ROT_STEP_SUB`, …) are read directly off the instructions. Those that merely tail-call a game routine are named for that routine (`VM_CALL_82E1`, `PLAY_SEQ_BD20`) rather than guessed at, because the semantic content lives in the callee, not the handler. --- ## Pass 3c — tracing the stage payload Static analysis cannot reach this region, and the trace confirms why. Across all 16 KB of code there is exactly **one** operand in `$B0FB–$BEBF`: ``` operands in $B0FB-$BEBF : 1 -> $BD28 StageDataPointerLow, the literal pointer table itself ``` The real address is formed at runtime, `$A062`: ``` $A062 clc $A063 adc $BD28,X ; A (a value carried in the bytecode stream) + table byte $A066 sta $12 ``` so the readers are indexed by a *runtime value* and cannot be enumerated by searching the code. ### Method FCEUX's Lua API has no memory-read hook — only `registerexec`, `registerexecute`, `registerrun` and `registerwrite` — so it cannot observe these reads either, and with no static list of readers there is nothing to hook. `_tools/trace_stage_payload.py` therefore uses **py65**, whose `ObservableMemory.subscribe_to_read` fires on every read of a range and can report the CPU's PC from inside the hook. py65 is only a 6502 core, so the tracer adds the minimum hardware to get the game running: a scanline counter driven off `processorCycles`, `$2002` vblank asserted at scanline 241 and cleared on read, NMI fired at the same scanline (MUSCLE does its per-frame work in `NmiHandler` `$86CB`), `$4014` OAM DMA, and the `$2005`/`$2006`/`$2007` latches. Without NMI the game never leaves `$80E0: lda $0321 / beq $80E0`, because `$0321` is the flag the bytecode VM raises — so the stage data is only reachable once interrupts work. ### Results (300 frames, 26.8M cycles, 4,071 NMIs) | | | |---|---| | Payload reads captured | **2,548** | | Distinct reader PCs | **44** | | Unique payload addresses touched | **914** of 3,421 bytes in range | Reader PCs are 1-2 bytes above the real instruction, because py65 advances PC past the opcode before fetching the effective address. ### What the access pattern shows **1. The payload is read through indirect pointers, confirming the static finding.** The hottest reader resolves to `$AC82`: ``` $AC82 lda ($10),Y $AC84 cmp #$C0 $AC86 bne $AC96 $AC88 lda $10 / clc / adc #$01 / sta $10 ; advance the stream pointer ``` This is the compressed-VRAM RLE decoder - it walks the payload **sequentially** (1,046 of its 1,128 reads advance by exactly +1), comparing each byte against `$C0`. That matches the `$AC82` RLE decoder already named in the data section. **2. There is a second, backward-copy reader.** `$9B51` resolves to ``` $9B50 lda ($12),Y $9B52 ldy $14 $9B54 sta ($10),Y $9B56 dey ``` and its reads step by **-1**, sixty times - a reverse copy between two buffers. **3. The payload contains 8-byte-aligned record arrays.** 107 distinct 8-byte blocks had all eight bytes read, and the spacing between consecutive blocks is dominated by **$08**: ``` $B1F0 $B1F8 $B200 $B208 $B210 $B218 ... (stride $08) $B3B8 $B3C0 $B3C8 $B3D0 ... $B5A0 (a long $08-strided run - the object lists) $B600 $B608 $B610 ... $B638 $B8A0 $B8A8 $B8B0 ... $B910 ``` with a second, sparser group on a **$D0 stride**: ``` $BBE0 $BCB0 $BD80 $BE50 ``` so at least two record tables exist: a dense 8-byte-stride array and a $D0-stride array. **4. Readers use three access disciplines** - sequential forward (+1), sequential backward (-1), and variable stride (+$15/+$14/+$11 at `$ACB2`, +2/+$4D at `$AF5B`/`$B03D`), the last consistent with a field-at-fixed-offset walk across records. ### Still open * Which reader corresponds to which semantic field is not yet pinned down; the trace gives the geometry (8-byte records, $08 and $D0 strides) but not the field names. * Only 914 of 3,421 bytes were touched, because the run never entered every stage. A longer run that cycles through all stages would cover more and may reveal further strides. * The compressed-VRAM payload and the object-list records are interleaved in one block; the boundary between them is not yet resolved. FCEUX remains useful as an independent oracle for any of these conclusions - it is a complete, correct emulator where this tracer is a deliberately partial one. --- ## Pass 3d — the payload is a shared pool, not stage data Pass 3c assumed `$B0FB–$BEBF` was per-stage level data and that "cycle all stages" would cover it. A longer, properly-clocked run **disproves that**. Coverage plateaus near 40% no matter how many stages are entered, and forcing every actor index to run adds **zero** new bytes. ### Two tracer bugs, both of which were suppressing everything **1. NMI re-armed by `$2002` reads.** The vblank flag must be cleared once per *frame*, not on every read of `$2002`. The main loop polls `$2002` and spins on the bytecode VM flag at `MainGameLoop_OnZero_8226` (`lda $0321 / beq $8235`); each poll clears bit 7, re-arming NMI inside the same scanline. Guarding only on `not vblank` produced **40,900 NMIs across 3,000 frames** and silently corrupted all game state. A separate per-frame `nmi_fired` flag restores exactly one NMI per frame — verified: 600 frames → 600 NMIs. **2. Input went to the wrong port.** The poll routine at `$AA69` is ``` $AA69 ldx #$01 / stx $4016 / dex / stx $4016 / ldy #$08 / lda $4016,X / and #$03 / cmp #$01 ``` so the *observed* reads are of `$4017` (X=1) and only bits 0-1 are ever compared. Driving `$4017` therefore consumes nothing: all three masks tested gave byte-identical traces. The game reads **joy1 on `$4016`**, and needs a real latch/shift register — a write with bit0 set latches, reads shift `A,B,Select,Start,Up,Down,Left,Right` out LSB-first. With that, START visibly matters: **47.7% coverage vs 39.0%** for other masks. Reaching a match takes **two discrete START presses** — title → character select → match. Rapid re-tapping does not work, and each press must be held ~3 frames. ### What `$BD28` actually indexes `$BD28` is not a stage table. The one operand in the window feeds: ``` $A04A lda $0324 / beq $A062 $A04F lda ActorIndex ($1E) / beq $A062 $A063 adc $BD28,X ; X = ActorIndex $A06A lda PlayerActiveFlag,X ($78,X) $A071 adc #$32 ; +$32 when that actor is active ``` `$BD28 = 6B 6B 6E 75 75 80 5A 78` is an **8-entry per-actor entry-offset table**, and `$B0FB + $BD28,n` is where actor *n*'s bytecode starts. Observed `ActorIndex` values over a 2,400-frame match are `$00–$04`, so the live actor set is 0–4. ### The sound engine reads the same window With input working, the reader list gained PCs *inside* `$B0FB–$BEBF`. These are not new code — they are the sound engine's own stream reader: ``` $B03C lda (SoundReadPointerLo),Y $B03E cmp #$F0 $B040 bcc StoreShortNoteDuration $B043 sbc #$F0 / tay / lda $B0E3,y ; NoteDurationTable at $B0E3 ``` `SoundReadPointerLo` ($EE/$EF) points into the payload while music plays, and the `$F0` compare plus the `$B0E3` duration lookup is note decoding. This is why the windows' bytes carry so many `$FF` terminators — `$FF`/`$F0` are the high-nibble codes of the note stream. So `$B0FB–$BEBF` is a **shared data pool**, mixing at least three things: per-actor bytecode scripts, the compressed-VRAM RLE stream (`$AC82`), and music note streams. It is not stage-specific, which is why stage cycling cannot raise coverage past ~40%. ### Coverage summary | Run | Frames | NMIs | Payload reads | Readers | Coverage | |---|---|---|---|---|---| | Pass 3c (NMI broken) | 300 | 4,071 | 2,548 | 44 | 26.3% | | NMI fixed, no input | 600 | 600 | 29,808 | 14 | 39.0% | | Two START presses | 2,000 | 2,000 | 79,192 | 21 | 40.9% | | Actor sweep 0–7 | 2,400 | 2,400 | 98,392 | 14 | 38.7% | Largest unreached spans after the two-START run (2,161 B in 15 runs) are `$BC00–$BEBF` (704 B), `$B980–$BB0F` (400 B), `$BB20–$BBEF` (208 B), `$B0FB–$B1A0` (166 B) and `$B81E–$B89D` (128 B). ### What would actually close the gap Not stage cycling. The remaining bytes need **other music tracks playing** and **actors 2–4 in their non-default states**. Both need a scripted match (moves, grapples, ring-outs), not a button-tap sequence — i.e. gameplay simulation, which is well beyond a coverage tracer. `$0326` is *not* a level byte: it never changes from `$00` in any run, including a confirmed match. `$0320` toggles `$00`/`$FF` and behaves as a boolean, not a screen-state machine. --- ## Still Open * ~~**Promote to the numbered inventory.**~~ **Done** — promoted to top-level `MUSCLE (U)/` and added as #16 in the root inventory. `copilot/Makefile` tool paths moved from `../../../_tools/` to `../../_tools/` for `NESDIS`/`PASS2`/`BYTECODE`; `make` and `make regen` both verified after the move. * **The payload record structure is partially decoded** (`$B0FB–$BEBF`, 3,525 bytes) — see "Pass 3d" above for what the emulator run established. It is a *shared pool* (per-actor bytecode + compressed VRAM + music note streams), not per-stage data; the per-record field names and the boundaries between the three uses are still unresolved. * **Two blocks are named but not fully interpreted.** `StageDescriptorData` (`$A618–$A65F`) is reached through `StageDescriptorPointerTable`, and the six 12-byte records are known to be tile/attribute runs, but which field of which record the game reads is not pinned down. The same is true of `LevelOamTemplateTable` (`$AB14–$AB67`): the routine that copies it into the OAM shadow at `$0300` is known, the eight-byte record layout is not. * **`StandaloneBytecodeProgram` (`$85ED–$8638`) and `StandaloneBytecodeProgram_2` (`$8A1E–$8A27`) are still unreachable.** Neither is in `BytecodeProgramPointerTable` and no instruction in the image points at either, so the VM never runs them; they are carried as bytecode because their bytes decode as tokens, but nothing in this dump says what starts them. * **The `BytecodeProgramPointerTable` index is masked to `$3F`** (`and #$3F` at `$9AD8`), so an index of 38 or more would read past the 38-entry table. Nothing in the dump produces such an index, but the safety of the mask is untested.