Files
disasm/AGENTS.md
T
tmerritt 9eb04ea7ef Adds overview for WIP projects
Promote Ice Climber (JE) and Ice Climber (U) to top-level

- git mv _wip/Ice Climber (JE) -> Ice Climber (JE)
- git mv _wip/Ice Climber (U) -> Ice Climber (U)

- Added header.s / chr.s (one-line .incbin each)
- Added HEADER/PRG/CHR linker.cfg with proper memory layout
- Added copilot/rom.nes symlink to original ROM
- Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes)
- Updated _tools paths from ../../../_tools/ to ../../_tools/

Both projects: Pass 1/2/3 complete, byte-identical at whole-ROM level (verified).

Root OVERVIEW.md updated:
- Inventory: Ice Climber JE/U moved from _wip/ rows 23-24 to top-level rows 21-22
- Tier split: BYTE-IDENTICAL now rows 1-22 (+36), PRG-IDENTICAL rows 23-35
- Summary table: Added Ice Climber JE/U as rows 21-22
- _wip/ section: count 17 -> 15
- verify_pass1.py scope: 19 -> 22 top-level projects

All 22 top-level projects verified byte-identical.

Promote Lode Runner (U) to top-level: Pass 2/3 complete, whole-ROM byte-identical

- git mv _wip/Lode Runner (U) -> Lode Runner (U)

- Added header.s / chr.s (one-line .incbin each)
- Added HEADER/PRG/CHR linker.cfg with proper memory layout
- Added copilot/rom.nes symlink to original ROM
- Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes)

Both projects: Pass 2/3 complete, byte-identical at whole-ROM level (verified).

Root OVERVIEW.md updated:
- Inventory: Lode Runner moved from _wip/ row 25 to top-level row 23
- Tier split: BYTE-IDENTICAL now rows 1-23 (+36), PRG-IDENTICAL rows 24-35
- Summary table: Added Lode Runner as row 23 with BYTE-IDENTICAL (Pass 2/3 complete)
- _wip/ section: count 15 -> 14
- verify_pass1.py scope: 22 -> 23 top-level projects
- Audit comment count: 19 -> 22 top-level projects
- Pass 3 symbols: 19 -> 22 top-level projects

All 23 top-level projects verified byte-identical.

TODO.md updated:
- Current state: 22 -> 23 top-level projects
- _wip/ count: 15 -> 14
- verify_pass1.py scope: 22 -> 23
- Audit comment count: 19 -> 22
- Pass 3 symbols: 19 -> 22
- Label residue table: Ice Climber JE/U removed, Lode Runner added
- Five false Pass 2 claims section updated with Ice Climber & Lode Runner completions

Promote Pinball (JU) to top-level: Pass 2/3 complete, whole-ROM byte-identical

- git mv _wip/Pinball (JU) -> Pinball (JU)

- Added header.s / chr.s (one-line .incbin each)
- Added HEADER/PRG/CHR linker.cfg with proper memory layout
- Added copilot/rom.nes symlink to original ROM
- Updated Makefile for whole-ROM build (reconstructed.nes vs rom.nes)

Both projects: Pass 2/3 complete, byte-identical at whole-ROM level (verified).

Root OVERVIEW.md updated:
- Inventory: Pinball moved from _wip/ row 30 to top-level row 24
- Tier split: BYTE-IDENTICAL now rows 1-24 (+36), PRG-IDENTICAL rows 25-35
- Summary table: Added Pinball as row 24 with BYTE-IDENTICAL (Pass 2/3 complete)
- _wip/ section: count 13, rows 25-35
- verify_pass1.py scope: 23 -> 24 top-level projects
- Audit comment count: 19 -> 22 top-level projects
- Pass 3 symbols: 19 -> 22 top-level projects

All 24 top-level projects verified byte-identical.

TODO.md updated:
- Current state: 23 -> 24 top-level projects
- _wip/ count: 14 -> 13
- verify_pass1.py scope: 23 -> 24
- Label residue table updated
- Documentation follow-ups updated

Promote Millipede (U) and Popeye (JU) (PRG 0) to top-level

Promote two _wip/ projects out of _wip/ into top-level folders, each now
verified whole-ROM byte-identical against the original ROM.

- Millipede (U): moves all copilot files up, adds header.s, chr.s,
  linker.cfg with PRG+HEADER+CHR areas, rom.nes copy, and switches the
  Makefile from PRG-only cmp to whole-ROM cmp. Makefile _tools depth
  adjusted (../../../../_tools/ -> ../../_tools/).
- Popeye (JU) (PRG 0): same promotion treatment.
- AGENTS.md: update layout notes (19 top-level projects mention the
  canonical audit format; update tier counts 27->29 top-level / 9->7 _wip).
- OVERVIEW.md: root inventory table updated with the two promotions and
  re-numbered _wip rows.
- TODO.md: handoff updates.
- Remaining _wip projects: assorted pass 2/3 edits (prg.s, locals.inc,
  reconstructed.lbl, reconstructed_prg.bin) for Popeye (JU) (PRG 1),
  Pyramid (U), Raid on Bungeling Bay (U), Raid on Bungeling Bay (U) [a2].
