# Pinball (JU) — Pass 1 Disassembly (Pass 2 substantial, Pass 3 drafted) ROM: `_wip/Pinball (JU)/Pinball (JU).nes` (24,592 bytes, iNES). Status: **Pass 1 complete — 59.5% code; Pass 2 substantial but not complete.** `copilot/prg.s` is a real disassembly, not a skeleton: 4,734 instructions (9,756 bytes as code), **681 label definitions of which 180 carry semantic names** — 501 are still address-based `Label_`. `locals.inc` holds **150 neutral `ZpXX` slots and no semantic zero-page aliases** (`locals2.inc` is an empty stub). It builds **byte-identical** at the PRG level (`cmp reconstructed_prg.bin prg.bin`). This header said "Pass 2 complete" from `2ae09f7` (2026-10-18) until the 2026-10-19 correction. Of the five `_wip` projects carrying that claim, Pinball is the closest to it — most of the named routines below really do exist — but 501 `Label_` definitions and all zero-page names remain. See "Pass 2 Status and Map Audit" below. ## Power-on flow analysis Derived from the disassembly by `_tools/derive_poweron.py`; the call graph is walked from the reset entry. **RESET** is at `$C000` (`Reset`). The CPU comes here out of reset with the PPU and APU in their power-on state and the zero page and RAM uninitialised. The reset path calls: | Address | Routine | |---------|---------| | `$C002` | `WaitVBlank_E023_1` (falls through) | | `$C00D` | `ClearZeropage` (falls through) | | `$C01D` | `SubCC01D` (falls through) | | `$C023` | `InitGameState` (falls through) | | `$DCDD` | `UpdateBumperChain` | **NMI** is at `$C1ED` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| **IRQ** is at `$C1ED` (`NmiHandler`) - a different address from RESET. ## ROM Facts | Property | Value | |----------|-------| | Mapper | 0 (NROM) — from the iNES header | | PRG | 16 KB (header claims map at CPU $C000–$FFFF) | | CHR | 8 KB | | Mirroring | Horizontal | | Battery-backed flag | clear | | ROM SHA-256 | `451a201c067bbabad8d762c3fdc2a83cb0b93f26ed0c350cf23bbf486f7addb6` | | PRG SHA-256 | `1d79f41ef62d5045db2c45c9c75766d54c0a84e167d8067bbb6fb90227ac5619` | ## Interrupt Vectors (taken from PRG $3FFA–$3FFF) | Vector | Target | Note | |--------|--------|------| | NMI | `$C1ED` | per-frame heartbeat | | RESET | `$C000` | cold start at $C000 — the usual pattern in this repo | | IRQ | `$C1ED` | identical to the NMI vector — IRQs share the NMI handler | ## `copilot/` Contents | File | Purpose | |------|---------| | `Pinball (JU).nes` | original ROM image (reference) | | `prg.bin` / `header.bin` / `chr.bin` | split image, produced by `_tools/splitnes.py` | | `prg.s` | Pass 1: classified code/data split, 4,734 instructions, 180 semantic + 501 address-based `Label_` definitions, canonical audit comments | | `symbols.inc` | shared NES hardware constants (PPU/APU/controller) | | `locals.inc` | 150 neutral `ZpXX` zero-page slots (Pass 2 naming outstanding) | | `locals2.inc` | empty leftover stub (not included by `prg.s`) | | `pinball_label_map.tsv` / `pinball_label_map2.tsv` / `pinball_full_map.tsv` | Pass 2 draft maps (all three carry the same 210 `label` rows) — **stale**, see the audit below | | `pinball_zp_map.tsv` | Pass 2 zero-page proposals — **not applied** (`locals.inc` is still all `ZpXX`) | | `data_regions.tsv` | Pass 3 data-region draft, 40 named regions — **none applied** | | `linker.cfg` | PRG at $C000, 16 KB, `fill = yes` | | `Makefile` | links `reconstructed_prg.bin` and `cmp`s it against `prg.bin` | | `reconstructed_prg.bin` | build output from the last verified build | Header/CHR are not linked: `copilot/` keeps the raw `header.bin`/`chr.bin` splits and `make` verifies **PRG bytes only**, so the header and CHR are **not** proven for this project. Promotion to the top level adds `header.s` / `chr.s` (as e.g. `Burger Time (U)`), a HEADER/PRG/CHR `linker.cfg`, and a full-ROM `make` target. ## Pass 3 data-table inventory Every data block in the PRG is listed below. "Read by" gives the address of the instruction in this dump that fetches the block; where no instruction reaches the bytes the row says so and records what the bytes are instead. Several blocks that earlier drafts had named with guesses turned out to be **code** the tracer could not reach, or fragments of a table whose other bytes the tracer split off as code; those are recorded here as what they are and are left unnamed in `prg.s`. | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|----------| | `$C16A-$C180` | 23 | `DesignerCreditText` | the designer credit in Shift-JIS: the ASCII text `BY S.MATSUKA & S.IWATA ` followed by two Japanese characters | no instruction in this dump reads it; the text is the game's own credit line | | `$C400-$CBA6` | 1959 | `ScreenTileData` + `ScreenPpuAddressTable` | the screen layout: `$C400`-`$C50A` holds the tile bytes (values `$3A`-`$3E` and `$B9`) and `$C50B`-`$CBA6` holds 784 16-bit **big-endian** PPU addresses (`$2064`, `$2084`, `$20A4`, ... `$2309`); the code writes an address to `PPUADDR` and then the next tile byte to `PPUDATA` | `lda $C50B,x` at `$C2BD` and `$C2C4` (the two halves of the address, high byte first) and `lda $C400,y` at `$C2CF` and `$C2F0` (the tile byte) | | `$CDB5-$CE4D` | 153 | (unnamed) | code the tracer could not reach - it decodes as 74 consecutive instructions | none | | `$CE55-$CE6B` | 23 | (unnamed) | code the tracer could not reach - 11 consecutive instructions | none | | `$D74B-$DB93` | 585 | (unnamed) | a run of byte pairs whose layout is not established: the bytes read as (index, value) pairs such as (`$00`,`$20`), (`$40`,`$01`), (`$21`,`$41`), (`$02`,`$22`) and then a longer run of small values. No instruction in this dump fetches the block, so the pairs cannot be tied to a consumer | none | | `$DB94-$DBB5` | 34 | `PlayfieldEventScript` | a list of two-byte records: the first byte is the value to act on (bit 7 set selects a special action) and the second byte is the counter that decides how long the action lasts; a counter of `$FF` ends the list | `lda $DB94,x` at `$CC81` and `lda $DB95,x` at `$CC88` | | `$DC2D` | 1 | (unnamed) | a single `$2C` byte - filler | none | | `$DCA2-$DCA9` | 8 | `ObjectMaskTable` | four two-byte entries - a value byte and a mask byte: (`$00`,`$00`), (`$80`,`$80`), (`$FF`,`$00`), (`$00`,`$FF`) | `lda $DCA2,x` at `$DC92` and `ldy $DCA6,x` at `$DC95` | | `$DDE5-$DDF0` | 12 | (unnamed) | code the tracer could not reach - 6 consecutive instructions | none | | `$DE03-$DE1A` | 24 | (unnamed) | code the tracer could not reach - 12 consecutive instructions | none | | `$DE24-$DE2F` | 12 | (unnamed) | code the tracer could not reach - 6 consecutive instructions | none | | `$DE87-$DF86` | 256 | `SquaredDistanceTable` | a 256-entry table of `floor(i*i / 256)`: sixteen zeros, then seven ones, five twos, four threes and so on, which is exactly the shape of a squared-distance table for a circle of radius 16 | `lda $DE87,y` at `$DC03`, `lda $DE87,x` at `$DC1B` and `sbc $DE87,x` at `$D348` | | `$DF87-$DFE7` | 97 | `MusicPeriodLowByteTable` | the low byte of the note periods, rising steadily and wrapping at 255: `$04`, `$0A`, `$10`, `$17`, `$1D`, `$23`, ... `$FA`, `$FF`, `$00`, `$06`, `$0D`, ... | `cmp $DF87,x` at `$DC7A` | | `$E048` | 1 | (unnamed) | a single `$2C` byte - filler | none | | `$E2B8-$E3E1` | 298 | `PlayfieldSpriteTable` | three parts: `$E2B8`-`$E2C7` holds eight 16-bit values (`$2212`, `$1D0A`, `$241C`, ...), `$E2C8`-`$E3B1` holds 16-bit little-endian code addresses and `$E3B2`-`$E3E1` holds three-byte sprite records (`$30`,`$0F`,`$30`), (`$25`,`$2A`,`$0F`), (`$30`,`$27`,`$12`), ... - a Y, tile and attribute triple each | `lda $E2CA,x` at `$E10A` and `lda $E2CB,x` at `$E10F` take the low and high byte of the code address for the game state in `ZpBF`; `lda $E2D0,x` at `$E114`, `lda $E2D1,x` at `$E119` and `lda $E2E2,x` at `$E11E` take the other fields; `lda $E3B2,x` at `$C223` reads the sprite records | | `$E482-$E486` | 5 | `PlayfieldObjectTypeTable` | one byte per playfield object; a zero byte means the object is not drawn | `lda $E482,x` at `$E410` (X = the object index in `Zp1B`) | | `$E487-$E48B` | 5 | `PlayfieldObjectTileTable` | the first tile of each object's four-tall sprite; the code adds one, two and three to it to get the other three tiles | `ldy $E487,x` at `$E415` and `lda $E487,x` at `$E45B` | | `$E576-$E58F` | 26 | `SpriteLayoutTable` | the first part of the sprite layout data; the arrays proper start at `$E590` | no absolute operand points at these 26 bytes on its own | | `$E590-$E595` | 6 | `SpritePositionTable` | the sprite X positions (`$70`,`$80`,`$80`,`$78`,`$70`,`$70`,`$80`,`$FF` - `$FF` ends the list) | `lda $E590,x` at `$E4B1` | | `$E596-$E5A1` | 12 | `SpriteTileTable` | the sprite tile indices and attributes, in two parallel six-byte arrays (`$78`,`$A8`,`$98`,`$98`,`$A0`,`$A8` and `$FF`,`$A0`,`$08`,`$00`,`$BF`,`$BF`) | `lda $E593,x` at `$E4B7`, `lda $E596,x` at `$E4BD`, `lda $E5A2,y` at `$E4C3` and `lda $E5A4,x` at `$E4C9` | | `$E787-$E796` | 16 | `FlipperControlTable` | small parallel byte arrays used by the flipper and ball routine - `$E787`, `$E788`, `$E78A`, `$E78D`, `$E7A6` and `$E7A9` are the array bases - holding flags and tile indices (`$81`,`$82`,`$83`,`$84`,`$85`). The block also holds eight bytes of code the tracer could not reach at `$E797` | `lda $E787,x` at `$E6FF`, `lda $E78A,x` at `$E6F9`, `ldy $E78D,x` at `$E738`, `lda $E788,x` at `$E747`, `lda $E7A6,x` at `$E6C6` and `and $E7A9,x` at `$E71B` | | `$EA60-$EAD0` | 113 | `PlayfieldObjectTable` | the per-object playfield data: eight bytes copied into the sprite array at `$028C`, then three parallel six-byte arrays at `$EA68`, `$EA6E`, `$EA74`, `$EA7A` and `$EA80` whose values repeat in a six-byte pattern (`$26`,`$2C`,`$2C`,`$27`,`$2D`,`$27`), then two `$00`-terminated tile lists (`$A6`-`$AD` and `$DD`-`$E3`) | `lda $EA60,y` at `$E7B0`, `lda $EA68,x` at `$E82E`, `lda $EA6E,x` at `$E834`, `lda $EA74,x` at `$E83A`, `lda $EA7A,x` at `$E840` and `lda $EA80,x` at `$E846` | | `$EB5A` | 1 | (unnamed) | a single `$2C` byte - filler | none | | `$EB66-$EB6A` | 5 | `PlayfieldObjectTileIndexTable` | the tile index of each of the five playfield objects (`$09`,`$13`,`$1D`,`$27`,`$31`) | `ldy $EB66,x` at `$EB2E` | | `$EB6B-$EB9C` | 50 | `PlayfieldObjectTileListTable` | five `$00`-terminated lists of tile indices: `$E0`-`$E5`, `$E2`-`$E5`, `$E0`,`$E1`,`$E6`,`$E7`,`$E6`,`$E7`,`$E6`,`$E7`, `$E0`,`$E1`,`$E8`-`$EB`, and `$EC`,`$ED`,`$E8`,`$E9` - the tiles each object is drawn from | `lda $EB6B,y` at `$EB46` | | `$EBE1-$EBF8` | 24 | `ScoreDigitTileTable` | two runs of six tile indices - `$C6`, `$CE` x 6, `$C7`, `$CF` x 6 and `$5E`, `$62` x 5, `$5F`, `$63` x 5 - the digit tiles of the two score displays | `lda $EBE1,y` at `$EBBE` and `$EBD1` | | `$EC4B-$EC5F` | 21 | `HudTileList` | the tile indices of the heads-up display, ending with a `$00` | `lda $EC4B,y` at `$EC32` and `$EC42` | | `$EC60-$EC7F` | 32 | `PaletteTable` | the playfield palette as eight RGB triples: `$70`,`$90`,`$B0`, `$2D`,`$4D`,`$6D`, `$8B`,`$AB`,`$CB`, `$70`,`$90`,`$B0`, `$B0`,`$D0`,`$F0`, `$8B`,`$AB`,`$CB`, `$70`,`$90`,`$B0`, `$33`,`$53`,`$73`,`$97`,`$B7`,`$D7` - every one of them a standard NES palette entry | `lda $EC65,x` at `$EC28` | | `$ECEE-$ED05` | 24 | `HudIconTileList` | three parallel eight-byte tile lists: `$A6`-`$AD`, `$E2`,`$E3` then `$A6` x 6, and `$24`,`$24` then `$DD`-`$E2` | `lda $ECEE,x` at `$ECDB`, `lda $ECF6,x` at `$ECE1` and `lda $ECFE,x` at `$ECE7` | | `$ED2E-$EDAD` | 128 | `SpritePatternAddressTable` + `SpriteTileListTable` + `SpriteTileListIndexTable` | the sprite setup: `$ED2E` holds the 16 high bytes and `$ED3E` the 16 low bytes of the PPU address each sprite's tile list is written to (a big-endian address pair, the high byte first); `$ED4E`-`$ED9D` holds the tile lists themselves, as four-byte (Y, tile, attribute, X) records such as (`$30`,`$24`,`$0D`,`$19`); `$ED9E` holds the index of the list each sprite uses | `lda $ED2E,x` at `$ED0C` and `lda $ED3E,x` at `$ED12` write the address to `PPUADDR`; `ldy $ED9E,x` at `$ED18` and `lda $ED4E,y` at `$ED1F` read the tile list; `lda $EDAE,x` at `$ED08` picks the list | | `$EDE6-$EDEC` | 7 | `HudRowTileData` | seven nametable tile bytes written straight to `PPUDATA` to finish the heads-up display row | `lda $EDE6,x` at `$EDD6` (X = 6..0) | | `$EE37-$EE3C` | 6 | `SoundChannelParameterTable` | the start of three parallel two-byte sound-channel tables at `$EE37`, `$EE39` and `$EE3B` | `ldx $EE37,y` at `$EDF9`, `lda $EE39,y` at `$EDFC`, `lda $EE3B,y` at `$EE04` and `$EE1B`, `ldy $EE3B,x` at `$EE77` and `$EE9F` | | `$EEDB-$EF27` | 77 | `MusicSequenceTable` | four-byte music records; 57 of the 77 bytes are `$00`, so the table is mostly empty | `ldy $EEDB,x` at `$EE50` and `adc $EEDD,x` at `$EE5E` | | `$EFBA-$EFD2` | 25 | `TitleLogoTileTable` | the title logo as a 5x5 matrix of tile indices: the five columns are at `$EFBA`, `$EFBF`, `$EFC4`, `$EFC9` and `$EFCE`, each five tiles deep - `$6D`,`$6F`,`$71`,`$73`,`$75` / `$8D`,`$8F`,`$91`,`$93`,`$95` / `$AD`,`$AF`,`$B1`,`$B3`,`$B5` / `$A6`,`$A8`,`$A9`,`$AA`,`$AB` / `$B1`,`$B2`,`$B3`,`$B4`,`$B5` | `lda $EFBA,x` at `$EF7E` and `lda $EFC9,x` at `$EF84` give the big-endian PPU address the logo is written to; `lda $EFBF,x` at `$EF94`, `lda $EFC4,x` at `$EFA8` and `lda $EFCE,x` at `$EFB3` give the other columns | | `$F0DB-$F0DE` | 4 | (unnamed) | four bytes (`$4C`,`$CF`,`$00`,`$6C`) that decode as two jump instructions; the instruction at `$F0B6` reads them as data, so the tracer split them out of the routine they belong to | `lda $F0DB,x` at `$F0B6` | | `$F220-$F225` | 6 | `SoundChannelVolumeTable` | three parallel two-byte tables of sound-channel parameters at `$F220`, `$F222` and `$F224` | `lda $F220,x` at `$F1FE`, `lda $F222,x` at `$F204` and `$F216`, `lda $F224,x` at `$F210` | | `$F2E7-$F2EF` | 9 | `MusicChannelPeriodTable` | the three music channels' note periods, three bytes each: `$36`,`$3A`,`$3E` / `$00`,`$08`,`$10` / `$84`,`$94`,`$A4` | `lda $F2E7,x` at `$F232`, `ldy $F2EA,x` at `$F2C9` and `lda $F2ED,x` at `$F2D8` | | `$F3B1-$F3B8` | 8 | `SoundEffectParameterTable` | four parallel two-byte sound-effect tables at `$F3B1`, `$F3B3`, `$F3B5` and `$F3B7` | `ldy $F3B1,x` at `$F323`, `lda $F3B3,x` at `$F33A` and `$F37F`, `lda $F3B5,x` at `$F355` and `$F398`, `lda $F3B7,x` at `$F32C` | | `$F416-$F41D` | 8 | `SoundEffectVolumeTable` | four parallel two-byte tables at `$F416`, `$F418`, `$F41A` and `$F41C`; the code copies the pairs into a RAM array at `$02C9` with an eight-byte stride | `lda $F416,x` at `$F3ED`, `lda $F418,x` at `$F3F3`, `lda $F41A,x` at `$F402` and `lda $F41C,x` at `$F408` | | `$F4AC-$FB2B` | 1664 | `MusicNoteDataTable` | the music engine's note data: three-byte rows, the row address being `$F4AC` plus the note index, each row holding the two channels' values and a third byte; further down come the envelope values (`$C0` x 8, `$80`, `$C0`, `$60`, `$30`, `$18`, `$0C`, `$06`, `$03`) | `ldy $F4AC,x` at `$F461`, `lda $F4AE,x` at `$F478`, `lda $F4B0,x` at `$F481` and `lda $F4B2,x` at `$F487` | | `$FB45` | 1 | (unnamed) | a single `$2C` byte - filler | none | | `$FB48` | 1 | (unnamed) | a single `$2C` byte - filler | none | | `$FCA8-$FCB3` | 12 | `NoisePeriodTable` | the twelve noise-channel period values `$0E`,`$0D`,`$0E`,`$0D`,`$0C`,`$0D`,`$0C`,`$0D`,`$0C`,`$0B`,`$0C`,`$0B` | `lda $FCA8,y` at `$FCC8` (Y = the noise note) -> `APUNOISEPERIOD` | | `$FD1E-$FD23` | 6 | (unnamed) | six tile indices (`$8B`,`$8A`,`$8B`,`$82`,`$83`,`$82`); no instruction in this dump reads them | none | | `$FEC6-$FFDB` | 278 | `SoundEngineDataTable` | the sound engine's working data: the block opens with code the tracer could not reach and then holds the per-channel tables the engine indexes at `$FF00`, `$FF01`, `$FF28`, `$FF3F` and `$FF40` | `lda $FF01,y` at `$FB4C`, `lda $FF00,y` at `$FB54`, `lda $FF28,y` at `$FB6A`, `lda $FF3F,y` at `$FDA4` and `$FDA8`, `lda $FF40,y` at `$FDAE` | | `$FFF6` | 1 | (unnamed) | a single `$FF` byte - filler | none | | `$FFFA-$FFFF` | 6 | `CpuVectors` | the NMI, RESET and IRQ vectors | none - the CPU reads them through `$FFFA`-`$FFFF` | ## Build ```bash cd "_wip/Pinball (JU)/copilot" make # → cmp reconstructed_prg.bin prg.bin → "BYTE-IDENTICAL" ``` ## Pass Status | Pass | Status | |------|--------| | Pass 1 | **Complete** — 4,734 instructions, 9,756 bytes as code (59.5%), 6,628 bytes as data, byte accounting exact (16,384) | | Pass 2 | **Substantial, not complete** — 180 of 681 label definitions named (26%); 150 zero-page slots still neutral `ZpXX`; the drafted label map is stale | | Pass 3 | **Data tables interpreted** — every data block in the PRG is named for what it is and listed in the [Pass 3 data-table inventory](#pass-3-data-table-inventory) below with its structure and the instruction that reads it. Ten blocks that earlier drafts had named with guesses turned out to be code the tracer could not reach; they are recorded as such and are left unnamed in `prg.s` | | Promotion | Outstanding — add `header.s`/`chr.s` + HEADER/PRG/CHR `linker.cfg`, switch `make` to a whole-ROM `cmp`, move the `_tools` paths | ## Pass 2 Status and Map Audit (corrected 2026-10-19) The "Pass 2 complete" claim overstated the state in the same way as its siblings — **"196 semantic labels"** is the number of *distinct old names* in the draft map (`pinball_full_map.tsv` has 210 `label` rows covering 196 distinct `Label_` names), while `prg.s` actually names **180** definitions and leaves 501 `Label_`. **"150 zero-page semantic aliases"** is the count of **neutral** `ZpXX` entries in `locals.inc`; there are **zero** semantic zero-page aliases. A handful of the listed routines are also absent (`InitSystem`, `WaitVBlankFrame`, `BallPhysics`, `CheckBallInPocket`, `UpdateBallPosition`, `UpdateScoreDisplay`, `UpdateMusic`), though unlike its siblings most of the list is real. Draft-map audit (`pinball_full_map.tsv`, 210 label rows): | Outcome | Rows | Meaning | |---|---|---| | already applied | 179 | the proposed name already sits at that address | | name conflict | 13 | a different name already sits there | | stale address | 17 | the address is not an instruction boundary in the current `prg.s` | | unlabelled, unreferenced | 1 | an instruction boundary with no label and no operand referencing it | | **safely applicable** | **0** | no row is both insertable and corroborated by control flow | The map is therefore kept only as provenance, and `pinball_zp_map.tsv` / `data_regions.tsv` are unapplied drafts. Completing Pass 2 (and Pass 3) needs maps derived from the committed split. **Still owed:** a real power-on flow analysis (RESET/NMI/main-loop walk) in the style of the top-level project `OVERVIEW.md`s. --- ## Naming residue — address-derived entry points Every `jsr`/`jmp` target in this project now carries a semantic name except **2**, which keep the honest address-derived `SubCCXXXX` form: | Label | Address | Why the dump does not settle the name | |--------|---------|-------------------------------------| | `SubCDCC9` | `$DCC9` | A single `ldx #$02` — an index on its own, with no code of its own to describe. The cartridge gives it nothing else to be named after. | | `SubCDFE8` | `$DFE8` | A single `ldx #$DB` — the index the routine below it works from. The dump does not say what `$DB` selects. | The other 12 entry points are named: `SetPlayfieldTimer`, `ClearPlayfieldFlag`, `UpdatePlayfieldObjects`, `DecrementPlayfieldTimer`, `StartGameState`, `ResetPlayfieldState`, `InitPlayfieldState`, `UpdatePlayfieldState`, `SaveObjectPosition`, `RestoreObjectPosition`, `SplitPositionToTileCoords` and `UpdateGameStateSprites`.