# MUSCLE (U) — Pass 3 (data names and structures) ROM: `_wip/MUSCLE (U)/M.U.S.C.L.E. (U).nes` (24,592 bytes, iNES). Status: **PASS 3 COMPLETE — byte-identical.** 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. `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 | | 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.** | ## ROM Facts | Property | Value | |----------|-------| | Mapper | 0 (NROM) | | PRG | 16 KB, mapped **low** at $8000–$BFFF (mirrored to $C000–$FFFF) | | CHR | 8 KB | | Mirroring | Horizontal (header byte 6 = $01) | | 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_36` `$8D3B–$9236` | 1,034 | the 37 distinct programs, each labelled with the table indices that reach 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. The two duplicate entries account for 37 distinct programs. ### `$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` … `_3` | 194 | four token programs **not** in `BytecodeProgramPointerTable` | ## 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` / `regen` regenerate | ## Build ```bash cd "_wip/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 regen # same as pass2; the whole pipeline is byte-for-byte reproducible ``` ## Still Open * **Promote to the numbered inventory.** The project still lives in `_wip/`; moving it to a top-level `/copilot/` is the last mechanical step, keeping the NROM-at-`$8000` note. * **The stage payload record structure** (`$B0FB–$BEBF`, 3,523 bytes) is named but not decoded. Working it out needs emulation or a token-level trace of the stage scripts, not static analysis. * **`BytecodeProgram_00`–`_36` are not disassembled.** They are the game's scripting layer; a token-level disassembler keyed off `BytecodeHandlerTable` would turn them into readable code.