2026-10-19 19:58:41 -04:00

10 KiB
Raw Blame History

AGENTS.md

What this repo is

Thirty-six AI-generated disassemblies of NES ROMs: 27 promoted to top-level folders and 9 still under _wip/. All 36 carry real disassemblies (Pass 1 or beyond); the difference is the build tier — the 27 at top level verify byte-identical for the whole ROM, the 9 in _wip/ verify at the PRG level only. The contract (stated in PROMPTS.md and verified by make in every copilot/ folder) is byte-identical rebuilds: reconstructed.nes must match the original ROM exactly. Not for distribution (README.md).

Layout

  • Each project owns a folder with a copilot/ build dir (prg.s, linker.cfg, symbols.inc, locals.inc, Makefile; promoted projects add header.s, chr.s and a copy/symlink rom.nes) and a per-project OVERVIEW.md at the folder root (power-on flow analysis). Some older ones use main.s instead of prg.s, and a few lack prg.bin (carve the PRG out of rom.nes instead).
  • Root OVERVIEW.md is the index: vectors, build fixes, and summary for all 36 projects. Keep it in sync when a project changes — its DK Jr. entries drifted badly before being corrected, and its vector column later turned out to have five rows wrong from copy-paste duplication.
  • TODO.md is the handoff: open work per project, plus the gotchas that cost real time. Read it before starting anything non-trivial.
  • PROMPTS.md is the workflow contract. default_project.tar.gz is the empty-project skeleton (prg.s, symbols.inc, linker.cfg, Makefile, locals.inc). _ROMS/ is the master ROM store.
  • _tools/: nesdis.py (Pass 1 tracer), pass2.py and bytecode_dis.py (Pass 2/3 emitters), apply_symbols.py (substitutes symbols.inc/locals.inc names into prg.s operands), trace_stage_payload.py (py65-based runtime tracer), cdl_reclass.py (Mesen-CDL-driven code/data split fix), verify_pass1.py (rebuild + re-trace check across the top-level projects), audit_comment_tool.py (adds canonical audit comments using tracer as ground truth), splitnes.py, chr2gif.py.

Commands

cd "<Project>/copilot" && make   # builds reconstructed.nes, then cmp vs rom.nes
python3 _tools/verify_pass1.py  # rebuilds + re-traces the 19 top-level projects

make's default target includes the byte-identity cmp — a build that doesn't print "Byte-identical ROM verified." is a failure. Requires ca65/ld65 on PATH.

There are two build tiers, and the difference is real. Top-level projects compare the whole ROM against copilot/rom.nes. The 17 _wip/ projects only run cmp reconstructed_prg.bin prg.bin, so their header and CHR are not proven — do not describe them as byte-identical to the ROM.

Promoting a project out of _wip/

  1. git mv "_wip/<name>" "<name>" — the relative copilot/rom.nes -> ../<name>.nes symlink survives untouched.
  2. Add copilot/rom.nes (a space-free copy or symlink of the original) and switch the Makefile from cmp reconstructed_prg.bin prg.bin to cmp reconstructed.nes rom.nes. 2b. A _wip/ project has no header.s/chr.s and its linker.cfg declares only the PRG memory area, so add all three (copy the shape from Burger Time (U)/copilot/: two one-line .incbins and HEADER: start = $0000, size = $0010 ahead of PRG) or the whole-ROM compare has nothing to link. Lunar Pool (U) is the worked example (2026-10-18), and Donkey Kong (JU) (2026-10-19) the second — it also fixed a header-byte mirroring error in its project OVERVIEW.md (Vertical → Horizontal from byte 6 = $00).
  3. Change ../../../_tools/ to ../../_tools/ in the Makefile — the depth changed by one. (No-op if the Makefile never referenced _tools/ — Donkey Kong (JU) had none, and no make regen target either.)
  4. Verify make, then make regen if a regen pipeline exists.
  5. Add it to the root inventory table and re-number the _wip rows.

Irreversible conventions (do not "clean up" past these)

  • Every instruction carries a trailing audit comment with its CPU address, PRG offset and exact emitted bytes. The canonical format is ; CPU:$C123 PRG:0123 AD 21 03 — all 19 top-level projects now use this exact format (standardized 2026-10-19; Donkey Kong (JU) confirmed canonical at its promotion the same day). This is the byte-identity audit trail — preserve it even while renaming labels or restructuring files. The earlier gap (6 variants, 1,013 uncommented lines in Excitebike) was closed 2026-10-19 via _tools/audit_comment_tool.py.
  • Unreached/unresolved bytes stay as data (Label_/Data/ .byte or .incbin blocks with ROM provenance), never deleted or reformatted.
  • Routine headers follow the template in prg.s: global + local description, parameters, returns, clobbers.
  • Two separate symbol files: symbols.inc = NES hardware constants only; locals.inc = zero-page and forward references. Unresolved zero-page bytes keep neutral ZpXX names rather than speculative names. Proven semantic names may sit beside the generated ZpXX entries as aliases.
  • Pass discipline: Pass 1 = code/data split (stop for review), Pass 2 = label replacement, Pass 3 = semantic naming of data tables/structures. Do not skip straight to guessy rewrites. When a pass is only partly achievable, record the residue and why rather than inventing names.
  • make regen (where it exists, e.g. MUSCLE) must keep reproducing prg.s byte-for-byte on the standard library alone. Re-check it after any nesdis.py change.

