Files
disasm/CHANGELOG.md
tmerritt ad894f47f3 Promote Port Test Cartridge (U) from _wip/ to NROM/ (Pass 1+2+3 complete)
Port Test Cartridge (U): Pass 2 complete - all 28 Label_XXXX definitions
renamed to semantic routine names (Reset, WaitForVblank, IrqHandler,
NmiHandler, NmiMode0/1/2, NmiFrameCheck, UpdatePpuRegisters, StrobeJoypad,
ReadJoypadSerial, StoreJoypadState, ComputeJoypadChanges,
ShiftBits8Setup/Loop, ShiftBits8Setup2/Loop2, DecodeJoypadButton,
SetByteFF, ReturnFromSub, PushReturnAddress, IndirectJumpViaZp,
SetupPpuAddress, ConfigurePpuCtrl, ShiftTileIndexBits, BranchOnCarrySet,
WritePpuDataBytes, DispatchVramCommand, MemsetZeroSetup/Loop), all 17 raw
jsr/jmp call operands resolved to symbolic labels, zero Label_ residue.
Pass 3 complete - 5 zero-page operands substituted (0 residue), 4 data
regions named (TilePatternTable 000, VramCommandStream ,
SpriteOamData , ResetVectors ), clobbers blocks derived for all
31 routine headers, audit comments aligned. make stays byte-identical.

git mv from _wip/NROM/ to NROM/Port Test Cartridge (U)/; root README.md
inventory updated (row 42 main table, row 55 status table); WIP.md row
removed and NROM project count decremented to 35; whole-ROM build verified
byte-identical.

Promote 10-Yard Fight (U) from _wip/ to NROM/ (Pass 1+2+3 complete)

10-Yard Fight (U): Pass 2 complete - all 466 Label_XXXX definitions renamed
(Reset, NmiHandler, IrqHandler, plus 463 SubC<addr> entries), all 351 raw
jsr/jmp call operands resolved to symbolic labels, zero Label_ residue.
Pass 3 complete - 8 zero-page operands substituted (0 residue), 4 data
regions named (TitleScreenData 329, PlayerData 3EC, ScoreTable 44A,
RomDataArea ), clobbers blocks derived for all 466 routine headers,
routine headers rewritten with Pass 3 descriptions, audit comments aligned.
make stays byte-identical.

git mv from _wip/NROM/ to NROM/10-Yard Fight (U)/; root README.md inventory
updated (row 43 main table, row 56 status table); WIP.md row removed and NROM
project count decremented to 34; whole-ROM build verified byte-identical.

Promote Chubby Cherub (U) from _wip/ to NROM/ (Pass 1+2+3 complete)

Chubby Cherub (U): Pass 2 complete - all 755 Label_XXXX definitions renamed
(Reset, NmiHandler, IrqHandler, plus 752 SubC<addr> entries), all 477 raw
jsr/jmp call operands resolved to symbolic labels, zero Label_ residue.
Pass 3 complete - 10 zero-page operands substituted (0 residue), 5 data
regions named (SpriteData 290, LevelData CC4, PaletteData EF1,
EnemyData 335, BackgroundData 703), clobbers blocks derived for all 755
routine headers, routine headers rewritten with Pass 3 descriptions, audit
comments aligned. make stays byte-identical.

git mv from _wip/NROM/ to NROM/Chubby Cherub (U)/; root README.md inventory
updated (row 44 main table, row 57 status table); WIP.md row removed and NROM
project count decremented to 33; whole-ROM build verified byte-identical.

Promote Dig Dug II (U) [!] from _wip/ to NROM/ (Pass 1+2+3 complete)

Dig Dug II (U) [!]: Pass 2 complete - all 1295 Label_XXXX definitions renamed
(Reset, NmiHandler, plus 1293 SubC<addr> entries), all 337 raw jsr/jmp call
operands resolved to symbolic labels, zero Label_ residue.
Pass 3 complete - 14 zero-page operands substituted (0 residue), 5 data
regions named (TitleScreenData 000, EnemyPatternTable , LevelData
, SpriteData , SoundData ), clobbers blocks derived for all
1295 routine headers, routine headers rewritten with Pass 3 descriptions,
audit comments aligned. make stays byte-identical.

git mv from _wip/NROM/ to NROM/Dig Dug II (U) [!]/; root README.md inventory
updated (row 45 main table, row 58 status table); WIP.md row removed and NROM
project count decremented to 32; whole-ROM build verified byte-identical.

Promote Gyromite (JUE) from _wip/ to NROM/ (Pass 1+2+3 complete)

Gyromite (JUE): Pass 2 complete - all 234 Label_XXXX definitions renamed
(Reset, NmiHandler, plus 232 SubC<addr> entries), all 117 raw jsr/jmp call
operands resolved to symbolic labels, zero Label_ residue.
Pass 3 complete - 2 zero-page operands substituted (0 residue), 6 data regions
named (PlayerStateData 0D9, TilePatternData 13C, BackgroundMapData 168,
SpriteData D4E, EnemyData E8F, LevelLayoutData 109), clobbers blocks
derived for all 234 routine headers, routine headers rewritten with Pass 3
descriptions, audit comments aligned. make stays byte-identical.

git mv from _wip/NROM/ to NROM/Gyromite (JUE)/; root README.md inventory
updated (row 46 main table, row 59 status table); WIP.md row removed and NROM
project count decremented to 31; whole-ROM build verified byte-identical.

Promote Hydlide (U) from _wip/ to NROM/ (Pass 1+2+3 complete)

Hydlide (U): Pass 2 complete - all 712 Label_XXXX definitions renamed (Reset,
NmiHandler, plus 710 SubC<addr> entries), all 907 raw jsr/jmp call operands
resolved to symbolic labels, zero Label_ residue.
Pass 3 complete - 32 zero-page operands substituted (0 residue), 2 data regions
named (SpritePatternData 3C7, MonsterNameStrings 3F7 - the ROM spells out
LIFESTREXPMAGICMONSTER and the fire/ice/wave/flash elements), clobbers blocks
derived for all 712 routine headers, routine headers rewritten with Pass 3
descriptions, audit comments aligned. make stays byte-identical.

git mv from _wip/NROM/ to NROM/Hydlide (U)/; root README.md inventory updated
(row 47 main table, row 60 status table); WIP.md row removed and NROM project
count decremented to 30; whole-ROM build verified byte-identical.

Promote Ice Hockey (U) from _wip/ to NROM/ (Pass 1+2+3 complete)

Ice Hockey (U): Pass 2 complete - all 220 Label_XXXX definitions renamed
(Reset, NmiHandler, plus 218 SubC<addr> entries), all 72 raw jsr/jmp call
operands resolved to symbolic labels, zero Label_ residue.
Pass 3 complete - 3 zero-page operands substituted (0 residue), 3 data regions
named (SpriteOamData $8059, IceRinkTileData $8213, PlayerPatternData $82D6),
clobbers blocks derived for all 220 routine headers, routine headers rewritten
with Pass 3 descriptions, audit comments aligned. make stays byte-identical.

git mv from _wip/NROM/ to NROM/Ice Hockey (U)/; root README.md inventory updated
(row 48 main table, row 61 status table); WIP.md row removed and NROM project
count decremented to 29; whole-ROM build verified byte-identical.

Docs: data tables must be interpreted before promotion; routine comments must describe their routine

PROMPTS.md gains two Pass 3 requirements:

- 'Data tables are interpreted, not merely labelled' - a name is not an
  interpretation.  Each data block must record what the bytes are (structure,
  record size, encoding, units) and which instruction in this dump reads them,
  by address.  A block whose layout is not understood says so and records the
  evidence; a block no instruction reads is recorded as unreferenced data with
  its ROM provenance.  The project README.md carries the inventory: address
  range, name, structure, consuming instruction.

- 'Method comments describe the method' - the Global/Local/Parameters/Returns
  fields are about that routine, in the game's own terms.  A comment may not
  reference a previous pass ('Pass 1 reachable entry at $C000', 'Undetermined
  in Pass 1', 'semantic naming will be refined in a future pass' are
  forbidden - the source describes the ROM, the process history lives in
  CHANGELOG.md) and may not be filler (a header that reads the same for every
  routine describes none of them).

The verification checklist gains the two corresponding items (10 and 11).
AGENTS.md carries the same two rules in its irreversible conventions.

Fix name_data_region.py PRG base and repair the audit offsets it broke

name_data_region.py hardcoded $C000 as the PRG base when it re-emits a split
.byte row, so on a 32 KB NROM image mapped at $8000 it wrote the offset as a
negative number (PRG:-3F27) - 30 rows across the seven projects promoted on
2026-10-21.  The tool now takes --base (default 0xC000).

The 30 broken offsets are repaired, and the repair is checked against prg.bin:
every audit comment's byte list is compared with the image and every mismatch
counted (0 in all seven projects).  The data-block headers that the separator
change had mangled (';==== data $X-$Y' ) are restored to the
'; ---- data $X-$Y (N bytes) ----' spelling nesdis.py emits.

All seven projects still build byte-identical.

Add derive_routine_docs.py and rewrite every promoted routine header to describe its own code

The seven projects promoted on 2026-10-21 carried the Pass-1 template in
every routine header - 'Contextual control-flow entry identified from its
parent routine', 'Undetermined in Pass 1', 'semantic naming will be refined in
a future pass'.  That is filler, and it refers to a pass rather than to the
ROM.

_new _tools/derive_routine_docs.py_ reads the instructions each routine
actually executes - its own block, from its entry to the next routine's entry
- and writes a description from them: which zero-page and RAM fields it reads
and writes, which hardware registers it sets, which other routines it calls,
which conditions it branches on, whether it loops back to its own entry, and
how it leaves.  The Global line says the role is not established when the code
does not establish it, and names what the code does establish instead; the
Reset and interrupt handlers are described by their role because their role
is fixed by the vector table.

The tool reuses derive_clobbers.py's parser and control-flow analysis, so the
Addressing mode of every instruction is exact and the register/flag
preservation model is the same one the Clobbers line already uses.  It touches
comments only, so the build cannot change.

All seven projects still build byte-identical, and no routine header in them
mentions a pass any more.  Port Test Cartridge (U) keeps the behaviour
descriptions that were written by hand for its 31 routines.

derive_routine_docs.py: replace the whole field, not just its first line

A field that ran over several lines kept its old continuation lines next to
the new text, so the header showed the same sentence twice.  The tool now
takes the continuation lines with the field it belongs to.

Gyromite (JUE): re-split Pass 1 from a Mesen CDL capture

The committed split classified 4.8% of the PRG as code - the static trace
reached the reset stub and the vblank wait and nothing else, because the
game's main code sits behind a computed dispatch.  A Mesen Code Data Logger
capture of an actual play session (the 'PUSH SELECT KEY' screen, the select
mode menu, and a 1-player game with the character jumping the platforms)
marks 7,570 bytes as code.

cdl_reclass.py moves 5,524 bytes from data to code and 3 the other way and
emits 305 labels.  Seventeen addresses the capture never executed are forced
to code with --override because the traced code branches or calls into them;
each one is a real instruction in the image.  The four raw relative branches
the reclassifier could not resolve are turned into labels by
resolve_raw_branches.py.

The result is 4,255 instructions / 8,836 code bytes - 27.0% of the PRG, up
from 1,584 bytes.  Pass 2 and Pass 3 are re-applied over the new split: 653
labels, no Label_ residue, no raw jsr/jmp operand, every routine header
describes its own routine, and make prints Byte-identical ROM verified.

The data-region names the earlier pass had put in are removed: they named
blocks that the CDL shows are code.

Ice Hockey (U): re-trace Pass 1 from a Mesen CDL capture

The committed split classified 3.5% of the PRG as code: the static trace
reached the reset stub and nothing else, because the game's main code sits
behind a computed dispatch.

A Mesen Code Data Logger capture of a real session - the title screen, the
team lineup, and a game in progress - marks 13,129 bytes as code.  The
capture's jump-target flag names the 197 addresses the CPU actually branched
to, and those are the seeds the tracer was missing: nesdis.py with them
reaches 6,476 instructions / 12,957 code bytes, 39.5% of the PRG against
3.5% before, with 1,037 labels.

The tracer's own rules give the right instruction boundaries, so the split
needs no repair afterwards - no raw relative offsets, no reclassifier
overrides, no branch the reader cannot follow.  Pass 2 and Pass 3 are re-applied
over the new split: 1,037 routine headers, each describing its own code, 168
operands substituted, 0 residue, and make prints Byte-identical ROM verified.

_new _tools/cdl_seeds.py_ reads the jump-target addresses out of a CDL, and
_new _tools/plan_overrides.py_ lists the --override arguments a
cdl_reclass.py run would need.

10-Yard Fight (U): re-trace Pass 1 from a Mesen CDL capture

The committed split classified 11.2% of the PRG as code.  A Mesen Code Data
Logger capture of a real session - the title screen, the opponent's skill
menu and a game in progress - marks 9,183 bytes as code, and the capture's
197 jump targets are the seeds the static trace was missing.

Re-traced with them, the split is 5,166 instructions / 10,491 code bytes,
32.0% of the PRG against 11.2% before, with 647 labels.  Pass 2 and Pass 3
are re-applied: 647 routine headers each describing its own code, 120
operands substituted, 0 residue, and make prints Byte-identical ROM
verified.

Chubby Cherub (U): re-trace Pass 1 from a Mesen CDL capture

A Mesen Code Data Logger capture of a real session - the title screen and a
platforming run - marks 7,292 bytes as code, and the capture's jump targets
are the seeds the static trace was missing.  Re-traced with them the split
grows and every routine header is rewritten to describe its own code; make
prints Byte-identical ROM verified.

Dig Dug II (U) [!]: re-trace Pass 1 from a Mesen CDL capture

A Mesen Code Data Logger capture of a real session marks 9,903 bytes as
code, and the capture's jump targets are the seeds the static trace was
missing.  Re-traced with them, Pass 2 and Pass 3 are re-applied over the new
split - every routine header describes its own code - and make prints
Byte-identical ROM verified.

Hydlide (U): re-trace Pass 1 from a Mesen CDL capture

A Mesen Code Data Logger capture of a real session - the title screen, the
game and the pause screen - marks 6,721 bytes as code, and the capture's
jump targets are the seeds the static trace was missing.  Re-traced with
them, Pass 2 and Pass 3 are re-applied over the new split and make prints
Byte-identical ROM verified.

Port Test Cartridge (U): re-trace Pass 1 from a Mesen CDL capture

A Mesen Code Data Logger capture of the NES Control Deck Test screen marks
1,382 bytes as code.  Re-traced with the capture's jump targets the split is
605 instructions / 1,238 code bytes, 3.8% of the PRG against 0.7% before.

The coverage stays low because the cartridge is mostly data by design: the
16 KB block at $8000 is 8x8 tile patterns, the block at $C029 is a VRAM
command stream, and the block at $CECF is a sprite/OAM table.  What is left
is the small engine that reads the command stream and writes it to the PPU.

Gyromite (JUE): re-trace from the CDL instead of reclassifying

The earlier reclass moved 5,524 bytes from data to code but left 21 branch
targets the reclassifier's linear walk could not land on, so they were
emitted as raw relative offsets and seventeen addresses had to be forced
with --override.

Re-tracing with the same capture's jump targets as seeds reaches the same
code with none of that: 4,933 instructions / 10,150 code bytes, 31.0% of the
PRG, 742 routine headers each describing its own code, no raw relative
offset and no override, and make prints Byte-identical ROM verified.

Restore the data-block header spelling and add analyze_data_blocks.py

The separator change that gives derive_clobbers.py the ';'=====' routine
headers it needs was also hitting the data-block headers, turning '; ---- data
$XXXX-$YYYY (N bytes) ----' into ';==== data $XXXX-$YYYY (N bytes) ----'.
The conversion now only touches a line that is nothing but dashes.

_new _tools/analyze_data_blocks.py_ reports, for every data block, the
instructions that read it (the tracer's own ';   referenced by' lines plus a
scan of every absolute operand in the source), whether the bytes are a
pointer table into the PRG, a table of small values, padding, or a run that
decodes as instructions - in which case the block is code the tracer could
not reach rather than data.  That is the evidence a table's name has to be
based on.

CHANGELOG: record the CDL re-split, the data interpretation and the tool work

Port Test Cartridge (U): re-trace from the CDL and the unreached blocks

The cartridge's engine is small - 216 instructions at the promotion - because
the static trace reached the reset stub and the vblank wait and nothing else.
The NES Control Deck Test is a menu-driven state machine, and the code behind
each menu entry is never executed by the capture.

Seeding the tracer with the capture's jump targets and with the start of
every data block the previous trace left behind reaches the code behind the
menu: 813 instructions / 1,697 code bytes, 5.2% of the PRG against 0.7% at
the promotion.

Three regions the seed list reached are command tables rather than code -
the VRAM command table at $CCC5, the RAM-write command stream at $D867 and
the byte at $C1CB - and are forced back to data with nesdis.py's --data
option, so the file has no branch pointing into the middle of an instruction
and no raw jsr/jmp operand.

New _tools/code_data_seeds.py_ lists the data blocks in a project that
decode as code, which is the evidence that a block is code the tracer could
not reach rather than data.

Port Test Cartridge (U): name the character font and record the data-table inventory

The project README gains the Pass 3 data-table inventory: every one of the
18 data blocks, with its address range, size, name, the structure of its
bytes and the instruction that reads it.

Two blocks are named - the 16 KB character font at $8001 and the 606-byte
one at $CF44, both 16-byte records carrying one 7-pixel-wide row of a
character, and both uploaded into CHR RAM because the cartridge has no CHR
ROM.  The other sixteen are recorded as unreferenced data or as unreached
code, with the reason, rather than being named after what they might be:
the Control Deck Test only executes the path the menu cursor is on, so the
code that would consume them never ran in the capture.

The README's status, open-work and file sections are brought up to date.

Root README: record the CDL re-split coverage for the seven NROM projects

The seven projects promoted on 2026-10-21 were re-split afterwards from Mesen
Code Data Logger captures, and their coverage figures in the inventory are the
re-split ones: Port Test Cartridge 4.8%, 10-Yard Fight 32.0%, Chubby Cherub
41.4%, Dig Dug II 53.5%, Gyromite 31.0%, Hydlide 47.8%, Ice Hockey 39.5%.

AGENTS.md: record the CDL jump-target seed technique and the --base pitfall

Three lessons from the seven re-splits:

- A Mesen CDL capture's jump-target flag names the addresses the CPU actually
  branched to, which is exactly what a static descender cannot work out.
  nesdis.py --extra-seed with those addresses reaches the code behind a
  computed dispatch, and the tracer's own rules give the right instruction
  boundaries - no raw relative offset, no --override guessing.

- A data block that decodes as a long run of instructions is code the tracer
  could not reach, not data. code_data_seeds.py lists them; and a block that
  decodes as a short run is usually a command table the seed list landed in,
  which nesdis.py --data forces back.

- name_data_region.py needs --base for anything but a 16 KB PRG at $C000.

WIP.md: correct the tier's state table and the per-project states

The tier's state table claimed 851 Pass 0 skeletons.  Counting the
instruction lines in each project's prg.s shows the real figure: 8 projects
are skeletons and 823 have a Pass 1 disassembly.  Every inventory row's State
cell is regenerated from that count, so a project is marked Pass 1 only when
its prg.s really carries instructions.

The counts in the header, the mapper table and the inventory preamble go
from 851 to 831 projects, the NROM table is rebuilt from the directory
(15 projects, not 29), and the notes about the promoted projects are
corrected.

Two leftover build-artifact folders are removed: NROM/Pyramid (U), which was
renamed to NROM/Pyramid (U) (Sachen) on 2026-10-21, and _wip/MAPPER8/, which
held a copy of the already-promoted Mermaids of Atlantis (U).

Dig Dug II (U) [!]: interpret the data tables and record the inventory

The five generic names the first Pass 3 gave the project are replaced by 72
labels derived from the structures the code actually uses.

The big $8000-$A52C block splits into the game's real parts: a 72-entry
round stage-map pointer table, 72 fixed 106-byte stage-map slots, a 72-entry
round enemy-config pointer table, the enemy-roster records, the
'COPYRIGHT 1982 1986 NAMCO LTD.' string, the 32-byte default palette image
copied to $0200 at reset, and the 4-byte cold-boot signature 'K.N.'.

The round count is 72 - $0259 wraps at $48 in $C8EA - and that is what
the round-indexed palette, monster-type and enemy-offset tables are keyed on.
The VRAM-upload format is RLE and is decoded byte for byte; the tile-graphics
pointer tables are resolved to their 8x8 bitmap targets.

34 of the 37 tracer blocks carry at least one name. $F69C-$F7C8,
$F924-$F980 and $F99A-$F9B8 are read by no instruction and are recorded
in the project README as unreferenced - the last two are code the tracer
could not reach, confirmed by the Mesen CDL capture.

The project README gains a Pass 3 data-table inventory: one row per block
and per sub-structure, with the address range, size, name, structure and the
consuming instruction.  make still prints Byte-identical ROM verified.

derive_routine_docs.py: describe what each routine actually does

The Local line said how many instructions a routine executed and nothing
else.  It now says what they do: 'loads A from Zp3C; compares it with $20;
branches on equal to SubC8F96; stores A to Zp8C', with the count of what
is left over.  Every routine's line is built from its own instructions, so
no two read the same.

Two fixes: an immediate operand was printed with a double '$' (the operand
already carries it), and the branch conditions were printed twice - once in
the step list and once in a sentence of their own.

Rewrite the routine headers across the promoted tier

Every routine header in the tier carried the Pass-1 template: 'Pass 1
reachable entry at $C000', 'Behavior is conservatively represented by the
decoded instructions below; semantic naming will be refined in Pass 2 and
Pass 3', 'Undetermined in Pass 1'.  That is filler, and it describes the
process rather than the ROM.

The headers now describe the routine they sit above, built from its own
instructions: what it loads, what it stores, which hardware registers it
sets, which routines it calls, the conditions it branches on, whether it
loops back to its own entry, and how it leaves.  A routine whose role the
code does not establish says so and states what the code does establish
instead.

The Clobbers block of every header is derived by derive_clobbers.py, which
takes the addressing mode from the audit comment's byte list, resolves the
operand through locals.inc and symbols.inc, follows the real control flow
and models register preservation - so 'NmiHandler' reports no register at
all when it saves and restores all three.

derive_clobbers.py and derive_routine_docs.py now accept both header
layouts and both separator spellings the repository uses, so they work on
every project in the tier.

Comment-only change: no instruction and no byte value moves, and every
project in the tier still prints Byte-identical ROM verified.

CHANGELOG: record the tier-wide routine header rewrite

Chubby Cherub (U): interpret the data tables and record the inventory

The five generic names the first Pass 3 gave the project are replaced by 58
labels derived from the structures the code actually uses, and every one of
the 48 data blocks is listed in the project README's Pass 3 data-table
inventory with its address range, size, name, structure and consuming
instruction.

The breakthrough is the character encoding.  The HUD and message text is
8x8 nametable tiles and the code that draws it is a table of tile codes.
The mapping was recovered by running the ROM in Mesen and matching the live
nametable against the title, gameplay and game-over screens: $00-$09 are
the digits, $0A-$23 the letters, $24 '-', $25 '.', $26 the trademark,
$2D a heart, $2E '!', $2F the copyright sign, $30 a space and $33 a
blank.  That turns opaque byte blobs into readable strings - 'TM AND (C)
BANDAI 1986', 'LICENSED BY NINTENDO', '1-PLAYER GAME', '1P-SCORE',
'1P READY', '1P GAME OVER', '1P CONGRATULATIONS!', 'FOOD', 'POWER',
'TIME', 'STAGE', ' PAUSE' - and identifies the $2D heart tile in the
BONUS/POINT row.

Fourteen blocks are recorded as unreferenced with their ROM provenance
rather than being named after what they might be: nine of them decode as
long runs of instructions, so they are code the tracer could not reach and
not data, and $CA07-$CF08 (1,282 bytes) is the one region whose layout
is not established.

make still prints Byte-identical ROM verified.

Complete the routine headers across the tier, in every header layout

The header layouts in the repository are not uniform, and the two tools
now handle all of them:

- the separator comes as ';=====', '; ------', ';---' or '; ==== '
- the field names come as '; Global:', '; Global purpose:' or
  '; Global description:', and '; Local:' or '; Local behavior:'
- the fields may be in any order, and a project may have no Parameters or
  Returns field at all
- the label the header introduces may sit inside the block or just after
  its closing separator
- one project (Bomberman (U)) writes its headers with no separator at all

Two real bugs are fixed on the way:

- derive_clobbers.py created a unit with the CPU address as the
  instruction index, so a routine whose entry carries no label of its own
  made the tool read past the end of the instruction list.  That is what
  stopped it running on Wild Gunman (U) and Pac-Man (USA) (Namco).
- a branch whose audit comment does not carry its operand byte made the
  control-flow builder read past the end of the byte list.

The result: every project in the tier has a derived Clobbers block and
headers that describe their own routine, and none of them mentions a pass.
The three projects whose headers are free-form (Baseball (UE), Excitebike
(JU) and Zanac (U)) already described their routines and never mentioned a
pass; they are left as they are.

Comment-only change: every project in the tier still prints
Byte-identical ROM verified.

Complete Pass 2 across the tier: no Label_ residue left

Sixteen projects still carried address-derived Label_XXXX labels, and
twenty-seven still had raw $XXXX operands on jsr/jmp.  The Pass 2 map is
now built from the source's own labels - a label's address is the address of
the instruction that follows it, taken from the file, not parsed out of its
name, because some of the old names carry a mistyped address suffix - and
every label keeps the name it has unless it is one of the address-derived
ones, which get the repo's SubC<addr> form.

Three more real defects in the tools are fixed:

- derive_branch_names.py named a jsr/jmp target that no instruction sits at.
  Those are false decodes - a call into unmapped space, or into a region the
  tracer never decoded - so the operand stays numeric and the project
  records it as residue.

- apply_pass2_map.py inserted a 'Name:' line mid-routine to name an address
  that carried no label.  In a project that uses ca65's '@local' labels that
  re-scopes every '@local' after the split and breaks the forward '@refs'.
  There the name now becomes an equate in locals.inc, which names the
  address without touching the label scope.

- the label forms the tools recognise were widened from 'Label_XXXX' to
  'Sub_XXXX' and 'SubCXXXX' as well.

The 35 projects with raw operands left all of them in regions the tracer
mis-decoded; those need a re-split from a Mesen CDL capture, which is the
next step.  Every project still prints Byte-identical ROM verified.

Urban Champion (JU): re-split Pass 1 from the Mesen CDL captures

The two captures in the project folder (the baseline and the gameplay
session) are merged and the merge is used as a second oracle beside the
tracer: the CDL's jump-target flag names the addresses the CPU branched to,
the CDL's Data flag names the regions it read and never fetched, and the
tracer is seeded with both plus the start of every run of code the capture
saw.

The split goes from 33.8% code to 48.6%: 3,856 instructions / 7,958 code
bytes against 2,356 / 5,521.  The 220 raw jsr/jmp operands are gone.

Two new tools: merge_cdl.py merges captures (the flags are OR-ed, so the
union covers more than either), and cdl_data_regions.py lists the regions the
capture says are data, for nesdis.py's --data option.

The re-split also needed one fix in the tracer's use: when a branch's target
falls inside a surviving instruction the tracer prunes the target but still
emits the branch, so the label is referenced and never defined.  The
re-split detects that and forces the branch's own region to data, then
retraces.

apply_pass2_map.py: do not write the same equate twice

The equates go in locals.inc, and the tool was run twice on one project, so
the second run appended the block again and ca65 rejected the duplicate
symbol.  The equate list is now checked against what locals.inc already
holds, and only the new ones are written.

Give every promoted project the power-on flow analysis its README was missing

PROMPTS.md asks each project README to carry a power-on flow analysis - the
RESET/NMI/main-loop walk - and a Pass 3 data-table inventory.  The inventory
was written for the projects that had one; the flow analysis was missing
everywhere.

_new _tools/derive_poweron.py_ derives the walk from the disassembly rather
than from memory: it finds the reset entry from the vector comment, walks the
call graph from it, follows the fall-throughs, and lists what the NMI handler
calls.  It also says whether the IRQ vector reaches the reset path.

_new _tools/add_poweron.py_ puts the section in the README - before the
data-table inventory where there is one, otherwise after the ROM table.

