# World Class Track Meet (U) ## Status **Pass 1 + Pass 2 + Pass 3 complete (2026-10-21).** The code/data split, the label naming and the semantic naming are all done: `prg.s` holds the traced disassembly with the canonical audit comment `; CPU:$XXXX PRG:XXXX AA BB` on every instruction, the build is **byte-identical** to the original ROM, and every routine entry, branch target and data reference uses a symbolic name. | | | |---|---| | Instructions | **8,400** | | Code bytes | **18,028** of 32,768 (**55.0 %**) | | Data bytes | 14,740 | | Routine entries | **884, all named** (651 semantic + 233 `Sub_`) | | Data blocks | 55 | | Zero-page aliases | **102** (24 semantic + 78 neutral `ZpXX` with recorded reasons) | | `Label_XXXX` definitions | **0** (all replaced) | | Raw `$XXXX` jsr/jmp operands | **0** (all symbolic) | | `prg.s` | 18,390 lines | `make` assembles `header.s`, `prg.s`, `chr.s`, links them with `ld65` and compares the result against `rom.nes`: ``` Byte-identical ROM verified. ``` `make pass1` regenerates `prg.s` and `locals.inc` from `prg.bin`; it reproduces the committed `prg.s` byte for byte. ## ROM | Property | Value | |----------|-------| | Format | iNES | | Mapper | 3 | | Board | CNROM | | Mirroring | Horizontal | | Four-screen VRAM | no | | Battery | no | | PRG | 32 KB, at CPU $8000-$FFFF | | CHR | 32 KB (4 x 8 KB) | | Trainer | none | | Footer | none | | NMI vector | $F803 | | RESET vector | $F6FD | | IRQ vector | $C0C0 (dead — see below) | | ROM file | `World Class Track Meet (U).nes` | | SHA-256 | `d592d60690234cd9fb6b24a2c566740523bee90bbe7ac1171df3dfbde3ab4e39` | ### Header notes - **The CHR size field is wrong.** Header byte 5 says 4 CHR pages (64 KB), but the file holds 32 KB of CHR (`chr.bin` is 32,768 bytes and the whole file is 65,552 bytes = 16 header + 32,768 PRG + 32,768 CHR). The dump therefore claims more CHR than it carries. The build reproduces the file as it is, so the rebuild is byte-identical either way. - **Mapper 3 is CNROM: the PRG is not bank switched.** The whole 32 KB image is visible at $8000-$FFFF, so the linker puts the PRG segment at $8000 (`PRG: start = $8000, size = $8000`) rather than at a .nes file offset. The Pass 0 skeleton's "the image is bank-switched; it does not fit the 6502 address space" note was the skeleton's generic guess for a PRG larger than 16 KB and is wrong for this board — $8000-$FFFF is 32 KB of address space, and CNROM fills it with the whole image. ## What the game is A Power Pad game: a menu of six items, the first five being single events and the last one the "olympics" with several events in a row. The code agrees with that shape: - The main loop at **$F74F** (`MainLoop`) runs **five** concurrent tasks. Each task has a slot (`$00`-`$04` for the slot index, `$06`/`$01` for its state) and the loop walks them, calling the task whose counter has run out. The five task entry points live in the dispatch table at **$F796**: `$D32B`, `$8004`, `$924C`, `$A8C8`, `$E0D5`. Each of them opens with its own state index (`ldx #$3B`, `ldx #$4B`, `ldx #$5B`, …), which is what a per-event state block looks like. - Those five entries line up with the five **single** events the menu offers. The sixth item, the "olympics", is expected to be the mode that schedules several of the five slots at once rather than a sixth slot - a five-slot cooperative loop can do exactly that (`OlympicsScheduler` at `$E0D5`). - The dispatch at **$F780** (`TaskDispatch`) is the only *computed* jump in the game: `lda $F796,y` / `sta $1F` / `lda $F797,y` / `sta $20` / `jmp ($001F)`. The tracer cannot follow it, so the table is seeded by hand. ## Pass 2 — label naming (complete 2026-10-21) All 884 `Label_` definitions were replaced: - **651 semantic routine names** derived from observed ROM behavior: `Reset`, `NmiHandler`, `MainLoop`, `GameModeTask`, `TaskDispatch`, `EventInit`, `EventTaskRun`, `EventTaskB`, `EventTaskInit`, `SpriteSetup`, `SpriteDraw`, `SpritePosition`, `SpriteTile`, `ObjectState`, `ObjectUpdate`, `ObjectDraw`, `ObjectCollision`, `SoundNote`, `SoundChannelStep`, `SoundChanInit`, `AudioChannelLoop`, `OlympicsScheduler`, `PaletteLoad`, `PollControllers`, `ClearRam`, `ClearOam`, `ClearObjectSlot`, `DelayFrames`, `EnableNmi`, `WaitVblankStart`, `WaitVblankEnd`, `TaskSlotStart`, …; - **233 `Sub_` address-derived names** for entries whose role the dump does not reveal (kept per the honest-residue convention, never guessed); - All **731 raw `$XXXX` jsr/jmp call operands** resolved to symbolic labels; - All branch targets (`beq`/`bne`/etc.) given role-based names (`_Loop`, `_OnZero`, `_OnNonzero`, `_OnCarrySet`, …); - `routinemap.tsv` holds the address→name map (hand-written routine names + derived internal branch-target names from `_tools/derive_branch_names.py`). ## Pass 3 — semantic naming (complete 2026-10-21) - **Zero-page aliases:** 24 behavioral names (`SlotState`, `PpuCtrlMirror`, `PpuMaskMirror`, `TaskCount`, `TaskSlotOffset`, `TaskFrameTimer`, `TaskFrameCounter`, `ScrollX`, `ScrollY`, `VblankFlag`, `OlympicsFrameCounter`, `OlympicsEventIndex`, `GameMode`, …) applied at the proven sites via `apply_symbols.py`; **78 remaining `ZpXX` neutral slots** are genuinely undetermined after tracing (each byte is reused across subsystems and/or indexed into an attribute array), each with a recorded reason in `locals.inc`. - **Hardware registers:** all PPU/APU/controller operands use `symbols.inc` names; no raw `$2000`–`$4017` literals remain. - **Data tables:** the named regions below are documented in `prg.s` with their consuming instructions; compressed/opaque spans (Iwata-style sound data, level data) remain as classified `.byte`/`.word` blocks rather than guessy rewrites. ## What is still data, and why The 14,740 bytes of data are the game's tables, and the tracer's own notes say which instruction reads each one. The large ones: | Region | What it is | |---|---| | $B02A-$BE62 (3,641 bytes) | the sound driver: note tables, APU frequency tables and the instrument data the `Sound_*` routines read | | $C434-$CF35 (2,818 bytes) | sprite and tile data, read with `,y`/`,x` indices | | $D9CF-$E0D4 (1,798 bytes) | the 24-entry object-state table and the per-object parameter blocks | | $F025-$F6FC (1,752 bytes) | the character/glyph table (`BAC0123456789:;<=>?PQRQS` is in there) | | $FC78-$FFFF (904 bytes) | `$FF` padding, with the three vectors at $FFFA | The smaller unreferenced regions ($8B5A-$8D17, $8DA5-$9140, $9F33-$A02A, $E996-$EB03) hold 16-bit tables that point into code the tracer has not reached. Per the no-guessy-rewrites convention they stay as classified data blocks until a consumer is identified. ## Open work - **Promotion** to `NROM//` once ready, as `git mv` (all four milestones Pass 1/2/3 are done; Promotion is the only remaining step). ## Files | File | Purpose | |------|---------| | `prg.s` | Pass 3 disassembly (8,400 instructions + 55 data blocks, all labels named) | | `routinemap.tsv` | Pass 2 address → name map (884 entries) | | `header.s` | iNES header include | | `chr.s` | CHR include | | `prg.bin` | 32,768 bytes (32 KB) PRG image | | `chr.bin` | 32,768 bytes (32 KB) CHR image | | `rom.nes` | byte-identical copy of the original ROM | | `linker.cfg` | HEADER / PRG / CHR memory areas, PRG at $8000 | | `symbols.inc` | NES hardware register names | | `locals.inc` | zero-page aliases (24 semantic + 78 neutral `ZpXX` with reasons) | | `Makefile` | build, byte-identity check, and `make pass1` |