# Mario Bros (JU) — Disassembly ROM: `_wip/Mario Bros (JU)/Mario Bros (JU).nes` (24,592 bytes, iNES). Status: **SKELETON — not yet disassembled.** The entire 16 KB PRG bank is a single unclassified block; `copilot/prg.s` is the default-project skeleton (`default_project.tar.gz`) that reproduces it as one `.incbin`. The skeleton build is **byte-identical** at the PRG level. ## 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 | `2810051c21132abd8b9b1e1bf741b302ee6c1d4af983e0df7fe50bd05ac0a5c8` | | PRG SHA-256 | `699ae2908486fce5a0fffdd051285e1144021f616d9af88eb511f319539acce1` | ## Interrupt Vectors (taken from PRG $3FFA–$3FFF) | Vector | Target | Note | |--------|--------|------| | NMI | `$C07D` | per-frame heartbeat | | RESET | `$C000` | cold start at $C000 — the usual pattern in this repo | | IRQ | `$C000` | redirects to RESET — unused placeholder | ## `copilot/` Contents | File | Purpose | |------|---------| | `Mario Bros (JU).nes` | original ROM image (reference) | | `prg.bin` / `header.bin` / `chr.bin` | split image, produced by `_tools/splitnes.py` | | `prg.s` | Pass-1 skeleton: `.incbin "prg.bin", $0000, $4000` (whole PRG) | | `symbols.inc` | shared NES hardware constants (PPU/APU/controller, from the skeleton) | | `locals.inc` | project-local zero-page aliases — empty for now (filled in Pass 2) | | `linker.cfg` | PRG at $C000, 16 KB, `fill = yes` | | `Makefile` | links `reconstructed_prg.bin` and `cmp`s it against `prg.bin` | | `reconstructed_prg.bin` | build output from the last verified build | Header/CHR are reproduced verbatim from `header.bin`/`chr.bin` (not linked): `make` verifies **PRG bytes only** at this stage, which is exactly what a skeleton proves. When this project is promoted to a full disassembly, `header.s` / `chr.s` (as e.g. `Burger Time (U)`) and a full-ROM `make` target get added. ## Build ```bash cd "_wip/Mario Bros (JU)/copilot" make # → cmp reconstructed_prg.bin prg.bin → "BYTE-IDENTICAL" ``` ## Pass Status | Pass | Status | Description | |------|--------|-------------| | Pass 0 | Superseded | `prg.s` was a 12-line `.incbin` of the whole PRG — no disassembly at all | | Pass 1 | **Complete** | 5,735 instructions / **11,829 code bytes (72.2%)** / 4,555 data bytes. Byte accounting exact: 11,800 + 4,584 = 16,384. 937 labels, 937 routine headers | | Pass 2 | **Substantial** | **252 subroutine entry points named** (all `jsr`/`jmp` targets resolved from raw `$XXXX` to labels): **90 semantic names** (audio/sound engine, gameplay helpers) + **165 address-based `SubCxxxx` names** for the remainder. **667 `Label_Cxxxx` internal branch targets remain** — these are local control-flow targets (loop starts, branch destinations) within routines, not subroutine entry points, and do not need semantic names for the call graph to be readable. Zero raw `$XXXX` call operands remain. | | Pass 3 | Outstanding | Data regions inside the 4,555 unreached bytes need naming | ### How Pass 1 got to 72.2% | Step | Instructions | Code | |------|--------------|------| | Vectors + direct `jsr`/`jmp`/branch, official opcodes | 1,185 | 2,392 B (14.6%) | | `--undocumented` (105 extra NMOS opcodes) | 1,210 | 2,445 B (14.9%) | | `--auto-seed` (recovered 61 table-referenced entries) | 4,187 | 8,375 B (51.1%) | | `--extra-seed 0xC0B6,0xC5A3` | **5,735** | **11,829 B (72.2%)** | A plain vector trace reaches only 14.6% because almost all of this game sits behind computed dispatch. ### The dispatch mechanisms **1. Function-pointer tables, dispatched through the stack.** `$CD9E` is the single choke point: ``` $CDA1 pla / sta $14 ; pop the table address the caller pushed $CDA4 pla / sta $15 $CDA7 lda ($14),y / tax ; read the entry, indexed by A*2 $CDAB lda ($14),y / sta $15 $CDB1 jmp ($0014) ; and jump to it ``` The table pointer arrives via `pla`, not `lda`, so it cannot be followed statically from the dispatcher — the pushed value is only known in the caller's frame. This is why `INDIRECT: [('0xcdb1', '0x14')]` is reported and left unfollowed. **2. A 20-entry table of `JSR` thunks at `$C5A3`.** Probing it linearly yields twenty valid `jsr`s followed by `rts`: ``` $C5A3 jsr $D56E / jsr $D202 / jsr $D301 / jsr $C5DB / jsr $C66A / jsr $E783 ... ``` Seeding `$C5A3` pulls in all twenty targets at once, which is most of the jump from 51% to 72%. **3. The NMI phase dispatcher at `$C0B6`**, reached only by computed dispatch: ``` $C0B6 lda $B0 / beq $C0BF $C0BA lda $0320 / bne $C0C2 $C0BF jmp $C149 ``` Seeding `$C0B6` recovered 677 bytes that the vector trace could not see. ### Tracer fixes required to get there Two overlapping-decode bugs had to be fixed in `_tools/nesdis.py` first: * **Control-flow targets must win over fall-through.** `bne` at `$C4BA` targets `$C528`, but the fall-through path had decoded `$C527` as a 2-byte `NOP #$A5` whose operand swallowed `$C528`. No real assembler branches into the middle of an instruction, so the spanning decode is now dropped in favour of the target. Its bytes fall out as data, so byte accounting is unaffected. * **Branch targets inside a surviving instruction must be pruned.** Otherwise `emit_code`'s linear walk consumes those bytes as an operand, the label is referenced but never defined, and **the file does not assemble at all** — `Error: Symbol 'Label_C528' is undefined`. Neither change affects any already-regen-stable project; MUSCLE's `make regen` was re-checked and still reproduces `prg.s` byte-for-byte. ### Known remaining gaps * The tracer still reports `INVALID: ['0xc5a7']` and three overlaps (`$C528`, `$C5A2`, `$C5A4`). The overlaps are now resolved in favour of the control-flow target, so they are diagnostics rather than defects, but the `$C5A2`/`$C5A4` pair means the alignment between the `D4`-padding run and the `$C5A3` thunk table is not fully settled. * 4,555 bytes (27.8%) are still data. Reaching more needs the tables that the stack dispatcher at `$CD9E` reads, which means resolving the pushed addresses in the callers' frames — that is Pass 2/3 work, not more seeding. ### Pass 2 so far, and why it is partial Two mechanical wins came first: * **477 `jsr`/`jmp` operands rewritten from raw `$XXXX` to labels.** The tracer emits call and jump targets as hex, so the call graph was unreadable — the most-called routine appeared as `jsr $CCA0` 14 times. All 477 sites now name their target, and zero raw hex call operands remain. * **252 subroutine entry points named:** **90 with semantic names** (audio/sound engine routines in `$F8xx-$F9xx`, gameplay helpers like `CheckZpB3Return`, `CheckRangeReturn012`, `ToggleZpFC_Bit3`) + **165 with address-based names** (`SubCxxxx`) for the remainder. All 252 `jsr`/`jmp` targets now resolve to labels. **667 `Label_Cxxxx` internal branch targets remain**, and that is the honest state. These are local control-flow destinations (loop starts, conditional branch targets) within routines — not subroutine entry points. Naming them requires reading ~5,700 instructions across 935 routines to establish what each one does; doing it from the call graph and the NES convention alone would be guesswork, and `PROMPTS.md` explicitly forbids skipping ahead to guessy rewrites. The productive next step is not more naming but **more reach**: the 4,555 unreached bytes are where most of the remaining unnamed routines live, and they are gated behind the stack-pointer dispatch at `$CD9E`. Resolving the pushed table addresses would enlarge both the code set and the set of names worth assigning. ## Next Steps (per `PROMPTS.md`) 1. **Pass 1** — complete (72.2% code, byte accounting exact). 2. **Pass 2** — substantial: all 252 subroutine entry points named; 667 internal branch labels (`Label_Cxxxx`) remain as local control-flow targets. Filling `locals.inc` with zero-page aliases is optional at this stage. 3. **Pass 3** — name data tables/structures in the 4,555 unreached bytes; release provisional flags. 4. **Promotion** — move to a top-level `/copilot/` folder, add full-ROM build (header.s + chr.s + vectors), and write the power-on flow `OVERVIEW.md` in the established style; then it joins the root index's numbered inventory.