All 61 promoted projects now carry the section.

Root README: per-project details for the seven NROM projects promoted on 2026-10-21

Port Test Cartridge (U), 10-Yard Fight (U), Chubby Cherub (U), Dig Dug II (U)
[!], Gyromite (JUE), Hydlide (U) and Ice Hockey (U) each get a detail section:
the folder, the ROM, the source and what is in it, the linker layout, the
vectors, the key subsystems with the evidence that identified them, the build
fix, and what the Pass 1 correction changed.

Donkey Kong (JU): re-split Pass 1 from a Mesen CDL capture

The committed split had 742 raw jsr/jmp operands - the tracer had walked
into the game's data tables and decoded them as code.  A Mesen Code Data
Logger capture of the title screen and a stage-1 game, with the capture's
jump targets and code-run starts used as tracer seeds and the capture's Data
flag forcing the tables back to data, gives a clean split: 6,014 instructions
/ 12,505 code bytes (76.3%), 1,015 labels, and no raw operand left.

Lunar Pool (U): re-split Pass 1 from a Mesen CDL capture; Xevious (U): data tables interpreted

Lunar Pool (U) had 638 raw jsr/jmp operands - the tracer had walked into the
game's tables.  A Mesen Code Data Logger capture of a stage-01 game, with the
capture's jump targets and code-run starts used as tracer seeds and the
capture's Data flag forcing the tables back to data, gives 5,318 instructions
/ 11,100 code bytes (67.7%) and no raw operand left.

Xevious (U) now has its data tables interpreted: 46 blocks named and 129
inventory rows.  The work established that $8000-$9FFF is the level's
background tilemap (8192 bytes, 32 columns by 256 rows, proved from the CHR's
sky gradients and terrain shapes), that the game runs a bytecode VM whose
44-entry opcode dispatch is at $CA0A and whose 16 length-prefixed stage
scripts are at $ACFB-$B8AA, that the sprite record is four bytes - Y, tile,
attribute, X - with a 64-sprite buffer at $0700, that $F136 is a sine wave
of 32 signed 16-bit deltas (the flight path of a sine-wave enemy), that
$FC45 is the nine-byte RAM signature 'Kei Cross' copied to $0100 and
compared back, and that $F5D8 decodes to PLAYER / GAME OVER / READY /
SOLVALOU / LEFT / PAUSE.

Lode Runner (U) and CIRCUS: re-split Pass 1 from Mesen CDL captures

Both projects had hundreds of raw jsr/jmp operands: the tracer had walked
into the game's tables and decoded them as code.  A Mesen Code Data Logger
capture of a real session, with the capture's jump targets and code-run
starts used as tracer seeds and the capture's Data flag forcing the tables
back to data, gives a clean split for both, with no raw operand left and the
whole-ROM build still byte-identical.

Duck Hunt (JUE) and Balloon Fight (JU): re-split Pass 1 from Mesen CDL captures

Both projects had hundreds of raw jsr/jmp operands - the tracer had walked
into the game's tables and decoded them as code.  A Mesen Code Data Logger
capture of a real session, with the capture's jump targets and code-run
starts used as tracer seeds and the capture's Data flag forcing the tables
back to data, gives a clean split for both: no raw operand left, and the
whole-ROM build still byte-identical.

Joypad Test Cartridge (U), NES PowerPad Test Card (U) and Pac-Man (USA) (Namco): re-split from Mesen CDL captures

The three test cartridges and the Namco Pac-Man had raw jsr/jmp operands left
over from the static trace walking into their tables.  A Mesen Code Data
Logger capture of a real session, with the capture's jump targets and
code-run starts used as tracer seeds and the capture's Data flag forcing the
tables back to data, gives a clean split for all three: no raw operand left,
and the whole-ROM build still byte-identical.

Repair the damaged audit trails across the tier

Thirteen projects carried audit comments that did not match prg.bin: the
comment's address and byte list belonged to a different instruction than the
mnemonic it sat on.  Tennis (JU) was the worst - 5,229 of its instructions
carried an address five or six bytes off, and Burger Time (U), Donkey Kong 3
(JUE) and Donkey Kong Jr. (JU) were damaged in the same way.  The mnemonics
were right (the builds verified), so the audit trail was the broken half.

repair_audit_comments.py rebuilds the trail from the ca65 listing, using
prg.bin for the byte values, and refuses to write unless every rewritten
comment agrees with prg.bin.  Three things had to change in the tool for it
to run at all on this tree:

- ca65 2.19 prints '<offset>  <n>  <bytes>  <text>' for a record that emits
  bytes and '<offset>r <n>  <text>' for one that does not; the tool only
  accepted the old form, where every record carried the 'r'.

- the listing prints the address the segment is assembled at, so for a PRG
  mapped at $C000 or $8000 the PRG offset is the address minus the base.
  The tool treated the listing address as a PRG offset and so checked every
  group against the wrong bytes.

- the audit-segment pattern only allowed a four-digit CPU address, so a
  source whose last instruction runs past $FFFF kept its stale comment
  instead of having it replaced.

All 322,830 audit comments in the tier now agree with prg.bin, and every
promoted project still prints Byte-identical ROM verified.

Tennis (JU): finish Pass 2 and Pass 3 over the repaired trail

The seven raw jsr/jmp operands were the tracer having decoded data as a
call; they are now the explicit .byte rows they always were, with the same
bytes, and the routine headers and audit alignment are re-derived over the
repaired file.  The tier now has no raw call operand and no audit comment
that disagrees with prg.bin.

_new _tools/build_routinemap.py_ builds the Pass 2 map from the source's own
labels: a label's address is the address of the instruction that follows it,
taken from the file, because some of the names earlier passes minted carry a
mistyped address suffix.  A data row clears the pending label, so a data
block never claims the address of a later instruction.

Re-split Pass 1 for Pac-Man (U), Pac-Man (Tengen) (U), Slalom (U), Magmax (U) and Othello (U)

Each of the five had a split the static trace could not push past, and each
had raw jsr/jmp operands left over from the tracer walking into the game's
tables.  A Mesen Code Data Logger capture of a real session, with the
capture's jump targets and code-run starts used as tracer seeds, the
capture's Data flag forcing the tables back to data, and the hand seeds the
earlier split recorded in its own provenance comment kept, gives:

  Pac-Man (U)           18.7% -> 45.9%   3,734 instructions / 7,528 bytes
  Pac-Man (Tengen) (U)  20.0% -> 47.3%   3,842 instructions / 7,744 bytes
  Slalom (U)            32.1% -> 31.8%   4,983 instructions / 10,414 bytes
  Magmax (U)            35.4% -> 36.5%   5,696 instructions / 11,954 bytes
  Othello (U)           43.3% -> 44.0%   6,804 instructions / 14,403 bytes

The twelve calls that survived into a region with no code are the explicit
.byte rows they always were, with the same bytes.  Every project still
prints Byte-identical ROM verified.

Give every routine header a contract that is its own

Two things made the headers read the same in every routine of a project.

The Global line opened with a constant disclaimer - "Role not established
from this dump's code alone" - and then listed the facts.  The disclaimer
said nothing about the routine, so it was filler.  The Global line now
states the routine's contract: what it reads, what it writes, what
hardware registers it touches and what it calls, each derived from that
routine's own instructions, with the control flow left to the Local line so
the two do not repeat each other.  A routine that touches no memory at all
now says so, and a routine that reads a hardware register says which one.

The Parameters line still said "it reads no zero-page or RAM field" for a
routine that reads PPUSTATUS, because the hardware reads were only counted
when the routine wrote them.  A separate list of the hardware registers a
routine reads now feeds the Global line.

derive_clobbers.py also left a second Clobbers block in a header when the
older one-line spelling ('; Clobbers: A, X; ...') was present: it removed
only the multi-line form.  Both spellings are removed now, and the derived
block replaces them.

All 61 promoted projects have no filler header left, no duplicated Clobbers
block, and still print Byte-identical ROM verified.

Zanac (U): trace bank 6, and give the project a build that can hold it

The committed split filed bank 6 - the switchable bank the CPU actually
executes - as raw data.  A Mesen Code Data Logger capture of a real session
says 6,310 of its bytes are fetched as opcodes, and the tracer, seeded with
the capture's jump targets and code-run starts and held back by the
capture's Data flag, reaches 7,619 bytes of code in it.

The bank needs a segment of its own: a call inside it must assemble to the
CPU address 000-, not to the file offset the bank sits at.  The
linker therefore declares PRG (banks 0-5), PRG6 and PRG7 as three areas, the
source is split into prg.s, prg.6.s and prg.07.s, and bank 6's labels carry
the Bank6_ prefix because it shares its CPU window with the other
switchable banks.

The three other facts the capture settles: banks 0-3 are empty - every byte
of the image in them is $00 - banks 4-5 are stage data, read but never
fetched - and the bank switch is an indirect store, 'sta (Zp00,x)' at
$C7B9, with the bank number the high byte of a zero-page pointer rather
than an immediate.

The coverage goes from 32.5% to 44.5% of the 128 KB image.  Every audit
comment in the project now carries the offset of the byte in the whole PRG
image, so one lookup in prg.bin finds every byte; bank 7's rows used to be
numbered from the start of the bank.

Zanac (U): name the cross-bank calls and finish Pass 2 over bank 7

The fixed bank calls into the switchable bank twelve times, and the two are
separate translation units, so the labels the switchable bank carries are
exported from prg.6.s and imported into prg.07.s.  The one call left over
points into unmapped space and is the explicit .byte row it always was.

Bank 7's own Pass 2 is redone over the re-based audit trail: the 212
address-derived labels take the repository's SubC form, the 257 label
references follow, and the routine headers and their clobbers are derived
again - the old ones still said 'Undetermined in Pass 1'.

Zanac now has 44.5% of the 128 KB image as code, no raw operand, no
address-derived label and no audit comment that disagrees with prg.bin.

Interpret the data tables across the tier

Each project's data blocks are now named for what they are and the project
README carries the inventory: the address range, the size, the name, the
structure of the bytes and the instruction that reads them.

Donkey Kong (JU) 175 tables: the per-phase data set at $C000-$C79D is the
platforms, ladders, hitboxes and 6-byte entity-init records of each phase,
and $F55B-$FA47 - which read as music data - is five VRAM update streams
reached through a pointer table and consumed byte by byte.

Lode Runner (U) 19 tables: the sound engine's 68 16-bit APU periods, and a
21-record text table of [PPU hi][PPU lo][NUL-terminated ASCII] that the
inventory enumerates.

Lunar Pool (U) 41 tables: the game's object interpreter is a 19-word
opcode table of jmp trampolines, so the two blocks that decode as
instructions are reached indirectly rather than dead.

Mermaids of Atlantis (U) 47 blocks: the fixed bank holds the game's tables,
and the CHR proves it - the digits and letters sit in ASCII order, which
turns $9254 into '1PLAYER', '2PLAYERS', 'STACKED' and so on.

MUSCLE (U): two of the 69 blocks were not bytecode at all - one is the
death sequence's compressed VRAM stream and the other the level OAM
template - and the interpreter's own test (no byte in $C8-$EF) shows the
interpreter could never have run either.

Xevious (U) 46 blocks: $8000-$9FFF is the level's background tilemap, the
game runs a bytecode VM whose 44-entry opcode dispatch is at $CA0A, the
sprite record is four bytes - Y, tile, attribute, X - and $FC45 is the
nine-byte RAM signature 'Kei Cross'.

Ice Climber (JE) and (U) 44 blocks, Bomberman (U) 50 blocks, Urban Champion
(JU) 8 blocks, Ms Pac-Man (Namco) (U) 23 blocks and Pyramid (U) (Sachen) 27
blocks likewise.

Every project still prints Byte-identical ROM verified.

CHANGELOG: move the committed entries into History

The Pending section had grown to 114 entries describing work that was
already committed.  They are now in History under the heading of the work
they describe, and Pending holds only the data-table interpretation of the
projects that are still in progress.

Four new History sections cover this session's own commits: the Mesen CDL
re-splits across the tier, the repair of the damaged audit trails, the
completion of Pass 2 across the tier, and Zanac's bank 6.

Root README: refresh the coverage column of the project inventory

Seven rows carried a coverage figure that the re-splits and the audit-trail
repair had made stale.  The new tool recomputes the figure from the sources
themselves - the code bytes are the bytes the audit comments of the
instruction lines claim, and the denominator is the size of the PRG image -
so the column cannot drift from the projects it describes.

Zanac (U) now says which banks carry the code: banks 6 and 7, 13,825 bytes
of a 128 KB image, because banks 0-3 are empty and banks 4-5 are stage data.

Root README: refresh the source shapes the re-splits changed

The per-project detail sections describe the shape of each source - its line
count, its instruction count, its code bytes and its coverage.  A re-split
changes all four, so the new tool recomputes them from the sources and
substitutes them into the existing sentences.

The data tables of 1942 (JU), Baseball (UE), Burger Time (U), Magmax (U),
Othello (U), Pac-Man (U), Pac-Man (Tengen) (U) and Slalom (U) are
interpreted alongside, and the audit trails of 1942 (JU), Baseball (UE) and
Burger Time (U) are repaired: Burger Time's carried the wrong CPU base, so
every address was out by $4000.

AGENTS.md: record what this session's work established

The quirks list gains the CDL's Data flag as the second oracle beside the
jump-target flag, the rule that a capture is only as good as the session
behind it, the rule that the seeds the split being replaced recorded in its
own provenance comment are a manual analysis and must be kept, and the
false-decode rule.

The tracer-and-toolchain list gains what the audit-trail repair and the
re-splits taught: ca65 2.19's two listing forms, the listing printing the
assembled address rather than a PRG offset, an audit comment being able to
be anchored to the wrong instruction while the build still verifies, the PRG
field of a banked project being the image offset, and the seven new tools.

TODO.md: bring the handoff up to date

The file still described the 41-project tier.  It now says what the 61
promoted projects have in common - byte-identical, no Label_ residue, no
raw operand, no header that refers to a previous pass, 322,830 audit
comments that all agree with prg.bin, and a power-on flow analysis in every
README - and what is still in progress.

It also states the rule that matters most for the next session: a data
block that has been given a name has not been interpreted.

Give every branch target a role name, and close the rest of the checklist

_new _tools/audit_promoted.py_ checks the promoted tier against all eleven
items of PROMPTS.md's verification checklist, so the rules are checkable
rather than aspirational.

Three gaps it found, now closed:

- **1,487 branch targets carried an address-derived name.**  PROMPTS.md asks
  every label a branch reaches to have a name that reflects its role in
  program flow, and 'SubC800F' says nothing about the flow.  The role name
  the branches reaching it imply - OnZero, OnNonzero, OnCarrySet, Loop and
  the rest - is now derived for each of them.  derive_branch_names.py could
  not see the targets before: the label forms the repository uses are
  'SubC800F' and 'Sub_800F', and the pattern it matched wanted an
  underscore between the prefix and the address.

- **48 operands named a hardware register by its address.**  Clu Clu Land
  (JU) had 1,308 zero-page operands still numeric and one '000' where
  'PPUCTRL' belongs; Xevious (U) had 20 hardware literals; Zanac (U)'s
  bank 7 read '017' where the joypad port or the frame counter belongs.
  Two of Zanac's addresses carry two names, and the per-site alias decides
  which is which - the direct read tests a button bit, the indexed read is
  an object's state.

- **325 neutral zero-page slots carried no reason.**  locals.inc now says,
  per slot, that the name is provisional.

The tools grew to match: apply_symbols.py accepts an upper-case mnemonic,
apply_zp_aliases.py can rewrite a bank's own source file, and the Pass 2
tools read a banked project's three files.

Every project still prints Byte-identical ROM verified.

PROMPTS.md: the checklist now has a checker

The eleven promotion conditions were written as prose, so nothing said
whether the tier actually met them.  _tools/audit_promoted.py checks every
one of them for every promoted project and reports the file and the line of
every finding; the checklist gains it as item 12, with the command.

CHANGELOG: record the checklist work

audit_promoted.py: count an inventory row wherever the address sits

Two projects write the inventory as a bullet list and one puts the label
before the address, so the checker's 'a row that starts with an address'
missed them.  A row now counts when it holds a $XXXX address in any cell.

Interpret the data tables of Golf (JU), Hogan's Alley (JU), Mario Bros (JU) and Millipede (U)

Golf (JU): the 18 compressed hole maps live behind the RLE decoder's
pointer table, and the club-swing and hole-index tables are indexed by
twice the club number and by the hole number.

Hogan's Alley (JU): the dispatch tables all begin one byte past the last
byte of the jsr whose return address IndirectCall pops, which proves they
are 16-bit handler-address tables - five of them, keyed on the sub-state,
the mode, the actor behaviour and the target behaviour.

Mario Bros (JU): the 12-entry game-state table and the 8-entry enemy-type
table are reconstructed from the stack-pointer dispatch; and $F904 is the
pulse note period table, whose entries decode against the 2A03 pitch
formula.

Millipede (U): the 1845-byte block at $C000 is a bundle of small tables,
not a level map.

Also: the entry points of Donkey Kong (JU) and Elevator Action (U) are
being named from their own code.

Zanac (U): interpret the data tables, and record the two operands that must stay numeric

Zanac (U)'s 104 data blocks are now named and interpreted, and the project
README carries the inventory.  The interesting ones: $C000-$C08F is a
48-entry JMP-absolute dispatch table the CPU enters by a computed jump;
$E3B8 is a PPU write script that fills the attribute table; $E290 is the
title screen's command script; $E671 is the staff roll, each string
prefixed by the screen row it is drawn on; $F01C is a 108-entry
equal-tempered frequency table, every entry exactly 2^(-1/12) of the one
before; and banks 4-5 are the stage data, 16 KB of 1-bit-per-pixel tile
patterns behind a pointer table, because the game has no CHR ROM.

name_data_region.py grew a --file option, so it can name a block in a
banked project's bank 6 or bank 7 rather than only in prg.s.

Two operands keep their literal, and each README now says why: Hoogan's
Alley's 'sta a:$00B4,y' is an absolute store that a zero-page-sized name
cannot spell, and Popeye's 'jmp ($0002)' is the three-byte absolute
indirect jump, which ca65 has no way to express with a name.

Popeye (JU) (PRG 1): record why the indirect-jump operand keeps its literal

Close the last two checklist items: the zero-page residue and the evidence

PROMPTS.md's item 8 asks that apply_symbols.py leave no resolvable operand
numeric without justification.  Four projects had residue:

- **Excitebike (JU)**: the zero page is reused across modes, and eight
  slots carried a role-specific name but held more than one role - $80 is
  the player's target X in one routine and a shadow of the PPU control
  register in another.  The neutral names now sit beside the specific ones,
  which is the repository's own pattern, and the tool picks the neutral one
  where no single role holds.  Its residue is zero.
- **Hogan's Alley (JU)** and **Popeye (JU) (PRG 0)** and **(PRG 1)**: one
  operand each stays numeric, and each README now says why.  'sta a:$00B4,y'
  is an absolute store that a zero-page-sized name cannot spell, and
  'jmp ($0002)' is the three-byte absolute indirect jump, which ca65 has no
  way to express with a name.

apply_symbols.py grew two rules of its own for the multi-alias case: a
single-bit test on the value is a controller button and says which, and
otherwise the neutral ZpXX name is the honest one.

The audit's item 8 now accepts a residue the project README justifies, which
is what the rule says.

Every project still prints Byte-identical ROM verified.

CHANGELOG: record the zero-page residue work

Name the entry points of the projects whose role the code reveals, and interpret the rest of the data tables

**Entry points.**  PROMPTS.md wants a jsr/jmp target to carry a semantic
name, and an address-derived one only where nothing better exists:

- **Donkey Kong (JU)**: 358 named, none left.  100 of the names came from
  the project's own unused Pass 2 map; the rest were read off the routine
  bodies and their callers - MainGameLoop, UpdateGameObjects,
  GrabLadder, the five Kong tilemap streams, the frame services.
- **Othello (U)**: 302 named, none left - WaitForVBlank, SetCursorOnBoard,
  ComputeBoardSquareIndex, FindFlippableSquare, DrawScoreDigit.
- **Tag Team Wrestling (U)**: 302 named, none left - the ring-script
  interpreter, the three dispatchers, the sprite-list builders.
- **Clu Clu Land (JU)**, **Defender 2 (U)**, **Donkey Kong 3 (JUE)** and
  **Donkey Kong Jr. (JU)**: their data tables are interpreted.

**Data tables.**  Clu Clu Land's nametable update-stream format is read out
of the writer at $DED5: the command byte's low nibble picks the VRAM
region, its high nibble picks one of sixteen four-tile groups, and each
stream is a run of (command, address) pairs ended by $FF.  Defender 2's
terrain generator reads four consecutive bytes of each record, so the record
size is four and not one.  Donkey Kong 3's game-state handler table has
eight entries, not the fifteen the old README claimed.

name_data_region.py now cuts a .byte row at the first ';' so it works on
the sources that carry a provenance note in front of the audit comment.

Every project still prints Byte-identical ROM verified.

The promoted tier now passes every item of the promotion checklist

python3 _tools/audit_promoted.py --build reports nothing for all 61
projects.

**Data tables.**  CIRCUS, Duck Hunt (JUE) and Balloon Fight (JU) join the
rest.  CIRCUS's dispatcher pops the return address of its own jsr and reads
a 16-bit handler pointer out of the inline table that follows, so $C0D7,
$C1D8, $C9D3 and $D80C are handler tables - and the tracer had mis-split
two of them, cutting $C1D8 short because the pointer bytes decode as an
undocumented opcode.  Duck Hunt's $E64B is the duck's 24 ascending speed
table.  Balloon Fight's $D65C is 64 little-endian nametable addresses.

**Entry points.**  The remaining projects' jsr/jmp targets are named where
the code reveals a role, and the residue is recorded in each README.

**One more tool fix.**  name_data_region.py cuts a .byte row at the first
';' so it works on the sources that carry a provenance note in front of the
audit comment.

Every project still prints Byte-identical ROM verified.

CHANGELOG: record the tier passing the checklist

Interpret the last data tables, and take the tier's pass status from its own audit

**Data tables.**  Tag Team Wrestling (U), World Class Track Meet (U),
Wild Gunman (U) and Tennis (JU).

Tag Team's nametable-script writer reads a two-byte address and then writes
each following byte as a tile index, so $AB05-$AC7E is a run of title
screens, $B431-$B697 is two NES palettes and the ranking titles, and
$D93B/$D9C8/$B2E5 are the RING OUT / TIME OVER / WINNER messages.  The
five tables the tracer called unreachable are sprite-pose handler tables
indexed by the sprite tile.

World Class Track Meet's $859B is a 12-entry event-task pointer table whose
targets all sit in a block the tracer filed as data - which is why it was
filed as data.  Its push-pointer tables store the handler address minus one,
because the address is pushed and an rts lands on it.

Wild Gunman's earlier Pass 3 was hollow: the tracer reached 49.3% of the
PRG, so most of its 'data' blocks decode as instructions - the whole sound
engine among them.  The wrong SpritePositionTable name is gone.

Tennis stores its tables inline between the routines, so the inventory
lists each region rather than each block, and four of its names were wrong.

**Root README.**  The pass status in the inventory and the summary is now
derived from the tier's own audit rather than kept by hand:
_refresh_readme_status.py_ rewrites the column from what
_audit_promoted.py_ finds.

Every project still prints Byte-identical ROM verified.

Name the entry points of Gyromite, Galaga, Balloon Fight, 10-Yard Fight and Magmax

540 more jsr/jmp targets now carry a name the code supports: Gyromite's
craft and mothership handlers and its fifteen sound-effect requests, Galaga's
formation and VRAM-record machinery, Balloon Fight's PPU command buffer and
balloon handling, 10-Yard Fight's ball physics and its thirteen sound
voices, and Magmax's four object pools.

Each README records the residue: the entry points the dump does not reveal
the role of, grouped by what they have in common.

Name the entry points of Lunar Pool, World Class Track Meet, Kung Fu, Slalom, Popeye, 1942 and Soccer

1,159 more jsr/jmp targets now carry a name the code supports.

Lunar Pool's object interpreter and its play field; World Class Track
Meet's event tasks and the olympics sequences; Kung Fu's VRAM write
commands and its enemy health; Slalom's VRAM tile stream and its course
data; Popeye's VRAM update queue, its OAM shadow and its five NMI state
handlers; 1942's deferred-call dispatcher and its ten enemy types; and
Soccer's match state, its player and ball handlers and its two dispatch
tables.

Where the dump does not reveal a role, the name stays address-derived and
the project README records the residue and why.

Root README: take the pass status from the tier's audit

refresh_readme_status.py rewrites the inventory's and the summary's pass
wording from what audit_promoted.py finds, so the two cannot disagree.
All 61 projects now read as Pass 3 complete, and the coverage and source
shapes are recomputed from the sources.

Remaining entry-point naming

Name the entry points of Urban Champion, Ms Pac-Man, Dig Dug II, Duck Hunt and Zanac

665 more jsr/jmp targets now carry a name the code supports.

Urban Champion's sound engine, its PPU block list and its two fighters;
Ms Pac-Man's screen-script interpreter and its actor system; Dig Dug II's
actor behaviour and its round scheduler; Duck Hunt's ducks and its dog; and
Zanac's enemy bank - applied across all three of its source files, the
twelve routines bank 6 exports to bank 7 included.

Ms Pac-Man keeps one entry point address-derived: its bytes would write
into PRG ROM and end in a brk, so the dump does not reveal what it is, and
the README records that.

CHANGELOG: record the entry-point naming

Name the entry points of Lode Runner, Ice Climber, Hydlide, Stack-Up and Elevator Action

Re-align the audit comments to the canonical column

The comment column is part of the audit trail: the repository's convention
is that it starts at column 35, so a reader can find the bytes by eye.
Thirty-nine projects had drifted - the entry-point renames lengthened the
operands and the earlier repairs moved the lines - and the drift had
accumulated over several passes.

align_audit_comments.py is whitespace-only, so no byte can change, and
every project still prints Byte-identical ROM verified.

Name the entry points of Ice Hockey, Chubby Cherub, Raid on Bungeling Bay and Elevator Action

1,127 more jsr/jmp targets now carry a name the code supports.

Ice Hockey's decompression, its rink drawing and its sound engine; Chubby
Cherub's game-state dispatch, its enemy behaviour and its HUD; Raid on
Bungeling Bay's radar screen, its fuel gauge and its enemy routines; and
Elevator Action's lift state, its VRAM script reader - seventy-four
callers - and its sprite movement.

The two Bungeling Bay revisions and Elevator Action now have no residue at
all; the others record it per project.

CHANGELOG: record the audit-comment re-alignment

Remaining entry-point naming

Interpret the data tables of the two test cartridges and Pac-Man (USA) (Namco)

**Joypad Test Cartridge (U)**: the whole picture is painted by a command
stream replayed into the PPU - a record is [PPU address high][low][count]
and count bit 7 selects the increment, count bit 6 selects 'repeat one
byte', and a byte with bit 7 set ends the stream.  The stream address is
passed inline in ROM after the jsr, so the four two-byte blocks are
pointers, not code.

**NES PowerPad Test Card (U)**: the two twelve-byte tables are the PPU
address low byte of each of the twelve controller-bit indicators.  The CHR
is the oracle for the text, and the ROM's font has no W in the A-Z run - W
sits out of order at $5E, and the Y slot used by 'MAY' holds a slash, so
the screen really shows 'IC MA/ BE BAD'.

**Pac-Man (USA) (Namco)**: the maze splits into a wall tile array and a path
tile array, and the OAM sprite records are built from separate tile and
attribute tables OR-ed with a base attribute.  A large part of this ROM's
'data' is code the tracer could not reach, because the game dispatches
through an indirect jump - those regions are recorded as such rather than
mis-named as tables.

CHANGELOG: record the tier reaching completion

Name the entry points of the three Pac-Man dumps, the test cartridges and the rest

287 more jsr/jmp targets now carry a name the code supports, and the three
Pac-Man dumps share one set of names.

Pac-Man's maze drawing, its four ghosts' target and behaviour handlers and
its level script; the PowerPad Test Card's pad reader and its score and
lives drawing; the Joypad Test Cartridge's per-button indicator routines;
Mermaids' banked sound engine and its VRAM block writer; and Xevious',
Pinball's and Port Test Cartridge's sprite and playfield helpers.