Hard-won quirks (details in root OVERVIEW.md per-project "Build fix")

  • The flat disassembler mislabels bne as bpl; fixes use bne *-$XX to reproduce the original bytes (Clu Clu Land, Defender 2, Excitebike).
  • Zero pages are reused across modes; aliases may be required in locals.inc (e.g. RandomStateAlt = $E4 beside RandomState0 = $54).
  • Donkey Kong Jr. is architecturally different: reset entry is $C66E, the reset path idles forever in an LFSR loop, and the entire frame state machine runs inside NMI. Its DeferredTaskHandlerTable entry 0 deliberately mirrors the IRQ vector word at $BFFE.
  • IRQ vectors often point at unused/data targets in these games — verify before assuming a handler is broken.
  • Unlicensed titles break static-trace assumptions: Pyramid (U) pokes a mapper unlock at $4100 (beyond the register space) despite a mapper-0 header, and dispatches through jmp ($0002) built from RAM — trace via its two queue tables ($C3A6, $CCD5), and do NOT fall-through-seed gaps that follow rts/jmp/brk (that misclassifies data as code).
  • ROM filenames contain spaces and parentheses (e.g. M.U.S.C.L.E. (U).nes); builds incbin a space-free rom.nes inside copilot/. Keep that copy/symlink working and don't rename it. Note that a name with spaces/parens is also what makes FCEUX hang headless — symlink it to a clean name and run under Xvfb.
  • .gitignore contains ROMS/* but the actual directory is _ROMS — the pattern matches nothing, so ROM binaries stay tracked. Check before committing large blobs.

Tracer and toolchain gotchas (2026-10-17)

Full detail and reasoning in TODO.md. These are the ones that silently corrupt results rather than failing loudly.

  • Undocumented opcodes silently truncate a trace. A tracer that knows only the 151 official opcodes halts on the first one used as ordinary code. Joypad Test Cartridge died at $C09E on DCP $3C20 ($CF) and filed 3,256 bytes of real code as .byte. nesdis.py --undocumented decodes all 105 (default off, so existing make regen pipelines stay byte-stable).
  • ca65 V2.19 implements none of the undocumented opcodes, so they cannot be written as mnemonics. nesdis.py emits them as explicit .byte with the disassembly in the comment — that keeps them in the code region, so both the split and the audit trail survive. ca65 is also happy to accept plain nop, which is official (0xEA).
  • Vectors are not in the iNES header. They are the three little-endian words at the end of the 16 KB PRG ($FFFA–$FFFF in the $C000 mirror). Header bytes 0–9 carry only sizes, mirroring and mapper. Parsing them from header.bin as if they were header fields yields garbage.
  • RESET and IRQ are usually the same address. The semantic-name map must not overwrite on collision, or the primary entry gets renamed to IrqHandler. Use setdefault in RESET/NMI/IRQ priority order.
  • Control-flow targets must win over fall-through. A fall-through decode whose operand spans an address that another path branches to is the wrong decode — no assembler branches mid-instruction. Mario Bros had a bne at $C4BA targeting $C528 swallowed by a NOP #$A5 at $C527.
  • Branch targets inside a surviving instruction must be pruned from the target set, or the emitter's linear walk consumes those bytes as an operand, the label is referenced but never defined, and the file does not assemble at all.
  • A plain vector trace reaches only ~15% of a real game. Almost everything sits behind computed dispatch. Look for jmp ($xx), and for dispatchers that pop the table address with pla — that form cannot be followed from the dispatcher because the pointer is only knowable in the caller's frame (Mario Bros $CD9E). Probing a suspected data region linearly with nesdis.py --probe is cheap and settles whether it is code; --auto-seed plus a couple of --extra-seed addresses took Mario Bros from 14.6% to 72.2%.
  • When emulating to trace, NMI must fire once per frame, not per $2002 read. The main loop polls $2002 to sync on vblank, and each read clears bit 7, re-arming NMI within the same scanline. Guarding on the vblank flag alone gave 40,900 NMIs across 3,000 frames and flattened coverage at 26%.
  • Controller input goes on joy1 = $4016 with a real latch/shift register. The poll routine's lda $4016,X with X=1 lands on $4017, but only bits 0–1 are compared, so driving $4017 consumes nothing and changes no coverage at all.
  • ca65 -l listings contain no source line numbers — they emit <offset>r <n> <bytes> <source text>. You cannot key listing entries back to source lines, and the listing prints rr rr for relocatable words, so byte values must come from prg.bin.
  • py65 is not a build dependency. Unpack on demand (pip download --no-deps py65, then unzip into a lib dir; PY65LIB env var points at it). /tmp is volatile — a reboot wiped it mid-session. Anything reusable belongs in _tools/.