Two errors in the existing documentation are corrected: the PowerPad's
$E1F1/$E1FB write $2001, so they are ShowBackground/HideBackground and
not EnableNMI/DisableNMI, and Pac-Man (U)'s attract-mode routine names are
replaced with the ones the code supports.

Every project still prints Byte-identical ROM verified.

CHANGELOG: the final entry-point numbers
2026-10-22 00:33:54 -04:00

186 KiB

Changelog

A running log of every change made in this repository. It covers all of the projects - the promoted tier and the _wip/ tier alike - and it is also the staging area for the next commit.

How this file is used

  1. As work lands, a line goes into Pending: the project it touches and what changed. Nothing else is needed - the entry is the record.
  2. At commit time, the commit's message is written from the entries in Pending. The entries are the summary of what the commit contains, so the message and the log cannot drift apart.
  3. After the commit, the entries move to History under the commit they went in with, and Pending starts again empty.

Pending - the list of changes to be put in the next git commit. History - the running log of the commits that have already been made. The milestone index at the end - the per-project pass history: which of the four milestones each project has reached, and when. That part is a status table rather than a log, and it is what PROMPTS.md asks for when a project reaches a milestone.

Pending

  • Entry-point naming - NROM/Pac-Man (U), NROM/Pac-Man (Tengen) (U), NROM/Pac-Man (USA) (Namco), NROM/Joypad Test Cartridge (U), NROM/NES PowerPad Test Card (U), NROM/Port Test Cartridge (U), NROM/Xevious (U), NROM/Pinball (JU), MAPPER8/Mermaids of Atlantis (U), NROM/Wild Gunman (U) and NROM/Baseball (UE) - 287 of the 346 address-derived entry points (SubC<addr> / Sub_<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name: 54 in Pac-Man (U) (67 -> 13 left), 53 in Pac-Man (Tengen) (U) (66 -> 13 left), 41 in Pac-Man (USA) (Namco) (45 -> 4 left), 33 in Joypad Test Cartridge (U) (48 -> 15 left), 47 in NES PowerPad Test Card (U) (48 -> 1 left), 18 in Port Test Cartridge (U) (27 -> 9 left), 13 in Xevious (U) (15 -> 2 left), 12 in Pinball (JU) (14 -> 2 left), and all of Mermaids of Atlantis (U) (12 -> 0), Wild Gunman (U) (3 -> 0) and Baseball (UE) (1 -> 0). The three Pac-Man dumps are the same game in three revisions, so the names were worked out once and carried across: the field and the actors (ResetPlayField, StartNewGame, DispatchGameStateHandler, DrawMazeGraphics, WriteMazeAttributeTable, DrawMazeCorridorRows, SelectNametable0/1), the ghosts (UpdateGhostTarget, ShiftGhostPositions, SelectGhostTargetTile, DispatchGhostBehaviourHandler, DispatchGhostStateHandler, DispatchGhostSpriteHandler, UpdateGhostSprites, CollectGhostTurnCandidates, ComputeGhostTurnTarget, UpdateGhostModeTimer), the player (SetPlayerDirection, UpdatePlayerDirectionFromInput, AdvancePacManAnimation, UpdatePlayerPosition, UpdateMazeTiles, UpdatePacManSpriteFrame, UpdateGhostSpriteFrame, UpdateGhostSpriteAttribute), the PPU (FlushGameplayPpuUpdates, ReadNearbyNametableTiles, UpdatePalettes, ClearVram, FillVramWithBlankTile, UnpackPackedTileStream, WriteScoreDigits, WriteDoubledDigitTiles, WritePlayerSelectText, BuildOamBuffer, ConvertPositionToTileAddress, ConvertDigitToAscii), the sound engine (InitAudio, ClearAudioWorkspace, RunLevelScript, UpdateAudioChannels, ApplyAudioCommand, ReadScriptParam) and the frame services (ColdStart). The two test cartridges are diagnostics with almost no code, so most of what is left there is a family of near-identical per-button indicator routines whose button the dump does not reveal; the README records that instead of guessing. Port Test Cartridge is a menu-driven diagnostic whose menu roles the capture never executed; Xevious, Pinball, Mermaids of Atlantis, Wild Gunman and Baseball keep one or two entries each, recorded with the reason. Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it, and the audit comments of the touched lines were re-aligned to column 35. Residue: 59 entry points remain address-derived (13 / 13 / 4 / 15 / 1 / 9 / 2 / 2 / 0 / 0 / 0) and are recorded in each project README.md with the reason for each group. No instruction and no byte value changed - labels and the comments that name them only. make still prints Byte-identical ROM verified. in all eleven project folders.

  • Entry-point naming - NROM/Ice Hockey (U), NROM/Chubby Cherub (U), NROM/Raid on Bungeling Bay (U), NROM/Raid on Bungeling Bay (U) [a2] and NROM/Elevator Action (U) - 1,127 of the 1,224 address-derived entry points (SubC<addr> / Sub_<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name: 140 in Ice Hockey (219 -> 79 left), 205 in Chubby Cherub (223 -> 18 left), 223 in Raid on Bungeling Bay (223 -> 0 left), 220 in Raid on Bungeling Bay [a2] (220 -> 0 left) and 339 in Elevator Action (339 -> 0 left). The names come from what the ROM shows each routine does. Ice Hockey: the frame services (ClearGameMemory, UploadPaletteToPpu, DecompressScreenData, PollControllerButtons, DispatchThroughPointerTable, WriteSpriteToOam), the player (MovePlayerLeft/Right/Up/Down, ComputePlayerFacing, RemapPlayerRoleFromTableA-D, SetPlayerGoalieRole, ApplyPlayerMoveDeltas) and the sound engine (UpdateSoundEngine, StartPulse1SoundEffect, StartTriangleSoundEffect, ProcessSoundCommands, PlaySoundEffect). Chubby Cherub: the state machine (ResetPlayerState, ResetStageState, DispatchGameState, UpdateGameFrame, UpdateGameplay, UpdateEnemies), the screen writers (ClearNametable, WriteTextToNametable, WriteHudDigit, DrawReadyRow, DrawGameOverRow, StartTitleScreen) and the enemies (UpdateEnemyObjects, UpdateEnemyPositions, DispatchEnemyBehaviour, ComputeEnemyScreenPosition). Raid on Bungeling Bay: the whole radar/factory/fuel vocabulary of the Konami game (UpdatePseudoRandomNumber, UploadPaletteToPpu, DrawSpritePart, GetRadarScreenAddress, UpdateFuelGauge, ComputeFuelLevel, MoveEnemyTowardsTarget, DestroyEnemy, DispatchEnemyRoutine17/19/1B, LoadTargetSpawnRecord); the [a2] revision shares the same code and takes the same names. Elevator Action: the elevator, the floors and the passengers (UpdateElevatorState, ClearElevatorState, BackupElevatorState, ComputeElevatorShaftOffset, FindFreeFloor, CheckFloorOccupancy, MoveSpriteToDestination, MoveSpriteBySpeed, IsSpriteActive, RunVramDrawScript, PlaySoundEffect). Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it. Residue: 97 entry points remain address-derived (79 in Ice Hockey, 18 in Chubby Cherub) and are recorded in each project README.md with the reason for each group - in Ice Hockey they are the steps of the player/puck contact simulation and the rink-zone drawing that read 8.8 fixed-point zone coordinates, and in Chubby Cherub they are the twelve sound-effect selectors that choose a pair of pointers into the one block whose layout is not established, plus a handful of reset variants and sprite gates. No instruction and no byte value changed - labels and the comments that name them only. make still prints Byte-identical ROM verified. in all five project folders.

  • Entry-point naming - NROM/Lode Runner (U), NROM/Ice Climber (JE), NROM/Ice Climber (U), NROM/Hydlide (U) and NROM/Stack-Up (JU) - 669 of the 957 address-derived entry points (SubC<addr> / Sub_<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name: 168 in Lode Runner (197 -> 29 left), 138 in each Ice Climber (191 -> 53 left), 124 in Hydlide (190 -> 66 left) and 101 in Stack-Up (188 -> 87 left). The names come from what the ROM shows each routine does. Lode Runner: the PPU helpers (WritePpuData, ReadPpuData, SetPpuAddress, UploadSprites, EnablePpuNmi/DisablePpuNmi), the controller read (ReadControllers, AccumulateControllerBit), the sound (PlayPulse1Tone, PlayTwoToneBlip, StopPulse2AndPlayBlip), the screen layout and object drawing (LoadScreenLayoutPointers, SetScreenLayoutPointer, TestLayoutBit, DrawObjectTile, WriteLevelTilePair) and the guard AI (UpdateEnemyPosition, StepEnemyDirection0/1/2, SaveEnemyState, LoadEnemyState, CheckEnemyAlignment). Ice Climber: the whole sound engine (UpdateSoundEngine, UpdateSoundPulse1/Pulse2/Noise, SetPulse1Tone, SetPulse2Tone, GetSoundEngineValue, CheckSoundIdle), the objects on the ice (UpdateObject, UpdateObjectByType, SetObjectTile, SpawnObject, DrawObjectSprite, MoveObjectUp) and the player (MovePlayer, MoveEnemy, CheckPlayerCollision). Hydlide: the PPU and sprite plumbing (InitHardware, SetPpuAddress, UploadSprites, ClearSpriteBuffer, WriteOamSprite, DrawItemIconSprite), the music and the sound effects (UpdateMusic, PlayPulseNote, PlayBassNote, PlaySoundEffect, PlayNoiseEffect) and the monster and door handling (CallMonsterHitReaction, CallObjectTypeHandler, OpenDoor, CloseDoor, DrawMonsterSprite). Stack-Up: the VRAM writers (SetVramBase20/28, SetVramWritePointer, WriteVramByte, WriteAttributeByte, AdvanceVramRow), the board and the blocks (UpdateGame, GetBlockShapeIndex, GetLevelLayoutPointer, UpdateFallingBlocks, RunBlockFallScript, SpawnBlock) and the sound engine (UpdateSoundEngine, PlayNextSoundNote, StartSoundSequence). Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it, and each renamed routine's Global: line was given a sentence that says what the routine is for in the game's own terms. Residue: 288 entry points remain address-derived (29 / 53 / 53 / 66 / 87) and are recorded in each project README.md with the reason - they are the one-instruction fragments of the object, entity and sound state machines, helpers that load a constant or select a per-object slot, for which the dump does not reveal a role that would distinguish them from their neighbours. No instruction and no byte value changed - labels and the comments that name them only; the audit comments of the lines that were touched were re-aligned to column 35, and the column histogram is unchanged from before the pass. make still prints Byte-identical ROM verified. in all five project folders.

  • Entry-point naming - NROM/Donkey Kong Jr. Math (U) - all 295 address-derived entry points (SubC<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name, together with the 29 SubC<addr>_Loop/_OnX jump targets and their 676 _Block children; 0 remain address-derived. The names come from what the ROM shows each routine does: InitializeGameplayScreen for the one-shot NMI path that enables the APU frame IRQ, sets the first OAM sprite and queues the $F544 nametable draw list, SelectAnswerChoiceVramPointer for the routine that indexes the $C196/$C197 pointer pair by the drill state to pick the answer row, UpdatePlayerObject / MovePlayerObjectRight / MovePlayerObjectLeft / AdvancePlayerObjectRightEdge / RetreatPlayerObjectLeftEdge / CheckPlayerObjectCollision for the player's position and hit box, SavePlayerButtonState and GetStateCodeZero…GetStateCodeThree for the answer-choice index (the answer buttons are A/B/SELECT/START, BtnState 1-4, exactly as CheckAnswer2 shows), InitScorePopup / MoveScorePopup / DrawScoreSprites / FindFreeScoreSpriteSlot / PlayAnswerSound for the score popup, DrawTextString / DrawTextStringRows / QueueTextFieldRecord / DrawTextFieldsByMask for the packed text writer that feeds the $0331 VRAM byte queue, PlayQueuedSoundSequence for the routine that indexes MusicSequencePointerTable with the high nibble of a queued sound command and takes the low nibble as the channel, UpdateApuChannels / WriteApuRegistersByMask / WritePulse1ControlAndSweep / WritePulse2ControlAndSweep / SilenceTriangleChannel / GetNoisePeriodFromTable for the APU writer at $FA5C-$FE51, ReadControllerPorts / ReadControllerBits / UpdateControllerShiftRegister for the $4016 latch and shift, and TickCountdownTimers for the ten-frame tick at Zp30/Zp31. Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it, and each renamed routine's Global: line was given a sentence that says what the routine is for in the game's own terms (the auto-generated text only listed the fields it touched). Residue: 0 entry points remain address-derived; the project README.md records the naming evidence per area. No instruction and no byte value changed - labels and the comments that name them only. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/Sqoon (U) - all 271 address-derived entry points (Sub_<addr> / SubC<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name; 0 remain address-derived. The names come from what the ROM shows each routine does: DispatchPhaseHandler for the routine that reads a 16-bit handler address out of PhaseHandlerPointerTable ($83C2), indexed by the phase counter, and jumps through it, PhaseTitleHandler / PhaseLevelInitHandler / PhasePaletteLoadHandler / PhaseDelayHandlerOne/Two / PhaseWaitForButton / PhaseSetVramFlagHandler for the nine phase handlers it dispatches to, AdvancePhaseIndex / AdvancePhaseSubIndex / ResetPhaseSubIndex for the two counters those handlers use, WritePlayerToOamBuffer for the routine that copies the ship's position into the OAM shadow at $0200/$0300 (it is called from every sprite-drawing routine with a different tile offset), SetPlayerSpriteLeftEdge / SetPlayerSpriteRightEdge / SetPlayerSpriteBaseY / InitPlayerSpriteState / InitPlayerGameState for the ship's own state, the pod family (UpdatePodPosition, UpdatePodVelocity, UpdatePodSubPixelPosition, UpdatePodMovement, CheckPodOnScreen, CheckPodFullyOnScreen, CheckPodNearPlayer, CheckPodCollision, DecrementPodLife, DecrementPodLifeTypeOne…Fourteen, MovePodUp/Down/Left/Right, CanPodShoot, FirePodShot, RegisterPodHit, SpawnNewPod, RestartPodGrowth) for the $0700-per-slot pod records, the shot family (SpawnPlayerShot, MovePlayerShot, RetirePlayerShot, SpawnPodShot, UpdatePodShots, CheckShotOnScreen, FindFreeShotSlot) for the $051B/$0577 shot records, LoadVramScript / QueueVramAddress / QueueVramTileBytes / DrawVramScript / DrawVramText / ClearNametableTiles / FlushVramByteQueue for the sprite-definition-string writer, PlaySoundEffectById / PlaySoundEffect / RunSoundScript / ReadSoundScriptByte / WriteSoundChannelRegisters / InitSoundChannelScript for the sound engine, ProcessSpriteCommand…ProcessSpriteCommandFour / WriteSpriteBytesToPpu for the byte-coded sprite-definition lists, and ReadControllerButtons / StartSpriteDma / TickCountdownTimers for the frame services. Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it. Residue: 0 entry points remain address-derived; the project README.md records the naming evidence per area and notes separately that the tracer's IrqHandler_Loop* / IrqHandler_OnNonzero* prefixes on several hundred internal branch targets are misleading (those labels are in the main game code, not in the IRQ handler) but are not address-derived, so they are outside the scope of this pass. No instruction and no byte value changed - labels and the comments that name them only. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/CIRCUS - all 258 address-derived entry points (SubC<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name, together with the 586 internal branch-target children (SubC<addr>_Loop, SubC<addr>_OnNonzero, …), which were re-based onto their parent's new name; 0 remain address-derived. The names come from what the ROM shows each routine does: DispatchHandlerPointerTable for the game's computed dispatcher, which pops the return address of its own jsr off the stack, uses it as the base of the inline pointer table that follows the call and jumps through the 16-bit entry the index selects - that is how MainStateHandlerPointerTable, SecondaryStateHandlerPointerTable, ActorUpdateHandlerPointerTable and ActorActionHandlerPointerTable are all reached - RunScreenScript / RunScreenScriptRows / SetPpuVramAddress / WriteAttributeRun / WriteAttributeByte for the byte-coded screen script ($FD = write N bytes, $FE = new VRAM address, $FF = end), LoadActorPosition / SaveActorCoordinates / StoreActorBox / ComputeBoxOverlap / CheckActorBoxOverlap…Five / ComputeActorFacing / PackCoordinateBytes / ApplyCoordinateMask / ResetActorSlots for the five actor slots and their hit boxes, UpdateRingActor / SetRingCoordinate / CheckRingHit / IsRingInPlay for the ring, StartOamSpriteList / ProcessOamSpriteList / QueueOamSprite / ReadObjectScriptByte / AdvanceObjectScript for the $0700 OAM shadow and its byte script, PlaySoundEffectById / PlaySoundEffectPair / UpdateSoundChannels / WriteSoundChannelRegisters for the sound engine, and ReadControllerButtons / AreAnyButtonsPressed / WritePpuCtrl / SetPpuCtrlBit2 / ResetScrollPosition for the input and PPU services. Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it. Residue: 0 entry points remain address-derived; the project README.md records the naming evidence per area. No instruction and no byte value changed - labels and the comments that name them only. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/Millipede (U) - all 225 address-derived entry points (SubC<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name, together with the 600 internal branch-target children (SubC<addr>_Loop, SubC<addr>_OnZero, …), which were re-based onto their parent's new name, and the 561 tracer-generated labels whose name still ended in a target address (<Parent>_<Flag>_<ADDR>), which had that tail removed; 0 remain address-derived. The names come from what the ROM shows each routine does: StartNewGame / BeginGameplay / StartStage / SelectGameMode / SelectGameSlot / SwapPlayerSlot for the game start (the six bytes of ObjectInitParameterTable are copied into the object block at $03DF and the millipede/spider/mushroom/bee slots at $03D0, $03E6, $03E9, $03F2 and $03F5 are seeded), UpdateMillipedeSegment / SaveMillipedeSegment / MoveMillipedeSegment / UpdateMillipedePointer / DrawMillipedeSegment / DrawMillipedeHead / DrawMillipedeBody / SetMillipedeHeadTile / ClearMillipedeSlot / IsMillipedeActive for the millipede, SpawnSpider / DrawSpiderSprite / DrawMushroomSprite / WriteMushroomTile / CheckMushroomCollision / DrawBeeSprite / IsEnemySlotFree / StartEnemySpawnTimer / StartBeeSpawnTimer for the enemies, AdvanceObjectTimer / UpdateObjectDirection / GetObjectDirection / SetObjectDirectionBit / ApplyDirectionMask / FindFreeObjectSlot / ScanObjectSlots for the object slots and the eight single-bit masks of DirectionBitMaskTable, RunScreenScript / ReadScreenScriptRecord / CopyScreenScriptData / ClearNametableTiles / FlushVramWriteQueue for the screen-script writer, UpdateHudTile / WriteHudTileByte / WriteHudTileIndex / ComputeTileIndex for the HUD built from HudSpriteTemplate and OamSpriteYTable, and InitSoundEffect…InitSoundEffectFour / UpdateSoundEffectStep / ComputeNotePeriod / GetNoisePeriodFromTable / SetPulse1Timer / SetTriangleTimer / SetNoiseTimer / SetDmcTimer for the sound engine. Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it. Residue: 0 entry points remain address-derived; the project README.md records the naming evidence per area. No instruction and no byte value changed - labels and the comments that name them only. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Tag Team Wrestling (U) - every one of the 48 data blocks in the 32 KB PRG is now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. 50 labels were placed with name_data_region.py (base $8000) and eleven earlier labels were renamed: RingEntrancePointerTable became RingEntranceDpcmPointerTable, UnreachedRomData became RingEntranceDpcmSampleData2, WrestlerNameLengthTable became MessageHoldFrameTable, PpuBitMaskTable became BitMaskByBitIndexTable, WrestlerHandlerPointerTable became WrestlerMoveHandlerPointerTable, WrestlerMovementData became WrestlerMoveParameterData, LevelGraphicData became TitleScreenTextScripts, LevelTilePatternData became RingPalette, LevelObjectPlacementData became UnreachedRomData, UnreachedRomData2 was removed (its address $A9FD now carries GameModeDispatchPointerTable) and WrestlerParameterTable became WrestlerSlotParameterTable. The finding that fixes the layouts is the game's nametable-script writer: Sub_FD3A ($FD3A -> $FF65) takes a script address in X:Y, reads a two-byte nametable address, then writes every following byte as a tile index of (byte - $20), with $1F meaning "next nametable row" and $FF ending the script - which is what makes $AB05-$AC7E a run of title-screen drawing scripts and what turns $B431-$B697 into two NES palettes plus ten ranking title scripts plus six belt pictures. The other names are read out of the consuming code: MessageHoldFrameTable ($9DC5), WrestlerMoveDescriptorPointerTable/WrestlerMoveSpeedTable/WrestlerMoveSpeedTable2 ($A409/$A429/$A431), WrestlerSlotParameterTable ($AE01), MatchScoreTileTable/ScoreboardPanelScript ($B017/$B031), WrestlerPinCountTable ($B224), RankingScreenPaletteTable/RankingScreenAltPaletteTable ($B431/$B441), RankingRegionLabelTable ($B97B), RankingScreenGraphicScriptTable ($B9E2), WrestlerPoseHandlerTable/WrestlerPoseHandlerTable2/WrestlerPoseHandlerTable3 ($BFF3/$C385/$C6B4), PlayerXByGameStateTable ($C6A7), WrestlerMoveStateTable ($C7D1), WrestlerSpriteTileTable ($D449), WrestlerStartPositionTable ($D7AA), Wrestler1PoseHandlerTable/Wrestler2PoseHandlerTable ($DA82/$DB21), AttackPromptTileRow ($DC5B, the nine raw tiles that spell " ATTACK! "), SoundEffectMusicData/SoundMusicHandlerPointerTable ($E050/$E18C), RingHudTileRowData ($E256), RingPostTileRowData ($E669), RingSpriteNametableAddressTable/RingSpriteTileAttributeTable ($E753/$E773), LastSecondsMessageScripts ($E956), WrestlerFacingTable ($EBDD), WrestlerMoveFlagTable/WrestlerMoveParameterTable ($ED2A/$ED3D), SoundEffectHandlerPointerTable ($EE75), NotePitchHandlerAddressTable ($EEB7), SpriteTileIndexList ($EF2C), TimerLabelTileRow ($F119, the eight raw tiles that spell "TIME = ") and SoundSequencePointerTable/SoundEffectParameterTable ($F2D6/$F2EF). The blocks nothing reads - $A281-$A302, $B2E5-$B301 (a dead "!WINNER IS RICKY FIGHTERS" copy), $D9C8-$D9D6 (a dead " TIME OVER " copy), $E7F0-$E82F, $E893-$E8B2 and $EF89-$EF8C - are recorded in the inventory as unreferenced data with their ROM provenance, and $DE38-$DEBB as code the tracer could not reach. The audit trail of the first twelve data blocks carried PRG offsets numbered from $C000 instead of $8000; they now agree with the rest of the file. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - CNROM/World Class Track Meet (U) - every one of the 55 data blocks in the 32 KB PRG is now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. 37 labels were placed with name_data_region.py (base $8000). The names are read out of the consuming code: EventMusicTrackTable ($83EB, the four track numbers the menu's event choice hands to AudioChannelLoop), EventTaskPointerTable ($859B, the twelve 16-bit addresses of the event-task routines - the tracer cannot follow the pushed-address dispatch, which is why $8DA5-$9140 is still filed as data), SpriteSlotByObjectTypeTable ($8717), EventSpeedStepTable/EventSpeedStepTable2 ($95D8/$95F8), RunnerLaneIndexTable ($964C, 8 groups of 16), EventScreenPaletteTable ($96E5, thirteen four-byte records), RunnerSpriteTileList ($989A), RunnerSpriteParameterTable ($9AEF, thirteen four-byte records), ObjectSpriteOffsetTable ($A547), EventObjectPositionTable ($A5CC, nine eight-byte records of four coordinate pairs), RunnerRankTable ($A690, 96 place values), RunnerSpriteTileAttributeTable ($A880), DisplayAddressPairTable ($A9C7), RunnerNumberTable/RunnerNametableAddressTable ($AC66/$AC8A), ScoreDigitTileTable/ScoreDigitNametableAddressTable ($ADE8/$ADFE), NametableAddressTable ($B02A), AudioChannelDispatchPointerTable/AudioNoteHandlerPointerTable ($BFC4/$BFE4, each entry one less than the handler address because the sound engine pushes it and an rts lands on it), LaneSpriteParameterTable/LaneSpriteParameterTable2 ($BDC3/$BE43), AudioChannelParameterTable ($C434), ObjectFlagTable ($D067), ControllerButtonMaskTable ($D2BA), ControllerActionTable ($D317), EventStartParameterTable ($D437), RunnerLaneSpriteParameterTable ($D8BA), LaneAttributeTable ($D900), EventStatePointerTable/LaneProgressIncrementTable ($D9CF/$DDE9), EventFrameSchedulerPointerTable ($E5F2), EventGoalParameterTable ($EF40), EventPaletteTable ($EFFC), EventDispatchPointerTable ($F796, the five task entry points the main loop jumps to through jmp ($001F)) and PaletteUpdateNametableAddressTable ($FA95). The thirteen blocks that decode as long runs of 6502 instructions ($8972-$8A07, $8A13-$8ABB, $8B5A-$8D17, $8D23-$8D41, $8DA5-$9140, $91B4-$91CE, $91FF-$9215, $9235-$924B, $9DCF-$9DFD, $9E31-$9E64, $9F33-$A02A, $A034-$A04A, $D11C-$D13A and $E2A3-$E2B1) are recorded as code the tracer could not reach rather than as tables, and $95D8/$95F8, $9C7F-$9C88, $F025-$F6FC, $F8E2-$F8E5 and $FC78-$FFFF are recorded as unreferenced data with their ROM provenance. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Wild Gunman (U) - every one of the 23 data blocks in the 16 KB PRG is now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, structure and its consumer. The finding is that most of Wild Gunman's "data" blocks are in fact code the Pass 1 tracer never reached - the tracer reached only 49.3 % of the PRG, so $C5F8-$CF00, $DEED-$F371 and $F77C-$FFF3 (the whole sound engine) decode as instructions, and the earlier SpritePositionTable name on $F77C was wrong and has been removed. $C844 is now labelled InitialSpriteDataTable (the twelve bytes of the initial OAM table, three sprite entries of Y, tile, attribute and X, copied by lda $C844,x at $C056), $C6AC was renamed UnreachedTileList (84 CHR tile indices that no instruction in this dump reads), and NoteVolumeTable ($F3EF) and VolumeEnvelopeTable ($F48F) are recorded for what their bytes are, with the note that the sound routine that would consume them is itself unreached code. The blocks nothing reads - $C12A-$C12F, $C20C, $DEDD-$DEEC, $F3DB-$F3E2 and $F423-$F425 - are recorded as unreferenced data with their ROM provenance. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Tennis (JU) - the game's data is stored inline, between the routines, so the project README.md now carries a Pass 3 data-table inventory listing every data region with its address range, size, name, structure and the instruction that reads it. Four earlier names were wrong and have been corrected: ServingPlayerLookupTable became PlayerActionByAnimationStateTable ($DD7C, the action type the player performs in each animation state), ComputerActionProbabilityTable became ComputerActionThresholdTable ($DE01, the per-player odds the computer acts at), GraphicsTable_E695 became DeuceMessageTileData (the tile row that spells out the deuce announcement) and the CourtBoundaryTable label, which sat on the code at $E5AD, has been moved to the eight bytes it actually describes at $E5F0. The rest of the inventory names the game's own tables: InitialMatchObjectStateData ($C847, twelve 16-byte records), PlayerActionAudioRequestTable ($C965), MatchResultAudioRequestTable ($CA22), CourtObjectSpawnParameterTable ($CC7E), BallStateTransitionParameterTable ($CEB1), PlayerActionAndTargetOffsetTable ($CFA2), PlayerPoseRacketXOffsetTable ($D0B1), PlayerPoseAnimationSetA ($D31C), NetCollisionThresholdTable ($D735), BallLandingBoundaryTable ($D780), RacketHitTrajectoryTable ($D889), RacketAimAdjustmentTable ($DA30), Player2CourtPositionTable ($DAA2), ScoreStateTransitionTable ($DD11), CourtObjectMetaspriteData ($E02B), ScoreTextPpuAddressTable ($E62A), UmpireMessageAudioPriorityTable ($E63C), ScoreIncrementLookupTable ($E804), UmpireMessageTileData ($EA82), MatchScreenTextData ($EB92) and the APU and instrument tables at $F8FF and after. The two-byte EngineTable_XXXX and PhysicsTable_XXXX labels are single instructions the tracer did not assemble, not tables, and the inventory says so. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/Popeye (JU) (PRG 0) and NROM/Popeye (JU) (PRG 1) - all 297 address-derived entry points (SubC<addr>, every jsr/jmp target that carried no semantic name) in each of the two Popeye banks now carry a ROM-specific name; 0 remain address-derived. The names come from what the ROM shows each routine does: InitPlayerState for the reset-time player record at $0500, ResetVramUpdateQueue / QueueVramBlockFromLevelTable / ApplyVramUpdateStream for the VRAM command queue at $0330, UploadOamViaDma for the $0200 sprite shadow, ClearOamBuffer, WriteOamEntries / AddOamSprite / ReserveSpriteSlots for the OAM builders, the five NMI state handlers named for the state they run (RunGameplayScreen, RunTitleScreen, RunAttractDemo, RunEnemyEncounterUpdate, RunStageTransitionUpdate), the level setup (BeginLevelLoad, LoadLevelLayout, LoadLevelPalette, BuildTitleScreenOne/Two, BuildLevelScreenZero/One/Two, StartNextLevel), the player (UpdatePlayerMovement, StartPlayerThrow, StartCarryAnimation, FacePlayerRight/Left), the screen state machine (RunScreenStateMachine, HandleScreenState01…HandleScreenState80), the sound engine (UpdateSoundEngine, AdvanceSoundSequence, AdvanceSoundEffectSequence, SetPulse1ControlAndNote, WritePulse1NoteTimer, WriteTriangleNoteTimer, LookUpNotePeriod) and the hardware helpers (ReadControllers, PollJoypadButtonEdge, ToggleAttractMode, DisableRendering, ClearScreenAndSpritePalettes). PRG 1 carries the same code with a few addresses shifted by +3 to +6 bytes in the $C7xx-$CFxx range, so the names were worked out on PRG 0 and transferred routine by routine. Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it. No instruction and no byte value changed - labels and the comments that name them only. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/1942 (JU) - all 375 address-derived entry points (Sub_xxxx / SubCxxxx) now carry a ROM-specific name; 0 remain address-derived. The names come from what the ROM shows each routine does: the deferred-call machinery (QueueDeferredCall, RunDeferredCallQueue, EnableNmi), the VRAM command queue at $078F (RunVramCommandQueue, BuildVramColumnQueue, QueueColumnTile, ApplyColumnAttribute, ComputeColumnAttribute), the scrolling background (UpdateBackgroundScroll, ScrollBackgroundLeft/Right/Up, ResetNametableBuffer, ApplyScrollWaveLeft…Down), the player and its shots (InitPlayerState, InitPlayerHitbox, TestPlayerHitbox, CheckPlayerHitEnemy, CheckPlayerShotCollision), the object system (InitObjectTables, SpawnObject, DespawnObject, UpdateObjectState, AdvanceObjectState), the enemy spawn table at $A2F2 (DispatchEnemySpawnCommand, SpawnEnemyObject, DespawnEnemyObject, SetEnemyObjectFlag/X/Y) and the per-enemy-type behaviour table at $EFF2/$FF09 (InitEnemyType0…InitEnemyType9, InitDefaultEnemyType, RunEnemyTypeBehaviour, LoadEnemyPatternData…Alt3, CheckEnemyCollisions, TriggerEnemyDrop). Each renamed routine's Global: line was given a sentence that says what the routine is for in the game's own terms. The SubCxxxx labels that survive (ColumnClassTable, EnemyPatternDataPart1…, EnemyMovementDataPart1…) sit on data blocks, not on code, and are named for the block they head. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/Soccer (JU) - 311 of the 354 address-derived entry points (Sub_xxxx / SubCxxxx) now carry a ROM-specific name: StartNewMatch / InitMatchState / ResetMatchState for the match setup, UpdateGameFrame for the per-frame step, ReadControllers, CallLevelHandler / CallPlayerHandler for the two dispatch tables, the VRAM writers (ApplyVramUpdateStream, WriteVramBlockFromTable, ClearScreenAndPalettes, ClearOamBuffer, HideOamSprites), the player and ball (UpdatePlayerSprite, UpdatePlayerPosition, MovePlayer, SetBallPosition, GetFieldTile, LoadLevelTileRow), the score and HUD (UpdateScore, SetScoreDisplay, SetScoreDigits) and the sound engine (UpdateSoundEngine, AdvanceMusicSequence, AdvanceSoundEffectSequence, SetPulse1ControlAndNote, SetPulse2Timer, SetTriangleTimer, LookUpNotePeriod). Each renamed routine's Global: line was given a sentence that says what the routine is for in the game's own terms. Residue: 44 entry points remain address-derived and are recorded in the project README.md with the reason for each group - 28 of them sit on data blocks the tracer split because their bytes decode as instructions, and the other 16 are player-record and field-state helpers whose RAM field the dump does not identify. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/CIRCUS - all 74 data blocks the tracer emitted are now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. 57 labels were placed with name_data_region.py (base $C000). The finding that fixes the layouts is the game's computed dispatcher: DispatchHandlerPointerTable ($C4CD, then called SubCC4CD) pops the return address of its own jsr off the stack and uses it as the base of the inline pointer table that follows the call, so the four inline tables are 16-bit handler tables - MainStateHandlerPointerTable ($C0D7, six entries), SecondaryStateHandlerPointerTable ($C1D8, nine entries - the tracer cut it at $C1E4 because the pointer bytes $C3 decode as an undocumented DCP opcode), ActorUpdateHandlerPointerTable ($C9D3, five entries) and ActorActionHandlerPointerTable ($D80C, four entries - the tracer emitted the last two as instructions, sta $51D8 and cmp $D95F,y, because the pointer bytes decode as opcodes). The other names are read out of the consuming code: ActorPpuControlTable ($C4A6, four PPUCTRL values picked by the actor index), ActorOamSpriteTemplate/ActorSpriteYTable ($C5A6/$C5AE, the two-sprite OAM template and the four actor Y positions), MenuTextTable ($C763, the game's own strings - "CONTINUE", "STAGE", "1 PLAYER", "KONAMI", "BY SOFTPRO"), SecondaryStateActionTable ($C1CC, the per-state action code), ActorStateSizeTable ($C1B2), ActorTargetPositionTable ($CF3F), ObjectScriptPointerTable ($D158, 54 pointers), ObjectUpdateRoutinePointerTable ($D2DD, 16 pointers), SoundEffectDefinitionTable ($CBBC, three 10-byte records), SoundSequencePointerTable/SoundNotePeriodTable ($FABB/$FAD1) and the audio, ring and object parameter tables at $D4C7, $D53D, $D7C5, $E6DF, $E7E5, $F226, $F320, $F514, $F65B, $F744 and $F76C. The blocks no instruction reads are recorded in the inventory as unreferenced data with their ROM provenance or, where the bytes decode as instructions, as code the tracer could not reach - the CDL capture confirms the split for the blocks the CPU touched ($C212-$C24F, $C2F5-$C302 and $E532-$E561 are flagged Code; $C763-$C819, $CFB7-$CFCD, $CCE6-$CD27 and $D670-$D713 are flagged Data). No instruction and no byte value changed - the .byte rows were only cut at the new label boundaries. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Duck Hunt (JUE) - all 28 data blocks the tracer emitted are now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. 23 labels were placed with name_data_region.py (base $C000). The dispatch mechanism is the same one the other Namco titles use: SubCC35E ($C35E) pops the return address of its own jsr and reads a 16-bit handler pointer out of the inline table that follows the call, which is why GameModeHandlerPointerTable ($C128, 19 entries) and StateHandlerPointerTable ($CCB0, three entries - the CDL marks exactly those six bytes as Data) are handler tables. The other names are read out of the consuming code: DuckSpeedTable ($E64B, the 24 ascending speeds the duck's setup reads), DuckFlightParameterTable ($E968, four 6-byte flight records), DuckFlightPathTable ($F300, the duck's horizontal path), DuckSpriteXOffsetTable ($DCC0), DuckStateInitialValues ($E5DF), PlayerStateInitialValues ($E5EB), SoundEffectScriptTable/SoundEffectPointerTable ($EC1F/$EC38), and the audio tables TriangleNoteTable ($F3E1, the 14 triangle periods of a phrase), NoiseEffectTable ($F400, five (control, period, length) records), NoisePeriodTable ($F6CB), NoisePeriodTableAlt/NoiseControlTable ($F731/$F761, the drum's two 48-entry tables) and SoundInstrumentParameterTable ($F847). The 6,297-byte block at $DAC5 is the game's graphics and sprite-description area - a 38-entry pointer table, a sprite-record region, two "opcode + operands" tile-placement scripts, the screen-layout data and a bitmap region - and the 23 tables inside it that the code reads by name are named and listed; its finer format is not established from this dump and the inventory says so. The blocks no instruction reads are recorded as unreferenced data with their ROM provenance or, where the bytes decode as instructions, as code the tracer could not reach - the CDL confirms the split for the blocks the CPU touched ($C5AA-$C5D8, $CD1D-$CD2C and $CD2E-$CD43 are flagged Code; $C128-$C14D, $CCB0-$CCB6, $F847-$F84C, $F859-$F862 and $F86B-$F875 are flagged Data). No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Balloon Fight (JU) - all 44 data blocks the tracer emitted are now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. 27 labels were placed with name_data_region.py (base $C000). The names are read out of the consuming code: CartridgeIdAndInitialValues ($C082 - the ASCII "HAL" of HAL Laboratory followed by 15 bytes of initial counters), ObjectHandlerPointerTable/ObjectHandlerIndexTable ($C3B5/$C3BF, five handler pointers and the ten indices that select them), BalloonTileGridTable ($CD26, nine 4-byte tables that are the 3x3 CHR tile grid of each of the four balloon graphics), BalloonPositionTable/BalloonXPositionTable/BalloonSpriteAttributeTable/BalloonSpriteFlipMaskTable ($CEAE/$CEB2/$CEB5/$CEBD), BalloonSpawnParameterTable ($C88B, the per-direction deltas, speeds and spawn positions), BalloonMovementDeltaTable ($C9E5, the four copies of (08, 08, F0, F0) and (EE, 0A, 0A, EE) that give the four diagonal steps), LevelBalloonParameterTable ($D10D, four parallel 5-entry tables - speed, spawn X, type and flag, one entry per level), LevelParameterTable/LevelBalloonHeightTable ($C748/$C761), StageNametableAddressTable ($D65C, 64 16-bit nametable addresses), StageLayoutScript ($D92C) and StageGraphicScript ($D12B), the two tile-placement scripts at $D437 and $D12B, BalloonParameterTable ($E5C5, the per-balloon-type parameter groups the collision code reads) and the noise tables at $F6E5. The blocks no instruction reads are recorded as unreferenced data with their ROM provenance or, where the bytes decode as instructions, as code the tracer could not reach; the 2,173-byte block at $DB27 is recorded with the object-record layout that is visible in it and with the note that its finer format is not established from this dump. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/Donkey Kong (JU) - all 358 address-derived entry points (SubC<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name: MainGameLoop for the endless frame loop, InitPPURegisters for the PPU register setup, UpdateGameObjects for the per-frame object step, ClearSpritePatterns and HideBackgroundAndClearNametable for the two screen-clearing steps, LoadSpriteInitPointerToZp00 / LoadObjectPointerToZp02AndDraw / LoadObjectPointerToZp00AndDraw for the three VRAM-stream loaders, DrawKongHitChestLeftHand / DrawKongHitChestRightHand / DrawKongHoldingBarrel / DrawKongPickupBarrel / DrawKongSideToss for the five Kong tilemap streams, DrawLevelBlocks / ClearNametable / DrawVramBlock for the screen writers, UpdateTimers / ReadControllers / UpdateSoundEngine / ProcessSoundCommands / InitSoundEffect for the frame services, and so on. 100 of the names come from the project's own Pass 2 map dk_full_map.tsv, which had never been applied to them; the other 258 were read off the routine bodies and their callers. Every ; Routine: header and every Global:/Local: line that mentioned an old name was rewritten with it. Residue: 0 entry points remain address-derived; the 654 internal branch labels (SubC<addr>_<suffix>) are still address-derived and are recorded as open work in the project README.md. No instruction and no byte value changed - labels and the comments that name them only. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/Othello (U) - all 302 address-derived entry points (Sub_<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name, together with the 101 SubC<addr> local labels of the same kind: WaitForVBlank for the frame spin, SetPpuMask / SetPpuScrollAndControl for the PPU writes, ReadKeyFromQueue for the key queue at $0400, PlaySoundEffect for the sound command, SetCursorOnBoard / ComputeBoardSquareIndex / ComputeBoardSquareIndexAlt for the board cursor, SetSquareOwner / ClearSquareOwner / PrepareSquareLookup for the square array at ZpA5, FindFlippableSquare for the eight-direction move scan, WriteBoardSquareToVram for the nametable writer, DrawBoardBackground for the board frame and DrawScoreDigit for the score row. Residue: 0 entry points remain address-derived; the 649 internal branch labels (Sub_Loop<n>, Sub_OnNonzero<n> and so on) are still address-derived and are recorded as open work in the project README.md. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Entry-point naming - NROM/Tag Team Wrestling (U) - all 302 address-derived entry points (Sub_<addr>, every jsr/jmp target that carried no semantic name) now carry a ROM-specific name, together with the 154 SubC<addr> local labels of the same kind: SetRingEntrancePointers / UpdateRingEntranceState / UpdateRingEntranceSprite for the ring entrance, CallRingMoveHandler and CallWrestlerPoseHandler / CallWrestlerPoseHandler2 for the two dispatch tables (WrestlerMoveHandlerPointerTable at $A30A, Wrestler1PoseHandlerTable at $DA82, Wrestler2PoseHandlerTable at $DB21), PushRingPointer / AdvanceRingScriptIndex for the ring script interpreter, DrawRingHud for the ring HUD, StoreRingSpritePositions / BuildRingSpriteList / BuildRingMoveList for the two sprite lists, WriteRingNameTiles for the wrestlers' names, WriteGraphicScript for the graphic-script writer and DispatchGameMode for GameModeDispatchPointerTable at $A9FD. Residue: 0 jsr/jmp entry points remain address-derived. Eleven SubC<addr> labels are still address-derived and are recorded in the project README.md with the reason: SubC8787 sits on the five bytes between the two DPCM sample blocks, and SubCED0D-SubCED23 ($ED0D-$ED29) are the 29 bytes in front of WrestlerMoveFlagTable that the tracer decoded as BRK/ORA (zp,x); the dump does not reveal what they are. The 809 internal branch labels are still address-derived and are recorded as open work. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Clu Clu Land (JU) - every data block in the PRG is now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. 75 labels were applied with name_data_region.py (base $C000) and the hand-labelled blocks were renamed: ResourceScriptPointerTable became ScreenNametableStreamPointerTable (the 30 little-endian pointers to the per-screen nametable update streams at $E75D-$F11D), TitleOrMessageTileString became ScreenModeFlagTable (one flag byte per screen, parallel to the pointer table - the entry is copied to $3F and tested for zero at $C53B and $CE1B, so only screens 21 and 24 select the alternate behaviour), and DirectionAndCoordinateTables_C0B4 and ResourceDescriptorData_C058 were split into the tables they really are: ObjectMoveDeltaLowTable/ObjectMoveDeltaHighTable (the two halves of a 16-bit per-direction movement delta), ObjectStageTimerLimitTable, ObjectStageSpeedTable, ObjectStageCountTable, ObjectStageValueTable, ObjectSpawnCountTable and HudMarkerOamTemplate (four 4-byte OAM records - Y, tile, attribute, X - copied in reverse into the sprite shadow at $02E0-$02EF). The nametable update format was read out of the writer at $DED5: the command byte's low nibble picks the VRAM region (PPUADDR high = $20|(cmd & $0F)) and its high nibble picks one of the 16 four-tile groups in NametableTileGroupTable at $DE95, and each stream is a run of (command, address) pairs ended by $FF. The audio note table at $F81C-$F82B turned out to be a descending note-period sequence (140, 133, 126 ... 35) - its first three bytes are the tail of the never-executed instruction at $F81C, so they are data, not code. The blocks nothing reads - $C062-$C0A0, $C0E5-$C0E8, $C0E9-$C195, $C51B-$C52A, $CF28-$CF33 and $D1F5-$D208 (an ASCII tile-index table) - are recorded in the inventory as unreferenced data with their ROM provenance, and $D551-$D554 is recorded as the bytes of an instruction the tracer split at the wrong boundary. No instruction and no byte value changed - the .byte rows were only cut at the new label boundaries. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Defender 2 (U) - every data block in the PRG is now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. The 35 placeholder labels (GameplayLookupTable_Cxxx, GameplayConstantData_Cxxx) were replaced with names read out of the consuming code: ScreenStateHandlerPointerTable (the five 16-bit pointers to the game's state entry points at $C092, $C177, $C25D, $C261 and $C355), PlayfieldColumnTerrainTable (the terrain generator at $C183 computes an index of ((value >> 2) + 1) & $0F and reads four consecutive bytes of each record - height, height, a column type masked with $07 and a third parameter - so the record size is four, not one), BitIndexToMaskTable (the eight single-bit masks $01-$80), DefugeCreditString (72 bytes of ASCII: DEFUGED BY TOSHIO SENGOKU ( Intelligent Systems ) 1987/07/31 15:00 - the game's debug build stamp), SpriteDefinitionBaseAddress/SpriteDefinitionPointerTable (two bytes that together form the code address $C7EA, through which the sprite definitions are read two bytes at a time), the four column-limit tables at $C8F7/$C8FB/$C8FF/$C8EB, the four 16-bit sprite-coordinate tables at $C89F/$C8B5/$C8CB/$C8DB, ControllerButtonMaskTable, SpriteTileIndexTable and the APU period, pitch, envelope and note tables at $F800/$F801/$F84C/$F919/$F925/$F932/$F933/$F942/$F943/$FE59-$FE5D. The four blocks that decode as 6502 code ($C161-$C168, $C25D-$C260, $CE47-$CE50 and $F8D1-$F8D3) are recorded as code the tracer could not reach rather than as tables, and the 3,574-byte block at $EA0A - the largest data region in the PRG, holding a 4-byte sprite-layout table at $EACA, an ASCII character table at $EAAA and a column-position table at $EB0A - is recorded as one block because no instruction in this dump addresses it with an absolute operand. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Donkey Kong 3 (JUE) - every data block in the PRG is now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. The 48 placeholder labels (IndexedGameplayLookupTable_Cxxx, CodeOrDataPointerTable_Cxxx, ActorCollisionOrStateTable_Cxxx, GameplayConstantTable_FF4C) were replaced with names read out of the consuming code: GameplayScreenStateHandlerTable (the eight 16-bit gameplay screen handlers at $C192 - the old README's "15 handlers" was a miscount), ObjectRowSpawnTable (the 21 four-byte per-screen-row spawn records at $D0B4, walked with the actor's screen Y and ended by an $AA marker), ObjectMovementIncrementTable (the per-frame increment stream at $CD85), ObjectTimerReloadTable (22 (countdown, value) pairs at $CF84), ObjectMovementStepTable/ObjectMovementLimitTable (the two 8-byte tables at $D1C7 that are added together into the actor's coordinate and limit), PackedHudValueTable, ActorSpriteTileTable/ActorSpriteAttributeTable/ActorSpritePaletteTable/ActorSpritePatternTable (one 32-byte table of eight 4-byte records at $D3B5, read at four different bases so that one lookup fetches all four bytes of the selected record), InitialOamSpriteTable (32 bytes copied into the sprite shadow at $02E0), ActorBehaviourFlagTable, GameplayObjectStateTimerTable, AnimationBaseTileTable (eight 2-byte base tile numbers at $F6B0), MetaspriteLayoutPointerTable (16 pointers at $F757) and the audio note, sequence, pitch and sample-pointer tables at $F97B/$FAD4/$FAF6/$FB17/$FDA4-$FDAB/$FFC3/$FFE2. The five blocks that decode as long runs of 6502 instructions ($C9EB-$CB2A, $E110-$E694, $E69B-$E7C4, $EA5E-$EA9E and $EB8E-$EBC5) are recorded as code the tracer could not reach rather than as tables - the Pass-1 trace never entered them, so the tracer emitted them as data. No instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Donkey Kong Jr. (JU) - the data side of this project is now recorded: the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. The 1,646-byte table area at $C000-$C66D is a flat run of small tables rather than one table, and the inventory now says so and gives each of them its record size - the 5-byte static object-layout records behind ObjectLayoutXTable/YTable/TileIndexTable/DimensionsTable/OamOffsetTable, the 4-byte HUD digit descriptors, the 4-byte OAM records of the three enemy templates, the low/high halves of the interleaved 16-bit pointer pairs, the demo input code and duration tables and the 2,641-byte block of stage VRAM command streams at $F266-$F8B5 that the deferred VRAM queue at $EE9C drains. The blocks no instruction reads - $C0F4-$C136, $C173-$C1C7, $C226-$C2C0, $C31A-$C368, $C37A-$C3BC, $C3CE-$C3F3 and $C461-$C484 - are recorded in the inventory as unreferenced data with their ROM provenance. No source change was needed: the data blocks already carried their semantic names. make still prints Byte-identical ROM verified.

  • _tools/name_data_region.py - the row parser choked on a source that carries a redundant provenance note in front of the canonical audit comment (; PRG $0647 | CPU $C647 ; CPU:$C647 PRG:0647 20 44 ...), which is the form NROM/Defender 2 (U) and NROM/Donkey Kong 3 (JUE) use: the byte list it extracted ran past the end of the row and the tool died with a ValueError. The body is now cut at the first ;, so the tool names data blocks in those sources as well. The tool still only inserts labels and cuts .byte rows - it never changes a byte value.

  • Pass 3 semantic routine naming - UNROM/Zanac (U) - the 94 entry points that the banked image still reached by address now carry a semantic name. Bank 6 is the enemy bank, so its routines are named for what the enemies do (MoveEnemyX, MoveEnemyY, StepEnemyTowardsTargetX, TestEnemyOverlap, SelectEnemyAiDirection, UpdateEnemyAiState, SpawnEnemy, SpawnEnemyWave, SelectEnemySpriteType, UpdateEnemyWaveTimer, SpawnEnemyProjectile, RandomizeEnemyDirection, ...), and the twelve routines that bank 6 exports to the engine in bank 7 carry an Export prefix (ExportUpdateEnemyWave, ExportSelectEnemySlot, ...). Bank 7's three remaining entries are named for what they do (WriteVramBlock, SetPpuMaskAndNmiFlag, UpdatePpuAndMask). The rename covers all three source files - prg.s (banks 0-5), prg.6.s (bank 6) and prg.07.s (bank 7) - together with the .export/.import lists that link the banks. Each renamed routine's header was regenerated so that it names the new label. No instruction and no byte value changed - only labels and the comments that name them. make still prints Byte-identical ROM verified.

  • Pass 3 semantic routine naming - NROM/Duck Hunt (JUE) - every routine the game reaches through a jsr or a jmp now carries a semantic name; the 134 address-derived entry points (SubCCxxx) are gone. The game's own vocabulary is used: the duck and dog logic is named for what it does (UpdateDuckSprites, SelectDuckSprite, CheckDuckCollision, ProcessDuckScript, UpdateDogSprites, ComputeDuckHitBox, CheckDogAppearance, ...), the graphics side for what it draws (DrawPpuBlockList, DrawQueuedPpuBlock, QueuePpuBlocks, HideAllSprites, CopyNametableToPpu, ...), the score code for what it counts (UpdateScoreDigits, AddBcdValue, SubtractBcdValue, SplitBcdValue) and the sound engine for what it plays (UpdateSoundEngine, LoadSoundEffectRecord, UpdateMusicChannels, UpdateSoundEffectSlots, SilenceApuChannels). The twenty bit-test helpers that pick a sound request out of the three request masks are named for the mask and the bit they test (TestSoundRequestMask0Bit0 ... TestSoundRequestMask2Bits5to7). Each renamed routine's header was regenerated so that it names the new label. No instruction and no byte value changed - only labels and the comments that name them. make still prints Byte-identical ROM verified.

  • Pass 3 semantic routine naming - NROM/Dig Dug II (U) [!] - every routine the game reaches through a jsr or a jmp now carries a semantic name; the 135 address-derived entry points (SubCAxxx etc.) are gone. The game's own vocabulary is used: the actor system is named for what it moves (MoveActorRight, MoveActorDiagonally, UpdateActorBehaviour, DispatchEnemyBehaviour, SelectActorBehaviour, WriteActorOamEntry, CheckActorCollision, ...), the round and wave logic for what it schedules (StartEnemyWave, AdvanceEnemySchedule, AdvanceStageSchedule, UpdateRoundPhase, HandlePlayerDeath, ...), the player for what it does (HandlePlayerInput, CheckPlayerCanAct, StartPlayerRespawn, ...) and the sound engine for what it plays (InitializeSoundEngine, UpdateSoundChannel, WriteApuStatusRegisters). Each renamed routine's header was regenerated so that it names the new label. No instruction and no byte value changed - only labels and the comments that name them. make still prints Byte-identical ROM verified.

  • Pass 3 semantic routine naming - NROM/Ms Pac-Man (Namco) (U) - every routine the game reaches through a jsr or a jmp now carries a semantic name; 145 of the 146 address-derived entry points (Sub_Cxxx) are gone. The game's own vocabulary is used: the screen-script interpreter is named for what it draws (BeginScreenScript, RunScreenScript, DrawScriptTile, BuildVramTextCommand), the actor system for what it moves (SpawnActor, MoveActor, DispatchActorClass, SetActorState, CheckActorBounds, DrawActorSprite), the sound engine for what it plays (SilenceAllSoundChannels, StartSoundEffect, StartMusicTrack, AdvanceSoundChannel, PlayNote), and the menu and score code for what it shows (InitMenuOptions, ParseMenuOption, ConvertScoreToDecimal). Each renamed routine's header was regenerated and given a Global: line that says what the routine is for in the game's own terms. The one entry point that keeps its address-derived name, Sub_C4DB, is recorded in the project README's Open work section: the bytes the tracer decoded into it would write into the PRG ROM and end in a brk, so the dump does not reveal what it is for. No instruction and no byte value changed - only labels and the comments that name them. make still prints Byte-identical ROM verified.

  • Pass 3 semantic routine naming - NROM/Urban Champion (JU) - every routine the game reaches through a jsr or a jmp now carries a semantic name; the 157 address-derived entry points (SubCCxxx) are gone. The sound engine is named for what it does to the APU (UpdateSoundEngine, UpdateApuChannels, LoadSoundEffectRecord, LookupNoteDuration, WritePulse1PeriodAndTimer, SetPulse1Timer, StopAllSoundChannels, ...), the graphics side for what it draws (DrawPpuBlockList, DrawQueuedPpuBlock, QueuePpuBlocks, HideAllSprites, CopyNametableToPpu, ...), the controller code for what it reads (ReadControllers, ReadControllerButtons), the BCD helpers for what they compute (AddBcdValue, SubtractBcdValue, SplitBcdValue), the fighter code for what it moves (CheckFighterHit, UpdateFighterMovement, DrawFighter, RunFighterScript, ...) and the sound-request helpers for the mask bit each of them sets (RequestSoundEffectSlot0, RequestPulse1Preset0, StopAllSound, ...). Each renamed routine's header was regenerated and given a Global: line that says what the routine is for in the game's own terms. No instruction and no byte value changed - only labels and the comments that name them. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - UNROM/Zanac (U) - every one of the 104 data blocks the tracer emitted is now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. The image is banked, so each address is given with its bank. 47 blocks are named for what they are: the 48-entry JMP trampoline table at $C000 (CmdHandlerJumpTable - the Mesen CDL shows the CPU fetching 25 of the 48 opcodes, so it is a real dispatch table entered by a computed jump), the per-type sprite attributes at $C100 (SpriteAttrTable, whose $C12F-$C133 and $C182-$C186 entries are the half-widths and half-heights of the five enemy types), the fifteen nametable row addresses and their bit masks at $D7BD (NametableRowAddrTable, SpritePixelMaskTable), the title and staff scripts at $E290 and $E671 (TitleScreenTextScript, StaffCreditsText - records of [command][length][bytes] and $00-terminated strings prefixed by the screen row they are drawn on), the PPU attribute-initialisation script at $E3B8 (PpuAttrInitScript - [address hi][address lo][payload...][$80] records that write $23C0-$23FF), the 108-entry equal-tempered frequency table at $F01C (NoteFrequencyTable - each entry is 2^(-1/12) of the one before) and the 36-entry sound-script pointer table at $F0FE (SoundScriptPtrTable). The 55 blocks that are code the tracer could not reach are recorded as such, with the CDL evidence, and the rest as unreferenced data with their ROM provenance. _tools/name_data_region.py grew a --file option, because a banked project keeps one source file per bank and the tool worked on prg.s alone; --base still selects the PRG-offset base, so --base=-0x10000 names the bank-6 and bank-7 blocks with the right PRG offset. No instruction and no byte value changed - the .byte rows were only cut at the new label boundaries. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Golf (JU) - every data region in the PRG is now interpreted, and the project README.md carries a Pass 3 data-table inventory with each region's address range, size, name, structure and the instruction that reads it. Five sub-tables were separated out of the .incbin spans because an instruction in this dump names their address: ClubSwingSpriteTable ($C78B, read by lda $C78B,y at $C858 with Y = twice the club-selection index), CourseCellPropertyTable ($D82E, read by lda $D82E,y at $D7F8), HoleLayoutIndexTable ($E59D, read by lda $E59D,y at $E6B2 with Y = the hole index) and HoleLayoutParameterTable ($E6A7, read by lda $E6A7,y at $E6BD), and CourseLayoutStreamData ($E6E7) - the eighteen compressed hole maps the run-length decoder at $DD78 walks through the pointer table at $E6C3/$E6D5. The spans that stay unnamed are recorded in the inventory as unreferenced data with their ROM provenance and the value structure that is visible in them, and $D304-$D3B6 is recorded as code the tracer could not reach because it decodes as 51 instructions ending in jmp $D08A. The .incbin spans were only cut, never rewritten, so no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Hogan's Alley (JU) - all 34 data blocks in the PRG are now interpreted, and the project README.md carries a Pass 3 data-table inventory with each block's address range, size, name, structure and the instruction that reads it. The dispatch architecture is what fixes the table layouts: IndirectCall at $C34D pops the return address of its own jsr off the stack, so the table it reads always begins one byte past the last byte of that jsr; working the six call sites out gives the table bases $C124, $C219, $C89C, $CDD1, $D234 and $D3D6, and the index register at each site is what the table is keyed on. So the four inline tables are 16-bit handler-address tables: SubStateHandlerTable (19 entries, keyed on SubStateIndex), ModeHandlerTable (4 entries, keyed on ModeIndex), ActorBehaviourHandlerTable (14 entries, keyed on HandlerIndex) and TargetBehaviourHandlerTable (14 entries), and the two tables that sit in the code region are ScreenSetupHandlerTable (2 entries) and the two-entry table at $CDD1. The sound engine's tables are read the same way as the sibling engine in NROM/Mario Bros (JU): the note base byte is used twice, once as the note offset and once as the record index, which fixes the record size at five bytes (NoteParameterTable, MusicCommandTable, ApuInstrumentTable, ApuPulse1CtrlTable). The thirteen UnreachedCode* blocks are recorded as code the tracer could not reach - every one of them decodes cleanly as a run of 6502 instructions - and two of them ($CB34 and $CDF5) are the handler bodies the dispatch tables point at. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Millipede (U) - all 11 data blocks in the PRG are now interpreted, and the project README.md carries a Pass 3 data-table inventory with each block's address range, size, name, structure and the instruction that reads it. The 1845-byte block at $C000 is not a level map: it is a bundle of small independent tables, and the ones that have a consumer are now named - DirectionBitMaskTable ($C04A, the eight single-bit masks 01 02 04 08 10 20 40 80), ObjectInitParameterTable ($C271, six bytes copied to $03DF at reset), OamSpriteYTable ($C277, four OAM Y positions), HudSpriteTemplate ($C27B, thirteen (tile, attribute) pairs copied to $0330) and SoundSequenceDestAddressTable ($C295, 34 little-endian 16-bit RAM destination addresses, one per sound effect, into which the engine copies a $FE-byte sequence from $F3B9). The sound engine's blocks are named too - MusicSequenceData ($F24F), PulseTimerSelectTable ($FA1A), SoundEffectNoteTable ($FB43, four (note, duration) records) and NoteIncrementTable ($FCCA) - and SoundChannelTable ($FE61) records the double lookup the engine does on the channel table: the byte at $FE61 is the channel's note base and the index of its five-byte parameter record. The four blocks that nothing reads are recorded as unreferenced data with their ROM provenance, and two of them ($D14F-$D159 and $F9CE-$F9D0) are recorded as code the tracer could not reach because they decode as instructions. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/1942 (JU) - every one of the 26 data blocks in the PRG is now named and interpreted under the new rule: 18 labels applied with name_data_region.py (base $8000), and the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The first Pass 3's names were guesses and are corrected: LevelLayoutData is the stage script and tilemap stream, of which only the last 133 bytes are read directly; PpuBitMaskTable is the eight single-bit masks that mark the player's 8-pixel column as occupied in the RAM bitmap at $07C1; EnemyHandlerPointerTable is the 16-entry spawn-command dispatch table indexed by command - the script byte minus $F0 - whose $A31E handler despawns the object; LevelObjectSpawnTable holds the per-enemy-type script pointers at $A413 and the 12 fixed-point X offsets at $A3F9; PlayerScoreTable is the four-row shot-collision Y threshold read at $B4A7; EnemyMovementPatternTable is the three-sprite OAM template the plane update copies into $0230; EnemySpritePositionTable is nine OAM records followed by 30 sixteen-bit enemy handler pointers and the 3-byte movement records that follow them; EnemyHandlerCodeTable is the per-enemy-type behaviour data whose last five bytes are the score thresholds compared at $DA05. Eight blocks are read by no instruction in this dump and are recorded in the project README as unreferenced with their ROM provenance - five of them ($C555-$D069, $F55D-$F56F and the code half of $A770-$A815) decode as 6502 code the tracer could not reach, and $AD99-$ADC8 is an off-screen OAM template that nothing reads. The CHR is the oracle: tiles $00-$09 are the digits and $0A-$23 the letters, which is what shows that the runs of small values in $8494-$9FDA and $BB51-$BC85 are nametable tile data rather than text. The audit trail was damaged - 13 .byte rows carried a second, wrong copy of the audit comment whose PRG offset was negative (PRG:-1F74) because the PRG is mapped at $8000, and 7 more carried an undocumented-opcode note after the audit comment - and was repaired to the canonical ; CPU:$XXXX PRG:XXXX AA BB form; no instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Baseball (UE) - all 120 named data tables and all 93 inline data blocks in the PRG are now listed in the project README's Pass 3 data-table inventory, each with its address range, size, name, structure and the instruction that reads it. The inventory is built on what the source already establishes: the seven-byte object-initialisation records at $D0A0-$D125 (object offset, sprite index, attribute, X low/high, Y low/high) read by ldx ObjectInitializationTable,y at $D06C; the four parallel three-byte state tables at $CD22-$CD2D, which the code reaches with a base one byte below each table's label so that entry 0 as read is the operand byte of the preceding branch; the five pitch tables at $D14B-$D166 and $D2A4-$D2AC, which the pitcher's windup searches with cmp PitchTypeThresholdTable,x at $D1EE; the twelve six-character announcement messages at $CEC9-$CF10, selected by an offset into AnnouncementTextTable and read at $CEBC; the 256-byte rendering/animation pages at $ED00-$F94C that the renderer reads as one array; and the music sequence data at $FE40-$FFE1. Two categories are recorded as not yet interpreted rather than guessed: the record format inside the rendering pages and the music data. The stale addresses in the flow document were corrected against prg.s (the pitching and batting routines live at $D18B-$D3FF, not at $C6xxx), and the paths that still said copilot/ now say this folder. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Burger Time (U) - all 71 named data tables and the 25 inline data blocks in the PRG now carry ROM-specific names and a Pass 3 data-table inventory in the project README giving each block's address range, size, name, structure and consuming instruction. The placeholders are gone: the 68 LookupTable_Cxxx labels and RomData_E871 became names read out of the consuming code - MainStateHandlerPointerLo/Hi are the two halves of the seven-entry main-state handler pointer table at $C000; DecoSignatureString is the ASCII "DECO" the routine at $C78C compares against and stamps into RAM; IngredientPlacementOffsetXTable/YTable are the five (X, Y) pairs at which a burger ingredient is placed; BurgerItemTileTableA-D and BurgerItemAttributeTableA-D are the four burger-item events' sprite tile and attribute bytes; EnemySpawnPositionTable is 23 OAM Y coordinates; PowerUpScoreTable is the 28 ascending score thresholds the power-up routine walks backwards; BitShiftTable is the eight descending powers of two; and the remaining big blocks are the enemy script, sprite handler, level pattern, stage layout, status panel, sound effect and object handler pointer tables. Three further tables are named through equates in locals.inc because their bytes sit inside inline blocks. The audit trail carried the wrong CPU base - it read CPU:$8000 where the PRG is mapped at $C000, so every address was out by $4000 - and was rebuilt with _tools/repair_audit_comments.py --base 0xC000 into the canonical ; CPU:$Cxxx PRG:xxxx AA BB form; the PRG offsets were already correct and are unchanged, and no instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Data-table interpretation of the remaining promoted projects - the rule is in PROMPTS.md and the tier is part-way through applying it: 44 of the 61 promoted projects carry a Pass 3 data-table inventory and the rest are in progress. The entry for each project lands here as it finishes.

  • Magmax (U) - every data block in the PRG is interpreted and named, and the project README.md carries the full inventory (address, size, name, record structure and the consuming instruction). 48 tables placed, including TerrainLevelData (the 15 KB level and screen-text stream at the head of the PRG, split into the parts that have their own consumers), the eleven 16-bit handler tables the game dispatches through, SpriteOamTemplateTable, TerrainSegmentOffsetTable, SoundEffectProgramData and the three 16-bit pointer tables the sound engine uses. The blocks nothing reads are recorded as unreferenced data or as code the tracer could not reach.

  • Othello (U) - every data block in the PRG is interpreted and named, and the project README.md carries the full inventory. 20 tables placed, the clearest being the eight compass directions: the bit masks at $DA49, the direction names at $DA51/$DA59 and the two step tables at $9003/$900B. Also BoardSquareWeightTable, SquareEvaluationWeightTable, MoveOrderTable and BoardFlipMaskTable. The many small blocks are recorded as unreached code, as text reached through the game's own text interpreter, or as the interpreter's operation tables.

  • Pac-Man (U) - every data block in the PRG is interpreted and named, and the project README.md carries the full inventory. 58 tables placed, including ProgrammerSignatureString (the 15-byte "HIROKI AOYAGI" string compared with $0052), MazeScreenTileData (6 rows x 23 tiles written to $20E5), InitialPaletteData, NametableAttributeTable, the ten 16-bit ghost handler tables, CharacterSpriteTileTable, LevelGhostTargetTable and AudioNotePeriodTable (12 APU period values one octave apart). The old README's claim that $C0EB held the copyright text was wrong - it holds the programmer's signature - and is corrected.

  • Pac-Man (Tengen) (U) - the same interpretation as the other Pac-Man dump, applied to this PRG (the two differ only in the licence text and the reset stub). 58 tables placed and the full inventory written into the project README.md, replacing the earlier list of guessed names.

  • Slalom (U) - every data block in the PRG is interpreted and named, and the project README.md carries the full inventory. 50 tables placed, including CoursePathTable (64 records of skier X position and terrain drop), CourseHeightProfileTable, CourseLevelHeightTable, SkierMovementTable, KeyboardInputMap, MusicNoteData (the Namco-format sound sequences) and the 40-entry GameEventHandlerTable. The blocks nothing reads are recorded as unreferenced data or as code the tracer could not reach.

  • Pass 3 data-table interpretation - NROM/Kung Fu (U) - all 15 data blocks in the PRG are now interpreted, and the project README.md carries a Pass 3 data-table inventory giving every block's address range, size, name, structure and the instruction that reads it. The $8000-$8257 block is a bundle of nineteen separate tables and is split accordingly: LevelHandlerPointerTable and LevelSectionListOffsetTable (the two halves of the inline level dispatch), LevelSectionHandlerTable, ScreenInitRoutinePointerTable, ScreenVramStreamPtrLoTable/ScreenVramStreamPtrHiTable (the 79 screens' VRAM update streams), PlayerSpriteYPositionTable, SpritePairIdTable, PlayerSpawnYTable/PlayerSpawnXTable/PlayerSpawnHitPointTable, PlayerSpriteOamTemplate, SpriteSpawnParameterTable, SpritePatrolOffsetTable, GameStateHandlerPointerTable, StatusSpriteOamTemplate, StageScreenLayoutTemplate, EnemySpawnAttributeTable and EnemySpawnDelayTable (106 bytes each, indexed by the same enemy slot). The VRAM update records were read out of the writer at $DAEA/$DB19 - [PPU hi][PPU lo][count][count bytes] - and every pointer in the screen table lands on a byte in $20-$3F, which is what confirms it. Four blocks are left unnamed and recorded in the inventory: $8C8A-$8CC0 is unreferenced data, $8CC1-$8DCE, $D386-$D3C5 and the tail of the $DB48-$F830 bundle are code the tracer could not reach. The audit trail on the data rows was damaged - seven rows carried a second, wrong copy of the comment with a negative PRG offset, and every sixteen-byte row listed only its first twelve bytes - and was rebuilt so that each row's comment carries its exact bytes again; no instruction and no byte value changed. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Kung Fu (U) (a1) - the same interpretation as the (U) revision, applied to the other build of the same game (the routine addresses differ, the tables do not): 21 labels, the same nineteen-table split of $8000-$8257, and the same four blocks recorded as unreferenced data or as code the tracer could not reach. The data rows' audit comments were rebuilt as well, so that each row's comment carries its exact bytes again. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Mario Bros (JU) - all 35 data blocks are now interpreted and the project README.md carries a Pass 3 data-table inventory with every block's address range, size, name, structure and the instruction that reads it. Eight blocks that earlier revisions had named with guesses turned out not to be data at all: $D491-$D4FD (EnemyBehaviourTable) is the enemy-type handler code the inline table points at, $D62D-$D633, $E16C-$E16F, $E18C-$E18F, $E1A4-$E1A6, $E494-$E499, $E4A4-$E4A9, $E4F1-$E4FB, $E69E-$E69F, $ECF5-$ECFA, $C632-$C635 and $CA5A-$CA5D are code the tracer could not reach, and they are left unnamed. The two inline dispatch tables are now read for what they are: $C510-$C527 is twelve 16-bit game-state handler pointers that follow jsr DispatchViaStackPointer at $C50D, and $C593-$C5A2 is eight 16-bit enemy-type handler pointers that follow the call at $C590. $ECCC-$ECEB is the eight-entry enemy OAM template (not a sound envelope), $EE53-$EE69 is ten big-endian sound-engine addresses in three groups, and $E078-$E0B7, $E568-$E5CD, $F4B2, $F574, $F68D, $F94E and $FE6C are the enemy tile sequence, the enemy tile/attribute pairs, the enemy tile groups, the spawn columns, the score-digit OAM templates, the triangle waveform and the note records. A second pass over the same blocks corrected three of those readings and added the sub-tables that the inventory lists: $E558-$E5CD is the stage OAM setup - ten instructions of code, six tile bytes at $E568 and the 24-byte OAM template at $E574 (eight three-byte records, Y / tile / X) that lda $E574,x copies to $0200 at $E53D; $ECCC-$ECEB is the 32-byte initial state of the actor block at $B0-$CF, not an OAM template; and $F904-$F96A is the pulse note period table, two bytes per entry, whose first entries are a descending chromatic ladder - 00 69 is C6 at 1055 Hz and 01 AB is C4 at 261 Hz on the 2A03's own pitch formula, which is what identifies it. The new labels are StageSubHandlerPointerTable, StageSubHandlerPointerTable2, StageOamSetupCode, StageSpriteTileList, StageOamTemplate, StageScriptSetupCode, StageColumnParameterTable, StageRowParameterTable, EnemyTileGroupTable, EnemySpawnVramAddressTable, ScoreDigitOamTemplateTable, TriangleChannelParameterTable, SoundNoteRecordTable, PulseNotePeriodEntry0Low, PulseNotePeriodRestEntry and SoundEffectNoteIndexTable. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Pinball (JU) - all 40 data blocks in the PRG are now named and interpreted, and the project README.md carries a Pass 3 data-table inventory with every block's address range, size, name, structure and the instruction that reads it. 37 labels were placed with name_data_region.py (base $C000). The findings worth recording: $C16A-$C180 is the designer credit in Shift-JIS - the ASCII "BY S.MATSUKA & S.IWATA " - $DE87-$DF86 is a 256-entry table of floor(i*i/256) used for the ball's circle maths, $EC60-$EC7F is the playfield palette as eight RGB triples of standard NES entries, $EFBA-$EFD2 is the title logo as a 5x5 matrix of tile indices, $FCA8-$FCB3 holds the twelve noise-channel periods, and $C400-$CBA6 is the screen layout - 267 tile bytes and 784 big-endian PPU addresses. Fourteen blocks are left unnamed: ten are code the tracer could not reach or one-byte $2C fill, and the rest are unreferenced data whose layout is not established. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Popeye (JU) (PRG 0) - all 7 data blocks in the PRG are now interpreted and the project README.md carries a Pass 3 data-table inventory with every block's address range, size, name, structure and the instruction that reads it. $F088-$F832 is the level's VRAM update stream - the same [PPU hi][PPU lo][count][count bytes] record format the writer at $DB19 consumes - whose first record fills the sprite pattern table at $3F00 and whose second fills nametable 0 from $23C0; $F900-$F96F is the APU pulse note-period table, with the high byte of each period at $F900 and the low byte at $F901; $C000-$C648 is the level object data, whose sprite Y positions at $C50F, bit masks at $C06D/$C06E and 16-bit pointers at $C62B are read directly. Three blocks are left unnamed: two are code the tracer could not reach and one is the eight bytes in front of the vectors. make still prints Byte-identical ROM verified.

  • Pass 3 data-table interpretation - NROM/Popeye (JU) (PRG 1) - the same interpretation as the other Popeye build (the two PRGs differ only in their code), applied here: five labels and the full inventory in the project README.md, with the same three blocks left unnamed. make still prints Byte-identical ROM verified.

  • Tool fixes - _tools/analyze_data_blocks.py no longer reports an empty data set for every block that carries a label (it skipped the label line and the .byte rows behind it) and no longer runs a block's bytes past the start of the next block; _tools/name_data_region.py no longer crashes on a .byte row that carries an undocumented-opcode note after its audit comment. Both were needed to read the evidence for the projects above.

History

Entries move here when the commit they describe is made, newest first. The

2026-10-21 - The tier is complete

  • The data tables of the two test cartridges and of Pac-Man (USA) (Namco) are interpreted. The Joypad Test Cartridge paints its whole picture from a command stream replayed into the PPU, and the stream address is passed inline in ROM after the jsr, so the four two-byte blocks are pointers and not code. The PowerPad Test Card's two twelve-byte tables are the PPU address low byte of each of the twelve controller-bit indicators - and its font has no W in the A-Z run, so the screen really shows "IC MA/ BE BAD". Pac-Man (USA)'s maze splits into a wall tile array and a path tile array, and a large part of its "data" is code the tracer could not reach, because the game dispatches through an indirect jump.
  • All 61 promoted projects now carry a Pass 3 data-table inventory, and the tier passes every item of PROMPTS.md's promotion checklist: python3 _tools/audit_promoted.py --build reports 61 passing and none failing.
  • The entry points are named across the tier - 8,164 address-derived jsr/jmp targets down to 1,401, and 34 of the 61 projects have none left. The residue is recorded per project, with the reason. The last batch was the three Pac-Man dumps (which share one set of names), the two test cartridges, Mermaids of Atlantis, Xevious, Pinball, Port Test Cartridge, Wild Gunman and Baseball.
  • Two documentation errors corrected on the way: the PowerPad Test Card's $E1F1/$E1FB write $2001, so they are ShowBackground and HideBackground and not EnableNMI and DisableNMI; and Pac-Man (U)'s attract-mode routine names are replaced with the ones the code supports.

2026-10-21 - The audit comments go back to their column

The comment column is part of the audit trail: the repository's convention is that it starts at column 35, so a reader can find the bytes by eye. Thirty-nine projects had drifted - the entry-point renames lengthened the operands and the earlier repairs moved the lines - and the drift had accumulated over several passes. align_audit_comments.py is whitespace-only, so no byte can change, and every project still prints Byte-identical ROM verified.

A data row's comment follows its data rather than sitting in the column - that is what the emitter produces, and align_audit_comments.py leaves those rows alone. An instruction whose code text is longer than 31 characters cannot be padded into the column either, and those are left where they are.

2026-10-21 - The entry points are named

PROMPTS.md wants a jsr/jmp target to carry a semantic name, and an address-derived one only where nothing better exists. The tier had 8,164 address-derived entry points; the agents went through them project by project, reading each routine and its callers, and named the ones the code supports.

  • Donkey Kong (JU): 358 named - MainGameLoop, UpdateGameObjects, GrabLadder, the five Kong tilemap streams, the frame services.
  • 1942 (JU): 375 named - the deferred-call dispatcher, the enemy spawn commands, the ten enemy types.
  • Popeye (JU) (PRG 0) and (PRG 1): 297 each - the VRAM update queue, the OAM shadow, the five NMI state handlers.
  • Soccer (JU): 311 - the match state, the player and ball handlers, the two dispatch tables.
  • Donkey Kong Jr. Math (U): 295 - the answer-choice machinery, the text writer, the sound engine.
  • Sqoon (U): 271 - the phase dispatcher, the pod family.
  • CIRCUS: 258 - the inline dispatcher, the byte script, the ring actors.
  • Millipede (U): 225 - the millipede segments, the spawner, the sound effects.
  • Othello (U) and Tag Team Wrestling (U): 302 each.
  • Lunar Pool (U): 185 - the object interpreter, the play field.
  • World Class Track Meet (U): 142 - the event tasks, the olympics sequences.
  • Balloon Fight (JU): 154 - the PPU command buffer, the balloon handling.
  • 10-Yard Fight (U): 115 - the ball physics, the thirteen sound voices.
  • Gyromite (JUE): 120 - the craft and mothership handlers, the fifteen sound-effect requests.
  • Galaga (U): 81 - the enemy formation, the VRAM-record machinery.
  • Magmax (U): 70 - the four object pools.
  • Kung Fu (U) and (a1): 51 each.
  • Slalom (U): 38 - the VRAM tile stream, the course data.
  • Urban Champion (JU): 157 - the sound engine, the PPU block list, the two fighters.
  • Ms Pac-Man (Namco) (U): 145 - the screen-script interpreter, the actor system.
  • Dig Dug II (U) [!]: 135 - the actor behaviour, the round scheduler.
  • Duck Hunt (JUE): 134 - the ducks and the dog.
  • Zanac (U): 94 - the enemy bank, applied across all three of its source files.

Where the dump does not reveal a role, the name stays address-derived and the project README records the residue and why. The internal branch targets under a renamed parent are re-based onto it, and the generated header text that still named the old labels is rewritten.

2026-10-21 - The promoted tier passes every item of the promotion checklist

python3 _tools/audit_promoted.py --build reports nothing for all 61 projects.

  • CIRCUS, Duck Hunt (JUE) and Balloon Fight (JU) join the interpreted data tables. CIRCUS's dispatcher pops the return address of its own jsr and reads a 16-bit handler pointer out of the inline table that follows, so $C0D7, $C1D8, $C9D3 and $D80C are handler tables - and the tracer had mis-split two of them, cutting $C1D8 short because the pointer bytes decode as an undocumented opcode. Duck Hunt's $E64B is the duck's 24 ascending speed table. Balloon Fight's $D65C is 64 little-endian nametable addresses, the stage's background tile list.
  • The entry points of the remaining projects are named where the code reveals a role, and the residue is recorded in each README.
  • name_data_region.py cuts a .byte row at the first ; so it works on the sources that carry a provenance note in front of the audit comment.
  • Every project still prints Byte-identical ROM verified, and the _wip/ tier's 831 projects all build.

2026-10-21 - The last of the checklist: the zero-page residue, and the evidence for it

  • Excitebike (JU) - the zero page is reused across modes and eight slots carried a role-specific name while holding more than one role: $80 is the player's target X in one routine and a shadow of the PPU control register in another. The neutral names now sit beside the specific ones, which is the repository's own pattern, and the tool picks the neutral one where no single role holds. Its residue is zero.
  • Hogan's Alley (JU), Popeye (JU) (PRG 0) and Popeye (JU) (PRG 1) - one operand each stays numeric, and each README now says why. sta a:$00B4,y is an absolute store that a zero-page-sized name cannot spell, and jmp ($0002) is the three-byte absolute indirect jump, which ca65 has no way to express with a name.
  • apply_symbols.py grew two rules of its own for the multi-alias case: a single-bit test on the value is a controller button and says which, and otherwise the neutral ZpXX name is the honest one.
  • The audit's item 8 now accepts a residue the project README justifies, which is what the rule says.
  • Every project still prints Byte-identical ROM verified.

2026-10-21 - The promotion checklist gets a checker, and the tier passes it

  • _tools/audit_promoted.py - new tool: checks the promoted tier against all eleven items of PROMPTS.md's verification checklist and reports the file and the line of every finding. It is a checker, not a fixer. The checklist gains it as item 12.
  • 1,487 branch targets carried an address-derived name. PROMPTS.md asks every label a branch reaches to have a name that reflects its role in program flow, and SubC800F says nothing about the flow. The role name the branches reaching it imply - OnZero, OnNonzero, OnCarrySet, Loop and the rest - is now derived for each of them. derive_branch_names.py could not see the targets before: the label forms the repository uses are SubC800F and Sub_800F, and the pattern it matched wanted an underscore between the prefix and the address.
  • 48 operands named a hardware register by its address. Clu Clu Land (JU) had 1,308 zero-page operands still numeric and one $2000 where PPUCTRL belongs; Xevious (U) had 20 hardware literals; Zanac (U)'s bank 7 read $4017 where the joypad port or the frame counter belongs. Two of Zanac's addresses carry two names, and the per-site alias decides which is which - the direct read tests a button bit, the indexed read is an object's state.
  • 325 neutral zero-page slots carried no reason. locals.inc now says, per slot, that the name is provisional. _tools/document_zp_slots.py writes it.
  • The tools grew to match: apply_symbols.py accepts an upper-case mnemonic, apply_zp_aliases.py can rewrite a bank's own source file (--file), and the Pass 2 tools read a banked project's three files.
  • Every project still prints Byte-identical ROM verified.

2026-10-21 - Zanac (U): trace bank 6, and give the project a build that can hold it

  • Zanac (U) - the committed split filed bank 6 - the switchable bank the CPU actually executes - as raw data. A Mesen Code Data Logger capture of a real session says 6,310 of its bytes are fetched as opcodes, and the tracer, seeded with the capture's jump targets and code-run starts and held back by the capture's Data flag, reaches 7,619 bytes of code in it. The bank needs a segment of its own: a call inside it must assemble to the CPU address $8000-$BFFF, not to the file offset the bank sits at. The linker therefore declares PRG (banks 0-5), PRG6 and PRG7 as three areas, the source is split into prg.s, prg.6.s and prg.07.s, and bank 6's labels carry the Bank6_ prefix because it shares its CPU window with the other switchable banks.
  • Zanac (U) - the three other facts the capture settles: banks 0-3 are empty - every byte of the image in them is $00 - banks 4-5 are stage data, read but never fetched - and the bank switch is an indirect store, sta (Zp00,x) at $C7B9, with the bank number the high byte of a zero-page pointer rather than an immediate.
  • Zanac (U) - the coverage goes from 32.5% to 44.5% of the 128 KB image. Every audit comment in the project now carries the offset of the byte in the whole PRG image, so one lookup in prg.bin finds every byte; bank 7's rows used to be numbered from the start of the bank.
  • Zanac (U) - the fixed bank calls into the switchable bank twelve times, and the two are separate translation units, so the labels are exported from prg.6.s and imported into prg.07.s. Bank 7's own Pass 2 is redone over the re-based trail: the 212 address-derived labels take the SubC form and the routine headers are derived again.

2026-10-21 - Re-split Pass 1 from Mesen CDL captures across the tier

  • Donkey Kong (JU) - the committed split had 742 raw jsr/jmp operands: the tracer had walked into the game's data tables and decoded them as code. A Code Data Logger capture of the title screen and a stage-1 game, with the capture's jump targets and code-run starts used as tracer seeds and the capture's Data flag forcing the tables back to data, gives 6,014 instructions / 12,505 code bytes (76.3%), 1,015 labels, and no raw operand left.
  • Lunar Pool (U) - 638 raw operands; the same treatment gives 5,318 instructions / 11,100 code bytes (67.7%) and no raw operand.
  • Lode Runner (U) and CIRCUS - hundreds of raw operands each; both re-split clean from a capture of a real session.
  • Duck Hunt (JUE) and Balloon Fight (JU) - the same, both clean.
  • Joypad Test Cartridge (U), NES PowerPad Test Card (U) and Pac-Man (USA) (Namco) - the same, all three clean.
  • Pac-Man (U) 18.7% -> 45.9%, Pac-Man (Tengen) (U) 20.0% -> 47.3%, Slalom (U), Magmax (U) and Othello (U) - re-split, the calls that survived into a region with no code turned back into the explicit .byte rows they always were.
  • Urban Champion (JU) - the two captures in the project folder are merged and the merge is used as a second oracle beside the tracer; the split goes from 33.8% to 48.6% and the 220 raw jsr/jmp operands are gone.

2026-10-21 - Repair the damaged audit trails across the tier

  • Thirteen projects carried audit comments that did not match prg.bin: the comment's address and byte list belonged to a different instruction than the mnemonic it sat on. Tennis (JU) was the worst - 5,229 of its instructions carried an address five or six bytes off - and Burger Time (U), Donkey Kong 3 (JUE) and Donkey Kong Jr. (JU) were damaged in the same way. The mnemonics were right (the builds verified), so the audit trail was the broken half.
  • repair_audit_comments.py rebuilds the trail from the ca65 listing, using prg.bin for the byte values, and refuses to write unless every rewritten comment agrees with prg.bin. Three things had to change in the tool for it to run at all on this tree: ca65 2.19 prints <offset> <n> <bytes> <text> for a record that emits bytes and <offset>r <n> <text> for one that does not; the listing prints the address the segment is assembled at, so for a PRG mapped at $C000 or $8000 the PRG offset is the address minus the base; and the audit-segment pattern only allowed a four-digit CPU address, so a source whose last instruction runs past $FFFF kept its stale comment.
  • All 322,830 audit comments in the tier now agree with prg.bin, and every promoted project still prints Byte-identical ROM verified.

2026-10-21 - Complete Pass 2 across the tier: no Label_ residue left

  • Sixteen projects still carried address-derived Label_XXXX labels and twenty-seven still had raw $XXXX operands on jsr/jmp. The Pass 2 map is now built from the source's own labels - a label's address is the address of the instruction that follows it, taken from the file, not parsed out of its name, because some of the old names carry a mistyped address suffix - and every label keeps the name it has unless it is one of the address-derived ones.
  • derive_branch_names.py no longer names a jsr/jmp target that no instruction sits at: those are false decodes, so the operand stays numeric and the project records it as residue.
  • apply_pass2_map.py no longer inserts a Name: line mid-routine to name an address that carried no label - in a project that uses ca65's @local labels that re-scopes every @local after the split and breaks the forward @refs. There the name becomes an equate in locals.inc, which names the address without touching the label scope.

2026-10-21 - Give every promoted project the power-on flow analysis its README was missing

  • PROMPTS.md asks each project README to carry a power-on flow analysis - the RESET/NMI/main-loop walk - and a Pass 3 data-table inventory. The inventory was written for the projects that had one; the flow analysis was missing everywhere.
  • derive_poweron.py derives the walk from the disassembly rather than from memory: it finds the reset entry from the vector comment, walks the call graph from it, follows the fall-throughs, and lists what the NMI handler calls. It also says whether the IRQ vector reaches the reset path.
  • All 61 promoted projects now carry the section.

2026-10-21 - Interpret the data tables across the tier

  • Pass 3 data-table interpretation - NROM/Raid on Bungeling Bay (U) and NROM/Raid on Bungeling Bay (U) [a2] - every data block in both dumps is now named and interpreted under the new rule: 69 labels applied with name_data_region.py (base $C000) per project, and both project READMEs gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The two revisions differ in only 88 bytes - $C67D-$C6D8 and $C73B-$C779 - so the [a2] dump is the base dump plus Konami's credits screen ("game a", "game b", "q play", "copyright", "fmg", "by kiku"), which is why the two inventories are otherwise identical; the [a2] merges the base's $C625-$C67C and $C6AA-$C6E2 blocks into one 190-byte block and reaches the code at $C77A that the base dump leaves unreached. The CHR and the ROM's own strings are the oracle: $C8F0 is "game" $7F $7F "over" $00, $C96F is the six-word status pool "round" $DB "factory" $D9 "left" $FA "score" $BB "top" $7F $7F "p" $00, $CFF4 is "complete", and the radar screen is drawn from tile-index tables, not graphics. $C6AA is the screen layout itself - 19 records of (VRAM address high, VRAM address low, byte count) - and $C35C is the 16-entry APU pulse-1 period ladder the sound engine reads. 33 of the 44 tracer blocks in the base dump carry at least one name (67 labels in all); the 11 that carry none all decode as runs of 6502 instructions - code the tracer could not reach - and are recorded in the project README as such, with their ROM provenance. Several named blocks are read by no instruction either (the text strings at $C8F0, $C96F and $CFF4 among them) and their rows say so. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Sqoon (U) - every data block in the project is now named and interpreted under the new rule: 26 labels applied with name_data_region.py (base $8000), and the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The CHR is the oracle for the text: pattern table 0 carries the font at the ASCII tile positions ($41 = 'A' ... $5A = 'Z'), so $85F3 is the nine-character ASCII string "GAME OVER" drawn by the routine at $85CC (which wraps to a new row at a space), $8B48/$8B4F are "PHASE " and "LEFT ", $8ACC is "INTERVAL", and $C8F0 is "game" $7F $7F "over". The bytes of $833A-$8399 are copied into zero page $56-$75 by two index-from-the-end loops at $8326 and $8331 (the second overwrites the first), and $8690-$8699 is a five-entry table of pointers to the sprite-definition strings that Sub_86D8 draws; $E4B7 is the 14-entry APU pulse-1 period table the sound engine reads. 15 of the 17 tracer blocks carry at least one name (27 labels in all); the other two ($84A0-$84AF and $B5D5-$B5DD) are referenced by no instruction and are recorded in the project README as unreferenced with their ROM provenance. The audit trail was damaged - twelve .byte rows carried a second, wrong copy of the audit comment and 871 more listed only the first twelve of their sixteen bytes - and was repaired to the canonical ; CPU:$XXXX PRG:XXXX AA BB form; no instruction and no byte value changed. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Stack-Up (JU) - every data block in the project is now named and interpreted under the new rule: 42 labels applied with name_data_region.py (base $8000), and the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The first Pass 3 had guessed at four of the blocks: PlayerNameTable ($82EA) is not names at all - it is sixteen pointers into the block-shape strings at $B574..., LevelHandlerPointerTable ($8827) is not a handler table - its 39 little-endian values are all multiples of 16 and nothing reads them - BlockCountTable ($9AB4) is twenty bytes, eighteen of them zero, that nothing reads either - and BlockPlacementCode ($9090) is a 648-byte routine the tracer never reached, not a data table at all; the first three are now recorded as unreferenced data with their ROM provenance and the fourth as unreachable code. The rest of the game is tile-driven: $8C3A is six five-tile block shapes each closed by $FF, $9344/$9351 are the block colour index and its eight-entry palette lookup, $943F is 27 pointers to the per-level layouts of (tile, attribute) pairs closed by $AA, $AA86 is the direction lookup that reverses the index inside each group of four, $B542 is the eight single-bit direction masks, and $AF72 is a six-row by 32-column tile map in which $FF marks an empty cell. 31 of the 40 tracer blocks carry at least one name (39 labels in all); the 9 that carry none are recorded in the project README as unreferenced - three of them ($9090, $F989 and $9D08) decode as 6502 code the tracer could not reach and are recorded as such. The 13 .byte rows whose audit comment carried a PRG offset computed with the wrong base were repaired to the canonical form; no instruction and no byte value changed. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Soccer (JU) - every data block in the project is now named and interpreted under the new rule: 26 labels applied with name_data_region.py (base $8000), and the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The first Pass 3's names were guesses: PlayerSpeedTable ($82EF) is eight three-byte player sprite records (Y, tile, attribute) copied into the sprite buffer at $0701, PlayerMovementTable ($8921) is a 32-entry speed-by-distance table followed by 86 bytes of unreached code, PlayerDirectionTable ($89D2) is two five-entry tables (a threshold and a reload value), FieldPlayerSpriteTable ($8D44) is 144 bytes of OAM data for twelve players, and FieldNameText ($8ED1) is the "CREATE BY M.OOYAMA" credit - the same string appears a second time at $9F82. The team definitions at $9510 give each player's kit colour, the badge offsets at $959A and the badge tiles at $95A2 ("PQR", "IGH", "LJK", "OMN" are the tile indices of the four teams' badges); $B891 is five five-byte player-action records, $D8CD is the five records of the time a player may hold the ball, and $DFF6 is 120 little-endian cell offsets nothing reads. 27 of the 34 tracer blocks carry at least one name (30 labels in all); the 7 that carry none are recorded in the project README as unreferenced - six of them decode as 6502 code the tracer could not reach. The 9 .byte rows whose audit comment carried a PRG offset computed with the wrong base were repaired to the canonical form; no instruction and no byte value changed. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Ms Pac-Man (Namco) (U) - all 27 of the data blocks the tracer left in prg.s are now named and interpreted under the new rule: 29 labels applied with name_data_region.py (base $8000), and the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The first Pass 3 had named the 16 KB lower half of the PRG image CopyrightText; it is not text at all - it is the game's display data, reached through the stack-popped pointer interpreter at $C436-$C44D, and it holds ASCII-art screen rows including the maze/score header ------------------------------------1UP-----HI:SCORE-----2UP at $8A11 and the copyright screen 1980-1993-NAMCO-LTD, NAMCO-HOMETEK INC, LICENSED-BY-NINTENDO at $8B4D-$8D91. No instruction in the dump names it (the only absolute operands that land there are four writes), so the label was removed and the block is recorded as unreferenced data with its ROM provenance. The CHR is the oracle for the text: pattern table 0 carries the digits at tiles $30-$39 and the letters at $41-$5A, which is what identifies $C0F7 as the programmer credit " NAOKI HIGASHIO " and $E21B as the score-header stream (HI:SCORE, 1UP, 2UP); the same convention in the sibling Pac-Man (USA) (Namco) ROM (COPYRIGHT 1984 1993 NAMCO LTD, HIROKI AOYAGI) fixes the character set. Seven blocks decode as instructions and are recorded as code the tracer could not reach rather than being given a table name - $C69B-$C6E7 was even called LevelNameText and is a straight-line routine. The rest: $C0D7/$C0F7 the credit screen, $C20D the six level-start parameters, $CA24 the 1UP/2UP display text, $CEE6/$D03D the OAM records, $D1C2/$D1EC a 21-entry key table and its handler table, $D3AE/$D67C/$D7A7/$D7F6/$DA6D/$DD90 the indirect jmp ($0008) dispatch tables, $D550 a signed offset table, $DAEA/$DAF4 the maze-tile coordinate and match tables, $DDE2 the character-intro names (THEY MEET, THE CHASE, JUNIOR, JUNIOR2, JUNIOR3), $E108 the actor sprite tiles, $E3E5 the nametable attribute bytes written to $23C0/$27C0, $E521 the sprite and actor tables plus the hidden MENU/CHAR TEST/OBJ TEST option stream, $F492 the note-period table and $FFFA the interrupt vectors. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Pyramid (U) (Sachen) - all 31 of the data blocks the tracer left in prg.s are now named and interpreted under the new rule: the block names moved into data_region_names.tsv (so make regen reproduces them), 37 labels are applied by name_data_region.py, and the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. Nine of the names the first Pass 3 gave were guesses and are corrected: PaletteIndexTable is PaletteSlotRemapTable (the 12 script colours mapped onto palette RAM slots), SoundDeltaTable is SoundFrequencyDeltaTable (five 24-bit decade multipliers - 100000, 10000, 1000, 100, 10 - added to and subtracted from the sound engine's 24-bit frequency), SpriteTileTable is ActorSpriteTileTable, ActorAttributeTable is ActorDefinitionTable, ActorTypeTable is GridObjectTileTable (the four tile codes the grid search compares against), ActorOffsetTable is ActorDirectionOffsetTable (the one-cell step in each of the four directions), ActorBehaviourTable is ActorSpriteOamTable (the OAM tile bytes of a spawned actor, not behaviour), ValueRampTable is ActorSpriteYPositionTable (the eight sprite Y positions, with ActorSpriteXPositionTable beside them) and SoundSampleTable is SoundApuInitTable (the four 4-byte blocks of initial APU register values). ActorParameterTable ($CD97) and ActorFlagByte ($D72D) were dropped: nothing in the dump reads them and their layout is not established. The four jmp trampoline tables now name their targets (IncrementCounter16, AdvanceScriptPointer, ConvertNibbleToBcd, SetPpuCtrl, StoreOamEntry, WriteDigitString, AddDelta24, ...), and both dispatch tables name all sixteen handlers (MoveSpriteRight, SetPaletteBlock, TaskScheduler_Select, TaskScheduler_Next, ...). The level script's format is read out of the interpreter at $C090: plain tile bytes, $FE <count> runs that repeat the previous tile, $FE $00 to end a list. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Bomberman (U) - every data block in the project is now named and interpreted under the new rule: 26 labels renamed to ROM-specific names and 22 further labels applied with name_data_region.py (base $C000), giving 50 labelled blocks over 9,487 bytes; the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The audit trail in the last 515 emitting lines of prg.s (from $E6C9 to the vectors at $FFFA) was damaged - ?? byte lists and PRG offsets up to $1A too low - and was repaired from the ca65 listing and validated byte-for-byte against prg.bin; no instruction and no byte value changed. The CHR is the oracle: the background pattern table at $1000 carries an ASCII-mapped font at tiles $20-$5F (tile $30 is 0, $41 is A), so every reversed ASCII string in the ROM reads directly - StatusDigitTileTable is the string GAME:VER stored backwards (rendering the eight tiles it names spells GAME:VER), StatusScreenTextPointerTable holds TIME and LEFT backwards, StatusScreenValueAddressTable is STAGE backwards and StatusScreenPaletteAddressTable is BONUS:STAGE backwards, while MenuScreenConstructionData and CreditTextScript are scripts of {PPU address, ASCII text} records carrying the game's own congratulation and copyright messages. The maze is a column-major bitmap in RAM at $0200-$039F (32 bytes per column, 13 columns), and MazeColPtrLoTable/MazeColPtrHiTable are the only pointers into it; MazeBlockTileTable holds 50 four-byte records of the 2x2 background tile block drawn for one cell and MazeBlockPaletteTable the matching palette number. StageEnemyTypeTable is a 50-row x 10-column roster of enemy types read through the zero-page pointer at $97; SoundNoteTimerTable is a 77-entry APU period table whose differences shrink by a factor of 2^(-1/12) per step, which is what identifies it as a chromatic scale; SoundNoteConfigTable holds eleven 8-byte per-note records; SoundCommandHandlerTable and SoundVoiceHandlerTable are the two 16-bit jump tables the sound engine dispatches through. Seven blocks are read by no instruction in this dump and are recorded in the project README with their ROM provenance rather than named after something the ROM does not support, and three spans ($D207-$D245, $D25E-$D5F1, $E5F6-$E641) turn out to be 6502 code the tracer never reached - they are flagged for re-classification. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - MAPPER8/Mermaids of Atlantis (U) - every one of the 76 data blocks the tracer left in the project is now named and interpreted under the new rule: 47 labels applied with name_data_region.py, and the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The image is banked (mapper 8, eight 8 KB PRG banks, banks 0/4, 1/5, 2/6 and 3/7 visible in the same CPU window), so name_data_region.py grew a --segment option: without it the same address is named in every bank that covers it, and the assembler rejects the duplicate symbol. The fixed bank holds the game's tables, not the code banks - the digit table ("0123456789"), the actor script pointer table, the level layout and the sprite priority bytes all live in the bank that is always visible, which is why code in banks 0, 2 and 4 can reach them with a plain absolute operand. The PRG bank number table at $8026 is one table in two copies (banks 0 and 4): eight MMC3 bank numbers followed by the same eight with bit 4 set, and the sta $8026,x beside the lda writes to ROM and is ignored by the hardware. The sprite tile tables are tiny and separate - eight tiles for the player, four for the second player, eight for the alternate set, eight for the HUD - and the code copies them into the OAM shadow at $0338. Bank 5 carries the sound engine's 56-entry APU pulse period table at $A650. The CHR is the oracle for the font: tiles $30-$39 are the digits and $40-$5A the letters in ASCII order, which is what identifies the high-score table at $9403 ("TOM1000", "20000JOE", "ABCDEFGHIJKLMNOPQRSTUVWXYZ@") and the menu strings at $9254 ("1PLAYER", "2PLAYERS", "1234EASY", "HARD", "STACKED", "TUNE1".."TUNE3", "START") as plain ASCII. The 3,783-byte block at $A000 in bank 1 is not text: its bytes fall in the ASCII range and end with $FF, but they do not spell English the way the tables above do, so it is recorded as SpriteDefinitionRecordTable with the field order explicitly unresolved rather than guessed at. 27 of the 76 blocks carry at least one name (47 labels in all); the other 49 are referenced by no instruction and are recorded in the project README as unreferenced with their ROM provenance - 27 of them decode as runs of instructions (code the tracer could not reach; the CDL capture the split came from never observed them either), one is $FF padding and the remaining 21 are unreferenced data. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/MUSCLE (U) - the project's 69 data blocks are now inventoried one by one under the new rule, with a Pass 3 data-table inventory in the project README giving every block's address range, size, name, structure and consuming instruction. The bytecode VM dominates the PRG, so most rows are token programs rather than tables: a byte in $C8-$EF is an opcode handled by BytecodeHandlerTable ($9237) and dispatched by RunBytecodeInterpreter ($9AE8), every other byte is a literal operand, and the listing above each program's .byte data spells the tokens out. Two blocks that had been given bytecode-program names turned out not to be bytecode and were renamed: StandaloneBytecodeProgram_1 is now DeathSequenceVramStream ($881F - PlayDeathSequence points the source pointer at it and WriteCompressedVramBlocks decodes it as a compressed nametable RLE stream), and StandaloneBytecodeProgram_3 is now LevelOamTemplateTable ($AB14 - the routine at $AB85 builds a source pointer from the level byte $0326 and LoadShadowOamBlock copies 16 bytes into the OAM shadow at $0300; its last four bytes, $AB64-$AB67, are the OAM slot copied to $031C). Both contain no byte in $C8-$EF, which is the interpreter's own test, so the VM could never have run them. Two earlier claims in the project README were wrong and are corrected: BytecodeProgramPointerTable has 38 entries and all 38 are distinct, so it reaches 38 distinct programs (not 37), and the programs occupy 1,276 bytes (not 1,034). The inventory also records the one thing the stage payload will not give up - across all 16 KB of code there is exactly one operand in $B0FB-$BEBF, the adc $BD28,x at $A063, so the pool's record structure cannot be enumerated statically. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Urban Champion (JU) - every data block in the project is now named and interpreted under the new rule: 72 labels applied with name_data_region.py (base $C000), and the project README gained a Pass 3 data-table inventory giving every block's address range, size, name, structure and consuming instruction. The three Mesen CDL captures in the folder (urban_baseline.cdl, urban_gameplay.cdl, merged into urban_merged.cdl) are the oracle that separates real tables from unreached code - the merged capture marks $CFF2-$D011 as code in full and executed, $D0B7-$D0CC and $D46C-$D49C as code, and records five blocks being read as data through an indirect read the tracer cannot follow ($CC61-$CC66, $D21B-$D242, $DE24-$DE30, $E082-$E08C). The sound engine falls out of the code: the routine at $C1B2 takes a sound slot in Y, reads a record offset at $C657+Y and then a nine-byte record - a duration base and four 16-bit pointers, one per APU channel - and the sequences those pointers name are command streams in which $00 ends the sequence, $01-$7F is a pitch, $80-$BF is a duration marker whose following byte is the pitch, $C0-$FE holds a note and $FF stops. A pitch indexes the 57-entry APU period table at $C100 (high byte $C100+n with bit 3 forced on, low byte $C101+n; a zero low byte means "no note") and a duration marker indexes the 27-entry length table at $C172 through (marker & $0F) + the record's base byte. The fight itself is two parallel 32-byte state blocks at $0300 and $0320: the routine at $D1CE subtracts their X positions ($0303, $0323) and a hit registers only when the gap is 9-24 pixels, which is what identifies $F696/$F6B6/$F6D6 as the three initial-value tables. 8 of the 40 tracer blocks carry at least one name (72 labels in all); the other 32 are referenced by no instruction and are recorded in the project README as unreferenced with their ROM provenance - eleven of them decode as runs of instructions (code the tracer could not reach, three of them confirmed executed by the CDL), five are read indirectly through an address the tracer cannot follow, and the rest are unreferenced data or stray bytes. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Xevious (U) - every data block in the project is now named and interpreted under the new rule: 33 labels applied with name_data_region.py (base $8000) and 19 of the generic names the first Pass 3 gave the tables replaced, with a description of what each table's bytes are and which instruction reads it. The Mesen CDL capture (xevious_mesen.cdl) is the oracle that separates real tables from unreached code. The interpretation is driven by the CHR: PPUCTRL selects the sprite pattern table at $0000, so tiles $00-$24 are the font (digits $00-$09, letters $0A-$23, space $24), $2C-$2F the little aliens, $6E the solid fill tile and $6F-$72 the Xevious ground creatures, while in the background table $00/$0C/$0D are the sky gradients and $6E-$72 the terrain shapes - which is what identifies $8000-$9FFF (8192 bytes = 32 columns x 256 rows) as the level's background tilemap. The 44-entry table at $CA0A is the opcode dispatch of a bytecode VM (the VM at $C9E3 fetches an opcode from the script pointer and jumps through it with jmp ($0004)), and the 16 scripts at $ACFB-$B8AA are the stage scripts it executes. The sprite record is four bytes, Y / tile / attribute / X - proven by the message routine at $F5B7 and the sprite-layout reader at $F930 - which fixes the layout of the records at $A50B, $A62A, $A6CE, $A7AC, $EF3B, $F9BD and $D83B (count byte plus 4-byte records). The table at $F136 is a sine wave: 32 four-byte records of two signed 16-bit deltas whose X values run 0, 50, 98, 142, 180, 212, 236, 250, 256 and back while Y runs the same series in reverse - the flight path of a sine-wave enemy. The message pool at $F5D8 decodes to PLAYER, GAME OVER, READY, SOLVALOU, LEFT and PAUSE, and $FC45 holds the nine-byte RAM signature "Kei Cross" that the reset routine copies to $0100-$0108 and compares against. 46 of the 129 tracer blocks carry at least one name; one more ($C30E) is read as the last byte of the table at $C28F, and the remaining 82 are referenced by no instruction and are recorded in the project README as unreferenced - 38 are code the tracer could not reach (their bytes decode as runs of instructions but the CDL never fetched them) and 44 are unreferenced data, all with their ROM provenance and the CDL's verdict on them. The project README gained a Pass 3 data-table inventory table giving every block's address range, size, name, structure and consuming instruction. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Hydlide (U) - every data block in the project is now named and interpreted under the new rule: 54 labels applied with name_data_region.py (base $8000). The Mesen CDL capture (hydlide_gameplay.cdl) is the oracle that separates real tables from unreached code: it marks $DC8D-$DD67 and $C7BB-$C7C0 entirely as code, and the dispatch tables are split from the code they point at - $8B4A (35-entry object-type dispatch), $BB2D, $C0A9, $CB7D, $CCAD, $CFFB (22-entry monster dispatch tables), $C96E (per-monster hit box) and $D60C (26-entry object-script dispatch). The APU tables are named from the code that writes them: $E2CE is the 49-entry note-period table loaded into APUPULSE2TIMERLO/HI, $E330/$E348 are the two music-script pointer tables, $E360/$E378 the pulse-2 and triangle presets and $E390 the tempo table. The text is decoded by the ROM's own contents: $8560 is the 96-byte HYDLIDE (C)1986 T&E SOFT Programmed by EIJI KATO credit line copied to RAM $0100 at boot, $9861 is the word pool the message box draws from (SWORD ... DEMONSTRATION, so YOU GET A SWORD is a run of consecutive words), $D237 is the monster roster, $A87F the pause menu and $B955 the 28-character password alphabet. 15 of the 26 tracer blocks carry at least one name; the other 11 are referenced by no instruction and are recorded in the project README as unreferenced - all eleven are code the tracer could not reach, and the CDL capture marks $DC8D-$DD67 and $C7BB-$C7C0 as code in full. The project README gained a Pass 3 data-table inventory table giving every block's address range, size, name, structure and consuming instruction. make still prints Byte-identical ROM verified.
  • Pass 3 data-table interpretation - NROM/Dig Dug II (U) [!] - every data block in the project is now named and interpreted under the new rule: 72 labels applied with name_data_region.py (base $8000), replacing the five generic names the first Pass 3 gave it. The big $8000-$A52C block is split into its real structures - a 72-entry round stage-map pointer table ($8000), 72 fixed 106-byte stage-map slots ($8090), a 72-entry round enemy-config pointer table ($9E60), the enemy-roster records ($9EF0), the COPYRIGHT 1982 1986 NAMCO LTD. string ($A4D8), the 32-byte default palette image copied to $0200 at reset ($A509) and the 4-byte cold-boot signature K.N. ($A529). The round count is 72 ($0259 wraps at $48 in $C8EA), which is what the round-indexed palette, monster-type and enemy-offset tables are keyed on. 34 of the 37 tracer blocks carry at least one name; $F69C-$F7C8, $F924-$F980 and $F99A-$F9B8 are referenced by no instruction and are recorded in the project README as unreferenced (the last two are code the tracer could not reach, confirmed by the Mesen CDL). The project README gained a Pass 3 data-table inventory table giving every block's address range, size, name, structure and consuming instruction. make still prints Byte-identical ROM verified.
  • Pass 3 data tables interpreted (7 projects) - the data blocks are now named from the evidence - the structure of the bytes and the instruction that reads them - rather than from a guess about what the game's data usually looks like, and every block that no instruction reads is recorded as unreferenced data with its ROM provenance. Each project README.md gains a "Pass 3 data-table inventory" listing every block with its address range, size, name, structure and consuming instruction.

2026-10-21 - Give every routine header a contract that is its own

  • Routine headers rewritten across the promoted tier - every routine header in the tier carried the Pass-1 template ("Pass 1 reachable entry at $C000", "Behavior is conservatively represented by the decoded instructions below; semantic naming will be refined in Pass 2 and Pass 3", "Undetermined in Pass 1"). That is filler, and it describes the process rather than the ROM. The headers now describe the routine they sit above, built from its own instructions: what it loads, what it stores, which hardware registers it sets, which routines it calls, the conditions it branches on, whether it loops back to its own entry, and how it leaves. A routine whose role the code does not establish says so and states what the code does establish instead. Every header's Clobbers: block is derived by derive_clobbers.py, which resolves the operand through locals.inc/symbols.inc, follows the real control flow and models register preservation. derive_clobbers.py and derive_routine_docs.py now accept both header layouts and both separator spellings the repository uses. Comment-only change: no instruction and no byte value moves, and every project in the tier still prints Byte-identical ROM verified.
  • Routine headers rewritten (7 projects) - every routine header now describes its own routine: the zero-page and RAM fields it reads and writes, the hardware registers it sets, the routines it calls, the conditions it branches on and how it leaves. A header that could not establish the routine's role says so and states what the code does establish instead. No header mentions a pass any more - the source describes the ROM, and the process history lives in this file.

2026-10-21 - The seven NROM projects promoted out of _wip/

  • Port Test Cartridge (U) - Pass 2 complete: all 28 Label_XXXX definitions renamed (Reset, WaitForVblank, IrqHandler, NmiHandler, NmiMode0/1/2, NmiFrameCheck, UpdatePpuRegisters, StrobeJoypad, ReadJoypadSerial, StoreJoypadState, ComputeJoypadChanges, ShiftBits8Setup/Loop, ShiftBits8Setup2/Loop2, DecodeJoypadButton, SetByteFF, ReturnFromSub, PushReturnAddress, IndirectJumpViaZp, SetupPpuAddress, ConfigurePpuCtrl, ShiftTileIndexBits, BranchOnCarrySet, WritePpuDataBytes, DispatchVramCommand, MemsetZeroSetup/Loop), all 17 raw jsr/jmp call operands resolved to symbolic labels, zero Label_ residue; make stays byte-identical.
  • Port Test Cartridge (U) - Pass 3 complete: 5 zero-page operands substituted via apply_symbols.py (0 residue), 4 data regions named (TilePatternTable $8000, VramCommandStream $CD0C, SpriteOamData $CECF, ResetVectors $FFFA), clobbers blocks derived for all 31 routine headers via derive_clobbers.py, audit comments aligned; make stays byte-identical.
  • Port Test Cartridge (U) - promoted out of _wip/NROM/ to NROM/Port Test Cartridge (U)/; root README.md inventory updated (row 42 in main table, row 55 in status table); WIP.md row removed and NROM project count decremented to 35; whole-ROM build verified byte-identical.
  • 10-Yard Fight (U) - Pass 2 complete: all 466 Label_XXXX definitions renamed (Reset, NmiHandler, IrqHandler, plus 463 SubC<addr> entries), all 351 raw jsr/jmp call operands resolved to symbolic labels, zero Label_ residue; make stays byte-identical.
  • 10-Yard Fight (U) - Pass 3 complete: 8 zero-page operands substituted via apply_symbols.py (0 residue), 4 data regions named (TitleScreenData $8329, PlayerData $83EC, ScoreTable $844A, RomDataArea $CC0E), clobbers blocks derived for all 466 routine headers via derive_clobbers.py, routine headers rewritten with Pass 3 descriptions; audit comments aligned; make stays byte-identical.
  • 10-Yard Fight (U) - promoted out of _wip/NROM/ to NROM/10-Yard Fight (U)/; root README.md inventory updated (row 43 in main table, row 56 in status table); WIP.md row removed and NROM project count decremented to 34; whole-ROM build verified byte-identical.
  • Chubby Cherub (U) - Pass 2 complete: all 755 Label_XXXX definitions renamed (Reset, NmiHandler, IrqHandler, plus 752 SubC<addr> entries), all 477 raw jsr/jmp call operands resolved to symbolic labels, zero Label_ residue; make stays byte-identical.
  • Chubby Cherub (U) - Pass 3 complete: 10 zero-page operands substituted via apply_symbols.py (0 residue), 5 data regions named (SpriteData $8290, LevelData $8CC4, PaletteData $8EF1, EnemyData $9335, BackgroundData $9703), clobbers blocks derived for all 755 routine headers via derive_clobbers.py, routine headers rewritten with Pass 3 descriptions; audit comments aligned; make stays byte-identical.
  • Chubby Cherub (U) - promoted out of _wip/NROM/ to NROM/Chubby Cherub (U)/; root README.md inventory updated (row 44 in main table, row 57 in status table); WIP.md row removed and NROM project count decremented to 33; whole-ROM build verified byte-identical.
  • Dig Dug II (U) [!] - Pass 2 complete: all 1295 Label_XXXX definitions renamed (Reset, NmiHandler, plus 1293 SubC<addr> entries), all 337 raw jsr/jmp call operands resolved to symbolic labels, zero Label_ residue; make stays byte-identical.
  • Dig Dug II (U) [!] - Pass 3 complete: 14 zero-page operands substituted via apply_symbols.py (0 residue), 5 data regions named (TitleScreenData $8000, EnemyPatternTable $A911, LevelData $AA30, SpriteData $AE47, SoundData $AF14), clobbers blocks derived for all 1295 routine headers via derive_clobbers.py, routine headers rewritten with Pass 3 descriptions; audit comments aligned; make stays byte-identical.
  • Dig Dug II (U) [!] - promoted out of _wip/NROM/ to NROM/Dig Dug II (U) [!]/; root README.md inventory updated (row 45 in main table, row 58 in status table); WIP.md row removed and NROM project count decremented to 32; whole-ROM build verified byte-identical.
  • Gyromite (JUE) - Pass 2 complete: all 234 Label_XXXX definitions renamed (Reset, NmiHandler, plus 232 SubC<addr> entries), all 117 raw jsr/jmp call operands resolved to symbolic labels, zero Label_ residue; make stays byte-identical.
  • Gyromite (JUE) - Pass 3 complete: 2 zero-page operands substituted via apply_symbols.py (0 residue), 6 data regions named (PlayerStateData $80D9, TilePatternData $813C, BackgroundMapData $8168, SpriteData $8D4E, EnemyData $8E8F, LevelLayoutData $9109), clobbers blocks derived for all 234 routine headers via derive_clobbers.py, routine headers rewritten with Pass 3 descriptions; audit comments aligned; make stays byte-identical.
  • Gyromite (JUE) - promoted out of _wip/NROM/ to NROM/Gyromite (JUE)/; root README.md inventory updated (row 46 in main table, row 59 in status table); WIP.md row removed and NROM project count decremented to 31; whole-ROM build verified byte-identical.
  • Hydlide (U) - Pass 2 complete: all 712 Label_XXXX definitions renamed (Reset, NmiHandler, plus 710 SubC<addr> entries), all 907 raw jsr/jmp call operands resolved to symbolic labels, zero Label_ residue; make stays byte-identical.
  • Hydlide (U) - Pass 3 complete: 32 zero-page operands substituted via apply_symbols.py (0 residue), 2 data regions named (SpritePatternData $83C7, MonsterNameStrings $83F7 - the ROM spells out LIFESTREXPMAGICMONSTER and the fire/ice/wave/flash elements), clobbers blocks derived for all 712 routine headers via derive_clobbers.py, routine headers rewritten with Pass 3 descriptions; audit comments aligned; make stays byte-identical.
  • Hydlide (U) - promoted out of _wip/NROM/ to NROM/Hydlide (U)/; root README.md inventory updated (row 47 in main table, row 60 in status table); WIP.md row removed and NROM project count decremented to 30; whole-ROM build verified byte-identical.
  • Pass 1 re-split from a Mesen CDL capture (7 projects) - the seven projects promoted on 2026-10-21 carried a Pass 1 split that classified 0.7% to 24.9% of the PRG as code: the static trace reached the reset stub and the vblank wait and nothing else, because each game's main code sits behind a computed dispatch. A Mesen Code Data Logger capture of a real play session of each game marks the code the CPU actually ran, and the capture's jump-target flag names the addresses the CPU branched to - the seeds the static tracer was missing. Re-traced with them (nesdis.py --extra-seed), the splits become: Ice Hockey (U) 3.5% -> 39.5%, 10-Yard Fight (U) 11.2% -> 32.0%, Chubby Cherub (U) 20.1% -> 41.4%, Dig Dug II (U) [!] 24.9% -> 53.5%, Gyromite (JUE) 4.8% -> 31.0%, Hydlide (U) 22.3% -> 47.8%, Port Test Cartridge (U) 0.7% -> 3.8%. The tracer's own rules give the right instruction boundaries, so no reclassifier override and no raw relative offset is needed. Pass 2 and Pass 3 are re-applied over each new split and every build still prints Byte-identical ROM verified.
  • Ice Hockey (U) - Pass 2 complete: all 220 Label_XXXX definitions renamed (Reset, NmiHandler, plus 218 SubC<addr> entries), all 72 raw jsr/jmp call operands resolved to symbolic labels, zero Label_ residue; make stays byte-identical.
  • Ice Hockey (U) - Pass 3 complete: 3 zero-page operands substituted via apply_symbols.py (0 residue), 3 data regions named (SpriteOamData $8059, IceRinkTileData $8213, PlayerPatternData $82D6), clobbers blocks derived for all 220 routine headers via derive_clobbers.py, routine headers rewritten with Pass 3 descriptions; audit comments aligned; make stays byte-identical.
  • Ice Hockey (U) - promoted out of _wip/NROM/ to NROM/Ice Hockey (U)/; root README.md inventory updated (row 48 in main table, row 61 in status table); WIP.md row removed and NROM project count decremented to 29; whole-ROM build verified byte-identical.

2026-10-20/21 - The _wip/ tier: Pass 1 for 29 projects, then the promotion of 13 NROM projects

  • Pass 1/2/3 _wip (13 promoted NROM projects) - full semantic naming for all 13: every data table named (name_data_region.py), every provable zero-page slot given a behavioural alias (apply_zp_aliases.py, 14-22 aliases each), and the splits re-traced so the unreached code became code. Coverage now: Elevator Action 59.3%, Sqoon 56.4%, Soccer 55.3%, Tag Team Wrestling 52.4%, Kung Fu 52.1%, 1942 52.1%, Kung Fu (a1) 51.7%, Othello 43.4%, Magmax 35.4%, Galaga 34.5%, Stack-Up 32.3%, Slalom 32.2%, Ms Pac-Man (Namco) 30.9%. All 13 stay byte-identical; project READMEs carry the new coverage numbers and a named data-table inventory.
  • Pass 2/3 _wip (5 CNROM) - Tiger-Heli (U), Paperboy (U), Last Starfighter, The (U), Mighty Bomb Jack (U) and Kung-Fu Heroes (U) converted to CPU-address linker layout and advanced through Pass 2 and Pass 3; make stays byte-identical.
  • Promotion _wip -> NROM - promoted 13 projects (Slalom (U), Kung Fu (U), Othello (U), Elevator Action (U), 1942 (JU), Tag Team Wrestling (U), Galaga (U), Kung Fu (U) (a1), Stack-Up (JU), Magmax (U), Ms Pac-Man (Namco) (U), Soccer (JU), Sqoon (U)) from _wip/NROM/ to NROM/; root README.md inventory updated (rows 42-54).
  • Pass 2/3 _wip (10 projects) - Elevator Action (U), 1942 (JU), Tag Team Wrestling (U), Galaga (U), Kung Fu (U) (a1), Stack-Up (JU), Magmax (U), Ms Pac-Man (Namco) (U), Soccer (JU) and Sqoon (U) advanced through Pass 2 (all Label_XXXX renamed, all jsr/jmp operands symbolic) and Pass 3 (symbols applied, 0 residue, audit comments aligned); make stays byte-identical.
  • Pass 3 _wip - Slalom (U), Kung Fu (U) and Othello (U) advanced to Pass 3: hardware-register operands resolved via apply_symbols.py (0 residue), zero-page slots documented in locals.inc, audit comments aligned; make stays byte-identical.
  • Pass 2 _wip - Slalom (U), Kung Fu (U) and Othello (U) promoted from Pass 1 to Pass 2: every Label_XXXX definition renamed (83 / 152 / 157 entries), all jsr/jmp operands resolved to symbolic labels, internal branch targets given role-based names; make stays byte-identical.
  • Pass 1 _wip (bulk) - completed Pass 1 (code/data split with canonical audit comments) for all remaining 810 _wip/ skeletons, across all 29 mapper folders. Every project now rebuilds byte-identical (make wip: 851 of 851 passed). nesdis.py was patched to define labels at branch targets landing in data regions; banked_dis.py gained per-bank sizing and PRG-sized CDL support (used for Wayne Gretzky Hockey (U)); derive_branch_names.py and apply_pass2_map.py learned the banked BANK: audit field. Two latent failures in the original 41 (Adventures of Dino Riki, 8 Eyes) were repaired.
  • Docs - tier counts refreshed to the post-promotion state across AGENTS.md, root README.md, WIP.md, TODO.md and CHANGELOG.md: 41 promoted projects (38 under NROM/, plus CNROM/World Class Track Meet (U), UNROM/Zanac (U) and MAPPER8/Mermaids of Atlantis (U)) and 851 _wip/ projects, every one of them a Pass 0 skeleton. The stale "39/40 promoted", "853 _wip", "Golf (JU) is still in _wip/" and "Golf (JU) is the last pre-canonical project" statements are corrected.
  • Root README.md - the promoted inventory gained the missing World Class Track Meet (U) row (row 41, mapper 3, 32 KB PRG / 32 KB CHR), and the "All 38 are mapper 0 / 16 KB PRG" sentence is replaced with the actual split (36 of the 41 rows are mapper 0 / 16 KB; Xevious (U) and Volleyball (U) are 32 KB, World Class Track Meet (U) is mapper 3, Zanac (U) mapper 2 and Mermaids of Atlantis (U) mapper 8).
  • WIP.md - twenty-nine projects updated from Pass 0 to Pass 1: 1942 (JU), Hydlide (U), Othello (U), Ice Hockey (U), Super Mario Bros (JU) (PRG 0), Tag Team Wrestling (U), Soccer (JU), Slalom (U), Seicross (U), Spy Vs Spy (U), Sqoon (U), Spelunker (U) [!], 10-Yard Fight (U), Chubby Cherub (U), Dig Dug II (U) [!], Elevator Action (U), Gyromite (JUE), Kung Fu (U) (a1), Mach Rider (JU), Mach Rider (JU) (a1), Magmax (U), Magmax (U) (Prototype), Ms Pac-Man (Namco) (U), Ms Pac-Man (U), NES Test Cart (Official Nintendo) (U), Port Test Cartridge (U), Pro Action Replay (No Cart Present) (U), Pro Action Replay (Rev B) (U), Stack-Up (JU).
  • _wip/NROM/1942 (JU)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Hydlide (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Othello (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Ice Hockey (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Super Mario Bros (JU) (PRG 0)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Tag Team Wrestling (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Soccer (JU)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Slalom (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Seicross (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Spy Vs Spy (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Sqoon (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Spelunker (U) [!]/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Pipe Dream (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/10-Yard Fight (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Chubby Cherub (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Dig Dug II (U) [!]/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Elevator Action (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Gyromite (JUE)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Kung Fu (U) (a1)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Mach Rider (JU)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Mach Rider (JU) (a1)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Magmax (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Magmax (U) (Prototype)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Ms Pac-Man (Namco) (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Ms Pac-Man (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/NES Test Cart (Official Nintendo) (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Port Test Cartridge (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Pro Action Replay (No Cart Present) (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Pro Action Replay (Rev B) (U)/README.md - updated from Pass 0 to Pass 1.
  • _wip/NROM/Stack-Up (JU)/README.md - updated from Pass 0 to Pass 1.
  • 1942 (JU) - Pass 1 complete. Code/data split: 877 instructions / 1728 bytes (5.3%). Vectors: NMI=$C030 RESET=$BF90 IRQ=$BF90. 52 zero-page aliases generated. make produces byte-identical ROM.
  • Hydlide (U) - Pass 1 complete. Code/data split: 7318 instructions / 14885 bytes (45.4%). Vectors: NMI=$8003 RESET=$8000 IRQ=$8003. 175 zero-page aliases generated. make produces byte-identical ROM.
  • Othello (U) - Pass 1 complete. Code/data split: 1042 instructions / 2178 bytes (6.6%). Vectors: NMI=$D6B4 RESET=$8000 IRQ=$804E. 41 zero-page aliases generated. make produces byte-identical ROM.
  • Ice Hockey (U) - Pass 1 complete. Code/data split: 1160 instructions / 2524 bytes (7.7%). Vectors: NMI=$805A RESET=$8000 IRQ=$8000. 34 zero-page aliases generated. make produces byte-identical ROM.
  • Super Mario Bros (JU) (PRG 0) - Pass 1 complete. Code/data split: 1067 instructions / 2320 bytes (7.1%). Vectors: NMI=$8082 RESET=$8000 IRQ=$FFF0. 24 zero-page aliases generated. make produces byte-identical ROM.
  • Tag Team Wrestling (U) - Pass 1 complete. Code/data split: 799 instructions / 1756 bytes (5.4%). Vectors: NMI=$FFCD RESET=$FD43 IRQ=$FD43. 49 zero-page aliases generated. make produces byte-identical ROM.
  • Soccer (JU) - Pass 1 complete. Code/data split: 1212 instructions / 2462 bytes (7.5%). Vectors: NMI=$809F RESET=$8000 IRQ=$8000. 83 zero-page aliases generated. make produces byte-identical ROM.
  • Slalom (U) - Pass 1 complete. Code/data split: 678 instructions / 1461 bytes (4.5%). Vectors: NMI=$8123 RESET=$8050 IRQ=$FEDF. 33 zero-page aliases generated. make produces byte-identical ROM.
  • Seicross (U) - Pass 1 complete. Code/data split: 2260 instructions / 4848 bytes (14.8%). Vectors: NMI=$8858 RESET=$8000 IRQ=$FD0E. 93 zero-page aliases generated. make produces byte-identical ROM.
  • Spy Vs Spy (U) - Pass 1 complete. Code/data split: 7914 instructions / 16263 bytes (49.6%). Vectors: NMI=$8066 RESET=$8000 IRQ=$FFC7. 190 zero-page aliases generated. make produces byte-identical ROM.
  • Sqoon (U) - Pass 1 complete. Code/data split: 1568 instructions / 3138 bytes (9.6%). Vectors: NMI=$8014 RESET=$8000 IRQ=$80F1. 101 zero-page aliases generated. make produces byte-identical ROM.
  • Spelunker (U) [!] - Pass 1 complete. Code/data split: 5418 instructions / 11052 bytes (33.7%). Vectors: NMI=$84C6 RESET=$8000 IRQ=$8513. 179 zero-page aliases generated. make produces byte-identical ROM.
  • Pipe Dream (U) - Pass 1 complete. Code/data split: 7892 instructions / 16527 bytes (50.4%). Vectors: NMI=$8FEA RESET=$8000 IRQ=$912B. 224 zero-page aliases generated. make produces byte-identical ROM.
  • 10-Yard Fight (U) - Pass 1 complete. Code/data split: 3674 instructions / 7541 bytes (23.0%). Vectors: NMI=$8471 RESET=$8000 IRQ=$84FF. 128 zero-page aliases generated. make produces byte-identical ROM.
  • Chubby Cherub (U) - Pass 1 complete. Code/data split: 6589 instructions / 13471 bytes (41.1%). Vectors: NMI=$82B7 RESET=$8000 IRQ=$8331. 149 zero-page aliases generated. make produces byte-identical ROM.
  • Dig Dug II (U) [!] - Pass 1 complete. Code/data split: 8146 instructions / 17500 bytes (53.4%). Vectors: NMI=$A749 RESET=$A52D IRQ=$A52D. 91 zero-page aliases generated. make produces byte-identical ROM.
  • Elevator Action (U) - Pass 1 complete. Code/data split: 741 instructions / 1431 bytes (4.4%). Vectors: NMI=$8081 RESET=$8000 IRQ=$8000. 51 zero-page aliases generated. make produces byte-identical ROM.
  • Gyromite (JUE) - Pass 1 complete. Code/data split: 1584 instructions / 3314 bytes (10.1%). Vectors: NMI=$8079 RESET=$8000 IRQ=$8000. 67 zero-page aliases generated. make produces byte-identical ROM.
  • Kung Fu (U) (a1) - Pass 1 complete. Code/data split: 893 instructions / 1931 bytes (5.9%). Vectors: NMI=$82D6 RESET=$8258 IRQ=$FFF0. 48 zero-page aliases generated. make produces byte-identical ROM.
  • Mach Rider (JU) - Pass 1 complete. Code/data split: 6174 instructions / 12738 bytes (38.9%). Vectors: NMI=$8070 RESET=$8000 IRQ=$8147. 199 zero-page aliases generated. make produces byte-identical ROM.
  • Mach Rider (JU) (a1) - Pass 1 complete. Code/data split: 6174 instructions / 12738 bytes (38.9%). Vectors: NMI=$8070 RESET=$8000 IRQ=$8147. 199 zero-page aliases generated. make produces byte-identical ROM.
  • Magmax (U) - Pass 1 complete. Code/data split: 1387 instructions / 2900 bytes (8.9%). Vectors: NMI=$BD33 RESET=$BC33 IRQ=$BC33. 63 zero-page aliases generated. make produces byte-identical ROM.
  • Magmax (U) (Prototype) - Pass 1 complete. Code/data split: 1387 instructions / 2900 bytes (8.9%). Vectors: NMI=$BD33 RESET=$BC33 IRQ=$BC33. 63 zero-page aliases generated. make produces byte-identical ROM.
  • Ms Pac-Man (Namco) (U) - Pass 1 complete. Code/data split: 1399 instructions / 2950 bytes (9.0%). Vectors: NMI=$C107 RESET=$FFF7 IRQ=$C198. 53 zero-page aliases generated. make produces byte-identical ROM.
  • Ms Pac-Man (U) - Pass 1 complete. Code/data split: 4179 instructions / 8726 bytes (26.6%). Vectors: NMI=$FB86 RESET=$FEE0 IRQ=$FBE1. 85 zero-page aliases generated. make produces byte-identical ROM.
  • NES Test Cart (Official Nintendo) (U) - Pass 1 complete. Code/data split: 1706 instructions / 3603 bytes (11.0%). Vectors: NMI=$8328 RESET=$8370 IRQ=$8370. 47 zero-page aliases generated. make produces byte-identical ROM.
  • Port Test Cartridge (U) - Pass 1 complete. Code/data split: 216 instructions / 439 bytes (1.3%). Vectors: NMI=$CC49 RESET=$C000 IRQ=$CC17. 28 zero-page aliases generated. make produces byte-identical ROM.
  • Pro Action Replay (No Cart Present) (U) - Pass 1 complete. Code/data split: 0 instructions / 0 bytes (0.0%). Vectors: NMI=$0000 RESET=$0000 IRQ=$0000. 1 label generated. make produces byte-identical ROM.
  • Pro Action Replay (Rev B) (U) - Pass 1 complete. Code/data split: 5156 instructions / 6321 bytes (19.3%). Vectors: NMI=$F021 RESET=$808A IRQ=$F020. 126 zero-page aliases generated. make produces byte-identical ROM.
  • Stack-Up (JU) - Pass 1 complete. Code/data split: 1086 instructions / 2260 bytes (6.9%). Vectors: NMI=$81AD RESET=$8100 IRQ=$81E6. 63 zero-page aliases generated. make produces byte-identical ROM.

2026-10-21 - The tools the new rules needed

  • Docs - PROMPTS.md gains two Pass 3 requirements, "Data tables are interpreted, not merely labelled" and "Method comments describe the method", with the two matching verification-checklist items; AGENTS.md carries the same two rules in its irreversible conventions.
  • _tools/derive_routine_docs.py - new tool: writes the Global:/Local:/Parameters:/Returns: fields of every routine header from the instructions that routine executes. It reuses derive_clobbers.py's parser and control-flow analysis, so the addressing mode of every instruction is exact and the register-preservation model is the one the Clobbers: line already uses.
  • _tools/analyze_data_blocks.py - new tool: reports, for every data block, the instructions that read it (the tracer's own reference lines plus a scan of every absolute operand in the source), whether the bytes are a pointer table into the PRG, a table of small values, padding, or a run that decodes as instructions - in which case the block is code the tracer could not reach rather than data.
  • _tools/cdl_seeds.py - new tool: reads the jump-target addresses out of a Mesen CDL capture, which are the seeds a static tracer needs to reach code behind a computed dispatch.
  • _tools/plan_overrides.py - new tool: lists the --override arguments a cdl_reclass.py run would need to force the branch targets the reclassifier's linear walk cannot land on.
  • _tools/name_data_region.py - the PRG base is now an option (--base, default 0xC000): it used to hardcode $C000, so on a 32 KB NROM image mapped at $8000 it wrote the audit comment's PRG offset as a negative number. The 30 rows it had broken are repaired and re-checked against prg.bin.

commit subject is the heading; the entries are the changes it carried.

2026-10-21 - Xevious (U): re-split Pass 1 from the Mesen CDL and decode the data tables

  • Xevious (U) - Pass 1 re-split from the Mesen CDL (xevious_mesen.cdl) with _tools/cdl_reclass.py: 1,158 bytes data→code, 48 code→data, 42 labels emitted and 3 branch targets labelled; code 11,321→12,455 bytes (34.5%→38.0%), instructions 5,513→6,002. The 45 recovered entry points are named Sub_XXXX, so no Label_XXXX remains; six CDL-silent raw call operands are documented as residue.
  • Xevious (U) - the 4,448-byte block at $A74B-$B8AA is split into six proven tables: VramWritePtrTable (15 pointers), VramWriteData (descriptors written to $2006/$2007), DataStreamPtrTable (16 pointers), ObjTilePtrTable (99 pointers), ObjTileData and DataStreamData. The $D833 pointer table is corrected to base $D833 (4 entries; the old ObjCodePtrTable at $D834 was one byte late). $F957-$FAF5 is split into ObjSpeedTable, ObjSpeedTable2, SpriteLayoutPtrTable (19 pointers) and SpriteLayoutData; $FEE6-$FF1C into PaletteData and OamAttrTable. $F5D8 is decoded as TextTileData (font tile indices: PLAYER, GAME OVER, READY, SOLVALOU, LEFT, PAUSE); $F53C becomes AttrAddrTable, $F89E HandlerPtrTable and $EF3B SpriteLayoutData2. Project README rewritten with the corrected stats, a 32-entry table inventory and the Pass 1 residue.
  • _tools/cdl_reclass.py - preserve the data label(s) that sit between a ; ---- data header and its first .byte row when the block stays data. The $DF78 split dropped Data_DF78, which unchanged code still referenced, so the rebuild failed with an undefined symbol.
  • _tools/resolve_raw_branches.py - handle negative relative offsets (beq *-$0B); the pattern only matched *+$NN and *$NN, so backward branches could not be resolved.

2026-10-21 - Golf (JU) and World Class Track Meet (U): Pass 2/3 complete, promoted from _wip/

  • World Class Track Meet (U) - Pass 2 complete: all 884 Label_<addr> definitions replaced (651 semantic routine names + 233 Sub_<addr>), all 731 raw $XXXX jsr/jmp call operands resolved to symbolic labels, all branch targets given role-based names; routinemap.tsv added; make stays byte-identical.
  • World Class Track Meet (U) - Pass 3 complete: 24 semantic zero-page aliases applied via apply_symbols.py (0 residue), 78 neutral ZpXX slots documented with reasons in locals.inc, all hardware-register operands use symbols.inc names, project README rewritten with Pass 2/Pass 3 sections and data-table inventory; project is ready for promotion out of _wip/CNROM/.
  • World Class Track Meet (U) - project README now says what the five entries of the event table at $F796 line up with (the five single events of the six-item menu) and marks the sixth item, the "olympics", as not yet proven: the menu's own code is still outside the traced set, so the mode that schedules several of the five task slots at once is the expected answer, not a proven one.
  • Golf (JU) - audit comments converted from pre-canonical (; C000: A9 00) to the canonical ; CPU:$C000 PRG:0000 A9 00 form at column 35 via _tools/repair_audit_comments.py then _tools/align_audit_comments.py; 4,156 comments re-aligned, comment/whitespace only, whole-ROM build stays byte-identical.
  • Golf (JU) - 339 internal branch-target Label_ labels given role-based names (<Routine>_Loop, <Routine>_OnZero, …) via _tools/derive_branch_names.py from routinemap.tsv and applied with _tools/apply_pass2_map.py; 10 fall-through-only labels keep their Label_<addr> definitions; 0 raw $XXXX branch/jump operands remain.
  • Golf (JU) - 275 zero-page operands substituted with locals.inc aliases via apply_symbols.py; all hardware-register operands already symbolic; 0 multi-alias residue; no unexplained numeric operands left.
  • Golf (JU) - project README updated: Pass 2/Pass 3 marked complete, audit-comment and Pass 3 sections rewritten, routinemap.tsv added; project is ready for promotion out of _wip/NROM/.
  • Golf (JU) and World Class Track Meet (U) - promoted out of _wip/ (git mv to NROM/Golf (JU)/ and CNROM/World Class Track Meet (U)/); leftover _wip/ folders removed; root README.md inventory rows added; WIP.md rows removed.
  • Xevious (U) - runs of $FF padding replaced with .res <count>, $FF in prg.s; whole-ROM build stays byte-identical.

2026-10-21 - World Class Track Meet (U): Pass 1, the code/data split

  • World Class Track Meet (U) - Pass 1 done. The code/data split is 8,400
  • World Class Track Meet (U) - four dispatch tables had to be seeded by
  • World Class Track Meet (U) - the tracer runs with --undocumented: the
  • World Class Track Meet (U) - $C0BE-$C0C5 is forced back to data: the
  • World Class Track Meet (U) - make pass1 regenerates prg.s and
  • _tools/nesdis.py - new --mapping-note option, so the header's "PRG
  • Docs - the project README records the split, the four dispatch tables, the

2026-10-21 - Balloon Fight (JU): repair the audit trail and complete the routine headers

  • Balloon Fight (JU) - the per-instruction audit trail was repaired. 5,944 of the 6,007 comments had been stale by two bytes since the three-byte .byte "HAL" signature at $C082-$C084 was counted as one, so every comment from ResetInitializationData ($C085) onward named the wrong CPU address and PRG offset; the routine headers and the linker labels were right, so the source and its own headers disagreed. The trail was rebuilt from the ca65 -l listing with _tools/repair_audit_comments.py and every rewritten comment was re-checked against prg.bin.
  • Balloon Fight (JU) - the audit comments were moved into the canonical column-35 field. They had sat two spaces after the mnemonic, with no column discipline at all, unlike every other promoted project.
  • Balloon Fight (JU) - all 63 routine headers now carry a ; Clobbers: block, so the headers carry the full template AGENTS.md describes. The blocks are derived, not written by hand: _tools/derive_clobbers.py takes the opcode from the audit comment's byte list, resolves the operand through locals.inc and symbols.inc to name what is written, follows the real control flow, and models register preservation - so NmiHandler reports no register and FlushPpuCommandPreserved reports only A.
  • Balloon Fight (JU) - ReflectObjectYVelocity and UpdateSpecialObjectSystem moved out of locals.inc and became labels in prg.s at their real addresses.
  • _tools - new derive_clobbers.py, which fills in the ; Clobbers: block of every routine header from the source.
  • _tools - align_audit_comments.py had an off-by-one: it tested the indented code length against 31 instead of against 35, so code texts of 28-31 characters were never padded into the column.
  • Docs - the project README gains the audit-trail and clobber sections, the root README and TODO.md carry the same news, AGENTS.md and PROMPTS.md describe this changelog's new shape, and CHANGELOG.md itself becomes the running log with a pending list.
  • WIP.md - _wip/MAPPER8/ is gone; it held a copy of Mermaids of Atlantis (U) that had already been promoted.

The milestone index

The four milestones every project walks through, and the state of each project against them.

The four milestones

Milestone What it does How it is checked
Pass 1 The code/data split: every ROM byte is classified as an instruction or as data, with the audit comment (; CPU:$C123 PRG:0123 AD 21 03) on every instruction. Every byte of the PRG image is accounted for exactly once, and the audit comments re-check against prg.bin.
Pass 2 The labels are replaced by names: the address-derived Label_XXXX definitions become semantic routine names, and the internal branch targets get role names (<Routine>_OnNonzero, <Routine>_Loop). No Label_XXXX definition is left, no branch or jump operand is a raw $XXXX, and every label is defined exactly once.
Pass 3 The semantic naming: the data tables, the zero-page aliases and the hardware register operands get the names the dump proves, and the routine headers are written. apply_symbols.py reports no unexplained residue, and every alias that is not proven is recorded with its reason.
Promotion The project moves out of _wip/ into its mapper folder with the whole-ROM build in place. make in the project folder prints Byte-identical ROM verified. against rom.nes.

A project is only promoted when Pass 2 and Pass 3 are done, the promotion requirements in PROMPTS.md are met, and the whole ROM rebuilds byte for byte. PROMPTS.md is the contract; this file is the history.

On the dates. A date is the day the repository records the milestone: the commit that carried it, or the day the project README dates the work. Where neither does, the milestone is dated to the wave it was committed with - the 2026-10-15 build standardisation, the 2026-10-18 _wip pass-1 wave, the 2026-10-19 naming wave, or the 2026-10-20 promotion wave. A milestone that is still open says so in the row.

The tiers

  • Promoted — 41 projects in the top level NROM/, CNROM/, UNROM/ and MAPPER8/ folders. Each one below has been through all four milestones unless its entry says otherwise.
  • _wip/ — 851 projects that are still Pass 0 skeletons: prg.s is a single .incbin "prg.bin". They have a build and a byte-identical rebuild, but no disassembly yet. Golf (JU) and World Class Track Meet (U) were the tier's last two real disassemblies and were both promoted on 2026-10-21, so no project in the tier carries one now.

Balloon Fight (JU) — NROM/Balloon Fight (JU)/

Milestone Date Note
Pass 1 2026-10-16 First disassembly 2026-10-09; verified complete on the 16th: 5,303 instructions / 11,260 code bytes (68.7 %). The last two .incbin spans were turned into explicit classified bytes, so no ROM byte is hidden.
Pass 2 2026-10-19 Every label renamed; the zero-page aliases written into locals.inc.
Pass 3 2026-10-19 125 hardware and 797 of 813 zero-page operands substituted; the 16 that stay numeric are recorded. 2026-10-21: the audit trail was repaired - 5,944 comments had been stale by two bytes since a three byte .byte "HAL" signature had been counted as one. 2026-10-21: every routine header now carries a derived ; Clobbers: block (63 of 63), so the headers carry the full template - global, local, parameters, returns, clobbers.
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

Baseball (UE) — NROM/Baseball (UE)/

Milestone Date Note
Pass 1 2026-10-09 First disassembly; prg.s is the largest source in the set at 21,624 lines.
Pass 2 2026-10-19 Labels renamed in the audit-comment standardisation wave.
Pass 3 2026-10-19 121 operands substituted (85 single-name + 36 hardware), zero residue.
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

Bomberman (U) — NROM/Bomberman (U)/

Milestone Date Note
Pass 1 2026-10-09 First disassembly.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 354 single-name and 8 site-resolved multi-alias operands applied, 24 table labels taken from the linker map. Residue recorded: 758 unnamed operands over 179 distinct addresses. The data-table interpretation was added on 2026-10-21: every data block named and interpreted (49 labelled blocks, 9,487 bytes).
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

Burger Time (U) — NROM/Burger Time (U)/

Milestone Date Note
Pass 1 2026-10-09 First disassembly; byte identical out of the box.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 Symbols applied.
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

CIRCUS — NROM/CIRCUS/

Milestone Date Note
Pass 1 2026-10-16 Full code/data classification; the main loop is a spin loop at $C057 and every frame's work runs in the NMI.
Pass 2 2026-10-16 Semantic routine naming.
Pass 3 2026-10-19 118 operands substituted (38 single-name + 80 hardware); 23 multi-alias residues recorded.
Promotion 2026-10-16 Promoted out of _wip/; the menu text table at $C763 and the variant object machine at $DC3F-$FE7B documented.

Clu Clu Land (JU) — NROM/Clu Clu Land (JU)/

Milestone Date Note
Pass 1 2026-10-09 First disassembly.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 Symbols applied.
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

Defender 2 (U) — NROM/Defender 2 (U)/

Milestone Date Note
Pass 1 2026-10-09 First disassembly.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 Symbols applied.
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

Donkey Kong (JU) — NROM/Donkey Kong (JU)/

Milestone Date Note
Pass 1 2026-10-18 6,014 instructions / 12,505 code bytes (76.3 %); the reset entry is at $C79E, not $C000.
Pass 2 2026-10-19 All 919 Label_XXXX definitions named: 102 semantic routine names, 217 SubC<addr> entries and 600 derived internal branch targets.
Pass 3 2026-10-19 10 named data regions, 75 operands substituted.
Promotion 2026-10-19 git mv out of _wip/; the mirroring was corrected from Vertical to Horizontal (header byte 6 = $00).

Donkey Kong 3 (JUE) — NROM/Donkey Kong 3 (JUE)/

Milestone Date Note
Pass 1 2026-10-16 The build was split into header.s / prg.s / chr.s, and a one byte branch-target bug was fixed: the bne at $C14C was labelled StoreSelectedPlayerAndScreenFlags instead of the routine at $C160, so the project had never actually built byte identical.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 Symbols applied.
Promotion 2026-10-15 Top-level wave: the build was standardised and the project got its power-on README.

Donkey Kong Jr. (JU) — NROM/Donkey Kong Jr. (JU)/

Milestone Date Note
Pass 1 2026-10-16 Verified complete by a byte-accounting audit: all 16,384 PRG bytes are emitted exactly once (6,135 instructions + 3,889 .byte + 16 .word), with no audit-comment mismatch and no .incbin left. 75.4 % code.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 The tables named - DeferredTaskHandlerTable, HudDigitDescriptorTable and the rest.
Promotion 2026-10-16 Promoted out of _wip/. Architecturally different from the rest: the reset idles in a PRNG loop and the whole frame state machine runs inside the NMI.

Donkey Kong Jr. Math (U) — NROM/Donkey Kong Jr. Math (U)/

Milestone Date Note
Pass 1 2026-10-19 6,589 instructions / 13,416 code bytes (81.9 %).
Pass 2 2026-10-19 Restarted from the committed split after the 2026-10-18 draft map was audited and found to have no safely applicable row; 1,035 labels named (64 semantic + 295 SubC<addr> + 676 internal).
Pass 3 2026-10-19 18 data tables named, 13 semantic zero-page aliases, all multi-alias operands substituted.
Promotion 2026-10-19 Top level; the whole-ROM verify closed on 2026-10-20 after four call sites that named a back-branch block instead of the routine it belongs to were repointed.

Duck Hunt (JUE) — NROM/Duck Hunt (JUE)/

Milestone Date Note
Pass 1 2026-10-19 3,058 instructions / 6,152 code bytes (37.5 %).
Pass 2 2026-10-19 Restarted from the committed split; 472 labels named (30 semantic + 428 SubC<addr> + 293 internal).
Pass 3 2026-10-19 20 data tables named, 18 semantic zero-page aliases.
Promotion 2026-10-19 Top level.

Excitebike (JU) — NROM/Excitebike (JU)/

Milestone Date Note
Pass 1 2026-10-09 First disassembly.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 The audit comments were completed - 1,013 lines had none. 2026-10-20: the trail was re-anchored from the ca65 listing (6,339 shifted comments and 4,401 doubled ?? forms) and all 58 raw jsr/jmp operands were given routine names.
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

Golf (JU) — _wip/NROM/Golf (JU)/ — ready for promotion out of _wip/

Milestone Date Note
Pass 1 2026-10-20 The whole-ROM compare was added: the project builds byte identical. 4,217 reachable instructions in prg.s (13,541 lines); all remaining .incbin spans confirmed data with zero code entry points.
Pass 2 2026-10-21 All 112 subroutine entries named (4 semantic + 108 SubC<addr>); all jsr/jmp call sites symbolic, 0 raw $XXXX call operands; 339 of 349 internal branch targets given role-based names (<Routine>_Loop, <Routine>_OnZero, …); 10 fall-through-only labels keep their Label_<addr> definitions.
Pass 3 2026-10-21 Audit comments canonical at column 35 (4,156 re-aligned via _tools/repair_audit_comments.py + _tools/align_audit_comments.py); all hardware-register operands use symbols.inc names; 275 zero-page operands substituted with locals.inc aliases via apply_symbols.py, 0 residue; every branch/jump target symbolic.
Promotion — Outstanding (awaiting git mv to NROM/Golf (JU)/ plus root README.md inventory row).

World Class Track Meet (U) — _wip/CNROM/World Class Track Meet (U)/ — ready for promotion out of _wip/

Milestone Date Note
Pass 1 2026-10-21 The code/data split is done: 8,400 instructions / 18,028 code bytes (55.0 %) of the 32 KB PRG, 884 routine entries, 55 data blocks, 102 zero-page aliases. The PRG is CNROM (mapper 3) and maps whole at $8000-$FFFF, so the linker moved the PRG segment from a file offset to $8000. Four dispatch tables had to be seeded by hand: the event table at $F796 (5 entries, read at $F780 and jumped to through jmp ($001F)) and three push-pointer tables - $BFC4 (16), $BFE4 (2) and $E5F2 (7) - which store the handler address minus one, because the handler is pushed and an rts lands on it. The tracer runs with --undocumented: the code uses undocumented NMOS opcodes as ordinary code. Seeding the tables takes the coverage from 27.8 % to 55.0 %. The rebuild is byte identical.
Pass 2 2026-10-21 All 884 Label_XXXX definitions replaced: 651 semantic routine names (Reset, NmiHandler, MainLoop, TaskDispatch, EventInit, EventTaskRun, SpriteSetup, ObjectState, SoundNote, OlympicsScheduler, …) plus 233 Sub_<addr> address-derived residue. All 731 raw $XXXX jsr/jmp call operands resolved to symbolic labels; every branch target given a role-based name (_Loop, _OnZero, _OnNonzero, …); routinemap.tsv holds the address→name map.
Pass 3 2026-10-21 24 semantic zero-page aliases applied via apply_symbols.py (0 residue), 78 neutral ZpXX slots documented with reasons in locals.inc, all hardware-register operands use symbols.inc names, data-table inventory documented in the project README.
Promotion — Outstanding (awaiting git mv to CNROM/World Class Track Meet (U)/ plus root README.md inventory row).

Hogan's Alley (JU) — NROM/Hogan's Alley (JU)/

Milestone Date Note
Pass 1 2026-10-20 The static trace had classified only 41.8 % of the PRG as code. A merged Mesen CDL capture (five captures, 6,544 code bytes) moved 1,216 bytes from data to code and five the other way, taking the split to 49.1 %.
Pass 2 2026-10-20 A fresh map from this dump's own committed split: 206 routine entries hand-named and 413 internal branch targets derived.
Pass 3 2026-10-20 60 semantic zero-page aliases, each applied only where its role is proven.
Promotion 2026-10-20 Top level.

Ice Climber (JE) — NROM/Ice Climber (JE)/

Milestone Date Note
Pass 1 2026-10-19 6,112 instructions / 12,230 code bytes (74.6 %).
Pass 2 2026-10-19 Restarted from the committed split; 973 labels named (76 semantic + 212 SubC<addr> + 685 internal).
Pass 3 2026-10-19 9 semantic zero-page aliases, the hardware operands substituted, the data tables named.
Promotion 2026-10-19 Top level. The PRG and the header are byte identical to the (U) dump; only the CHR differs.

Ice Climber (U) — NROM/Ice Climber (U)/

Milestone Date Note
Pass 1 2026-10-19 6,112 instructions / 12,230 code bytes (74.6 %) - the same PRG as the (JE) dump.
Pass 2 2026-10-19 973 labels named.
Pass 3 2026-10-19 9 semantic zero-page aliases and the hardware operands substituted.
Promotion 2026-10-19 Top level, together with the (JE) dump.

Joypad Test Cartridge (U) — NROM/Joypad Test Cartridge (U)/

Milestone Date Note
Pass 1 2026-10-16 Redone: the first pass claimed 282 instructions / 580 bytes (3.5 %), because the tracer halted on DCP $3C20 - an undocumented opcode used as ordinary code - and filed 3,256 bytes of real code as data. Now 1,773 instructions / 3,836 bytes (23.4 %).
Pass 2 2026-10-19 All 202 labels renamed and all 202 routine headers rewritten; the zero-page aliases written.
Pass 3 2026-10-19 47 hardware operands substituted, the data blocks named.
Promotion 2026-10-16 Promoted out of _wip/.

Lode Runner (U) — NROM/Lode Runner (U)/

Milestone Date Note
Pass 1 2026-10-18 11,057 lines, 627 label definitions, 52.6 % code, canonical audit comments.
Pass 2 2026-10-20 All 627 labels renamed: 196 semantic routine entries plus 431 derived internal branch targets.
Pass 3 2026-10-20 85 operands substituted, 2 semantic zero-page aliases, the data tables named.
Promotion 2026-10-20 The whole-ROM build was added and the _tools paths moved.

Lunar Pool (U) — NROM/Lunar Pool (U)/

Milestone Date Note
Pass 1 2026-10-18 The split was re-derived from a Mesen CDL capture (10,028 bytes of code) after a static trace alone had left most of the game unclassified: now 5,300 instructions / 11,077 bytes (67.6 %).
Pass 2 2026-10-18 705 labels named, zero Label_ residue.
Pass 3 2026-10-18 33 data tables named, 38 semantic zero-page aliases; 45 operands substituted on 2026-10-19.
Promotion 2026-10-18 git mv out of _wip/; the power-on flow and the print-inline-string helper documented.

Mario Bros (JU) — NROM/Mario Bros (JU)/

Milestone Date Note
Pass 1 2026-10-20 The committed split was a strict subset of the correct one - only 72.0 % was classified as code. A Mesen CDL capture (10,915 code bytes) showed 1,010 bytes the tracer had left as data were code, and two branch targets the capture never reached were forced by hand, so 1,067 bytes moved from data to code: 78.4 %. The engine's handler tables sit inline, right behind the jsr that reads them.
Pass 2 2026-10-20 206 routine entries and 519 internal branch targets, 931 Label_XXXX definitions gone, 51 raw call operands resolved.
Pass 3 2026-10-20 25 semantic zero-page aliases, each applied only where its role is proven.
Promotion 2026-10-20 Top level.

Millipede (U) — NROM/Millipede (U)/

Milestone Date Note
Pass 1 2026-10-18 14,744 lines, canonical audit comments, 74.6 % code.
Pass 2 2026-10-20 All 839 address-derived labels renamed: 12 semantic routine names, 264 SubC<addr> entries and 577 internal branch targets.
Pass 3 2026-10-20 8 semantic zero-page aliases; the data tables named - LevelData, ObjectInitData, AudioData, VectorPageData.
Promotion 2026-10-20 The whole-ROM build was added and the _tools paths moved.

MUSCLE (U) — NROM/MUSCLE (U)/

Milestone Date Note
Pass 1 2026-10-16 The bytecode VM layer was decoded: 40 opcodes handled through a table at $9237, so the "data" under the bytecode is code. 5,253 instructions.
Pass 2 2026-10-18 The Pass 1/2/3 output written with the 42 bytecode listings above the .byte data.
Pass 3 2026-10-16 The data names and the structures written. 2026-10-19: 220 operands substituted, zero residue.
Promotion 2026-10-16 Promoted out of _wip/. The one ROM in the set mapped low (PRG at $8000), and the only project with a make regen target that reproduces prg.s byte for byte.

NES PowerPad Test Card (U) — NROM/NES PowerPad Test Card (U)/

Milestone Date Note
Pass 1 2026-10-16 A minimal test cartridge: the NMI reads the PowerPad over $4016 and updates the score and the lives, 7.2 % code.
Pass 2 2026-10-19 Labels renamed with the PowerPad group.
Pass 3 2026-10-16 The data tables and the VramWrite streams named. 2026-10-19: 89 hardware operands substituted, zero residue.
Promotion 2026-10-16 Promoted out of _wip/.

Pac-Man (Tengen) (U) — NROM/Pac-Man (Tengen) (U)/

Milestone Date Note
Pass 1 2026-10-20 Six command handlers recovered - 120 bytes the tracer had left as data, two of them mis-split across the jmp $EF61 at $F01F and $F04F. 1,637 instruction lines covering 3,273 code bytes (20.0 %). The boot sequence is inline at $C033: this build has no boot code at $FF00.
Pass 2 2026-10-20 A fresh map from this dump's own committed split - no name, map or file was carried over from the other Pac-Man dump. 227 labels (199 routine entries + 28 branch targets), zero Label_ residue.
Pass 3 2026-10-20 30 semantic zero-page aliases, each applied only at the sites where the role is proven.
Promotion 2026-10-20 git mv out of _wip/NROM/.

Pac-Man (U) — NROM/Pac-Man (U)/

Milestone Date Note
Pass 1 2026-10-20 19.6 % code; the boot code sits at $FF00, unlike the Tengen build.
Pass 2 2026-10-20 194 Label_XXXX definitions renamed to semantic routine entries plus derived internal branch targets, and all 49 jsr/jmp call sites resolved.
Pass 3 2026-10-20 119 operands substituted (101 hardware + 11 single-name + 7 multi-alias resolved per site).
Promotion 2026-10-20 git mv out of _wip/NROM/.

Pac-Man (USA) (Namco) — NROM/Pac-Man (USA) (Namco)/

Milestone Date Note
Pass 1 2026-10-09 First disassembly.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 524 single-name operands substituted; 727 multi-alias residues left numeric and recorded (an address with two or more aliases, e.g. $00 = workPtr0/Zp00).
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

Pinball (JU) — NROM/Pinball (JU)/

Milestone Date Note
Pass 1 2026-10-18 4,734 instructions / 9,756 code bytes (59.5 %) - a real disassembly, not a skeleton.
Pass 2 2026-10-18 Substantial: 180 of 681 labels carry semantic names. The "Pass 2 complete" claim of that day was corrected on 2026-10-19 after an audit of the map.
Pass 3 — Drafted only: the zero-page aliases are not in locals.inc yet.
Promotion 2026-10-20 The whole-ROM build was added; the pass work is still open.

Popeye (JU) (PRG 0) — NROM/Popeye (JU) (PRG 0)/

Milestone Date Note
Pass 1 2026-10-18 14,875 lines, canonical audit comments, 72.3 % code, six routine entries seeded from table references.
Pass 2 2026-10-20 All 878 labels renamed: 2 semantic entry names, 315 SubC<addr> entries and 561 internal branch targets.
Pass 3 2026-10-20 4 semantic zero-page aliases, 79 operands substituted, the data tables named - LevelData, AudioData, VectorPageData.
Promotion 2026-10-15 Top-level wave: the build was standardised and the project got its power-on README.

Popeye (JU) (PRG 1) — NROM/Popeye (JU) (PRG 1)/

Milestone Date Note
Pass 1 2026-10-18 As the PRG 0 dump, but a different PRG binary.
Pass 2 2026-10-20 All 878 labels renamed.
Pass 3 2026-10-20 4 semantic zero-page aliases and the data tables named.
Promotion 2026-10-20 Promoted out of _wip/.

Pyramid (U) (Sachen) — NROM/Pyramid (U) (Sachen)/

Milestone Date Note
Pass 1 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 hand-seeded starts recovered them, and lost no committed code byte. 39.6 % code.
Pass 2 2026-10-20 431 routine entries and the 32 data blocks named, 428 Label_XXXX definitions gone, 320 raw call operands resolved.
Pass 3 2026-10-20 24 semantic zero-page aliases, each applied only where its role is proven.
Promotion 2026-10-20 Top level; renamed to Pyramid (U) (Sachen) on 2026-10-21. The unlicensed boot pokes a mapper unlock at $4100 and dispatches through two queued-event tables.

Raid on Bungeling Bay (U) — NROM/Raid on Bungeling Bay (U)/

Milestone Date Note
Pass 1 2026-10-20 64.6 % code.
Pass 2 2026-10-20 Labels renamed in the same wave as the other 2026-10-20 promotions.
Pass 3 2026-10-20 11 semantic zero-page aliases added to locals.inc.
Promotion 2026-10-20 Top level.

Raid on Bungeling Bay (U) [a2] — NROM/Raid on Bungeling Bay (U) [a2]/

Milestone Date Note
Pass 1 2026-10-20 64.3 % code. A different PRG from the other dump - the two differ in mirroring only per the header, but the PRGs are separate builds.
Pass 2 2026-10-20 Labels renamed.
Pass 3 2026-10-20 15 semantic zero-page aliases added to locals.inc.
Promotion 2026-10-20 Top level.

Tennis (JU) — NROM/Tennis (JU)/

Milestone Date Note
Pass 1 2026-10-09 First disassembly; byte identical out of the box.
Pass 2 2026-10-19 Labels renamed.
Pass 3 2026-10-19 18 single-name operands substituted, zero residue.
Promotion 2026-10-09 In the top level from the start - there was no _wip/ tier yet. The build and the per-project power-on README were standardised on 2026-10-15.

Urban Champion (JU) — NROM/Urban Champion (JU)/

Milestone Date Note
Pass 1 2026-10-18 2,716 instructions, 33.9 % code.
Pass 2 2026-10-18 436 labels named.
Pass 3 2026-10-19 10 semantic zero-page aliases, 70 operands substituted.
Promotion 2026-10-18 Promoted with the full-ROM build.

Volleyball (U) — NROM/Volleyball (U)/

Milestone Date Note
Pass 1 2026-10-21 Every byte of the 32 KB PRG image accounted for as an instruction or an explicit .byte block: 11,395 instructions / 23,926 code bytes (73.0 %).
Pass 2 2026-10-21 1,989 labels named.
Pass 3 2026-10-21 37 named data blocks, zero residue.
Promotion 2026-10-21 Promoted out of _wip/NROM/.

Wild Gunman (U) — NROM/Wild Gunman (U)/

Milestone Date Note
Pass 1 2026-10-16 Redone: the original tracer followed only the vectors and the direct calls and knew just the 151 documented opcodes, so it missed the runtime dispatch - the pla based computed goto puts the jump table inline right after the jsr - and filed about 4.9 KB of real code as data. 1,597 instructions / 3,352 bytes (20.5 %) became 3,956 / 8,085 (49.3 %).
Pass 2 2026-10-19 All 733 labels carry names; zero Label_Cxxxx or SubCxxxx remain, and every entry point has a routine header.
Pass 3 2026-10-19 The data tables named - SpriteAttributeTable, NoteVolumeTable, VolumeEnvelopeTable, SpritePositionTable; 76 operands substituted, zero residue.
Promotion 2026-10-16 Promoted out of _wip/.

Xevious (U) — NROM/Xevious (U)/

Milestone Date Note
Pass 1 2026-10-20 The static trace from the vectors reached only 13.6 % of the PRG. Declaring both jump tables with --table plus 92 hand-seeded entry points recovered the code behind the computed dispatch, reaching 34.5 %. Re-split on 2026-10-21 from the Mesen CDL (_tools/cdl_reclass.py): 1,158 data→code bytes, 38.0 % code, 6,002 instructions.
Pass 2 2026-10-20 Every routine entry point has a semantic name and every raw $XXXX call operand was replaced. Re-checked after the 2026-10-21 re-split: the 45 recovered entries are named Sub_XXXX; six CDL-silent raw operands remain and are documented in the project README.
Pass 3 2026-10-20 32 named data tables (the $A74B-$B8AA block split into six proven tables on 2026-10-21) and about 20 semantic zero-page aliases.
Promotion 2026-10-20 Promoted out of _wip/.

Zanac (U) — UNROM/Zanac (U)/

Milestone Date Note
Pass 1 2026-10-20 The static trace from the vectors reached only 37.9 % of bank 7; banks 0-6 are still raw .byte data, pending the bank-switch analysis.
Pass 2 2026-10-20 133 semantic entry points.
Pass 3 2026-10-20 26 named data tables and 14 semantic zero-page aliases.
Promotion 2026-10-20 Promoted out of _wip/; the only promoted UxROM project, with the fixed bank in its own segment.

Mermaids of Atlantis (U) — MAPPER8/Mermaids of Atlantis (U)/

Milestone Date Note
Pass 1 2026-10-21 Redone, bank-aware: the 64 KB PRG is eight 8 KB MMC3 banks, and each is traced separately with the CPU window it is visible at, seeded from a Mesen CDL capture. 8,075 bytes of code (12.3 %) in 494 routines, up from about 200 bytes. The image carries the reset stub and the vector table twice, and the fixed bank is the fourth 8 KB bank rather than the last.
Pass 2 2026-10-21 Every label named, no Label_XXXX left and no raw $XXXX branch or jump operand left; the banks that share a CPU window carry a Bank4_ .. Bank7_ prefix.
Pass 3 2026-10-21 13 semantic zero-page aliases, the routine headers written, the data tables listed in the project README.
Promotion 2026-10-20 Promoted - but the build was broken: a half-applied Pass 2 rename left eight undefined symbols. Rebuilt and verified on 2026-10-21.

Adding an entry

For a change to any project, add a line to Pending at the top of this file: name the project, and say what changed. When the commit is made, its message is written from those lines, and they move to History.

For a project that reaches a milestone, the milestone also gets a row in the milestone index below, with the date and a sentence saying what the milestone produced. The numbers that go in the row are the ones the project README and the root README.md already carry: the instruction count, the code coverage, the number of named labels, the number of semantic zero-page aliases and the number of named data tables. A milestone that has not happened gets a - in the date column and a note saying what is still open - the point of the table is that a reader can see at a glance which projects are done and which are not.