# Pac-Man (Tengen) (U) — Game Flow from Power-On This document describes the execution flow of the disassembled **Pac-Man (Tengen) (U)** PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `prg.s` in the repository. The rebuild is verified **byte-identical to the original ROM** — `make` compares the whole `reconstructed.nes` against `rom.nes`. **Variant note:** `Pac-Man (Tengen) (U)` and `Pac-Man (USA) (Namco)` are the same game, but they are **different builds**. This dump was disassembled from scratch; no name, map or file was carried over from the sibling dump, and the two were only compared afterwards. **Build difference from the sibling dump.** The two PRGs differ in **56 bytes in 8 runs**: | Region | Tengen | `Pac-Man (USA) (Namco)` | |--------|--------|------------------------| | $C3E3–$C408 | 26 spaces — the licensee field is blanked | `LICENSED BY NAMCO AMERICA INC` | | $FF00–$FF16 | `$FF` padding — there is **no boot code** | `sei` / `cld` / PPU off / double vblank wait / `jmp ColdStart` | | $FFFC–$FFFD | RESET vector = **$C033** | RESET vector = $FF00 | Everything else is byte-identical, so the two dumps disassemble to the same structure with the same addresses. The CHR is identical; the iNES header differs only in byte 6 (mirroring). **Consequence for the reset path.** In the sibling dump the RESET vector points at $FF00, which holds real boot code. **Here it points at $C033, and the boot sequence is inline at the start of the PRG.** The code is the same, just relocated: ``` $C033 sei / cld / lda #$00 / sta $2000 / sta $2001 $C03D lda $2002 / bpl $C03D ; wait for the first vblank $C042 ldx #$FF / txs / lda #$00 / tay ``` --- ## 1. Reset Vector — CPU $FFFC → $C033 | Vector | Address | Handler | |--------|---------|---------| | NMI | $C0FA | `NmiHandler` | | RESET | $C033 | `Reset` | | IRQ | $C16A | `IrqHandler` (a bare `rti` — unused placeholder) | **`Reset`** ($C033–$C042) performs the cold start: | Step | Code | Purpose | |------|------|---------| | 1–2 | `sei` / `cld` | disable interrupts, clear decimal mode | | 3–4 | `lda #$00` / `sta PPUCTRL` / `sta PPUMASK` | silence the PPU | | 5 | `lda PPUSTATUS` / `bpl` | wait for the first vblank | | 6 | `ldx #$FF` / `txs` | stack pointer to $01FF | | 7 | `lda #$00` / `tay` | A = 0, Y = 0 | Then: | Routine | Address | What it does | |---------|---------|--------------| | `ClearZeroPage` | $C048 | clears $0000–$003D (62 bytes) | | `ClearHighRAMPages` | $C054 | clears 9 pages from $8700 to $8F00 | | `CheckCopyrightSig` | $C077 | compares the 15-byte `ProgrammerSig` at $C0EB with RAM at $0052 | | `CopyrightMismatch` | $C086 | on a mismatch, clears all 256 zero-page bytes | | `CopyCopyrightToRAM` | $C091 | on a match, copies the signature to $0052 and sets `CopyrightVerified` | | `InitGameState` | $C0A0 | clears `GameState`, sets `OamDmaPage` = 7 | | `SwapEntityBuffers` | $C0B2 | swaps two 16-byte entity buffers at $0067 and $0077 | | `InitAudioAndPPU` | $C0C7 | enables the APU, initialises the sound engine, primes the PPUCTRL shadow, then `jmp RequestNMIAndDispatchGameState` | The signature is the string **"HIROKI AOYAGI" + $AA + "U"** — the programmer's name, not a company name. --- ## 2. NMI / Frame Heartbeat — `NmiHandler` ($C0FA) ``` NmiHandler ($C0FA): pha / txa / pha / tya / pha ; preserve A, X, Y lda #$1E / sta PPUMASK lda #$00 / sta OAMADDR / sta NmiFlag lda OamDmaPage / sta OAMDMA ; OAM DMA from $0700 jsr FlushGameplayPpuUpdates jsr ReadNearbyNametableTiles jsr UpdatePalettes lda ScrollX / sta PPUSCROLL lda ScrollY / sta PPUSCROLL lda ActivePlayerFlag / and CocktailModeFlag / and #$01 / asl a ora PpuCtrlShadow / sta PPUCTRL lda #$01 / sta JOY1 / lda #$00 / sta JOY1 ; strobe the controllers ldx #$08 ReadControllers: lda JOY1 / and #$01 / cmp #$01 / ror Player1Input lda JOY2 / and #$01 / cmp #$01 / ror Player2Input dex / bne ReadControllers ldx Player1Input / lda CocktailModeFlag / and ActivePlayerFlag / beq NmiSelectActiveInput ldx Player2Input NmiSelectActiveInput: stx ActiveInput inc FrameCounter lda GameplaySuspended / bne NmiEpilogue jsr RunLevelScript NmiEpilogue: pla / tay / pla / tax / pla / rti ``` The NMI reads **both** controllers into 8-bit shift registers and then picks one: in two-player mode the second controller wins, otherwise the first. `GameplaySuspended` ($FF while the game is running) gates the level script, so the script only runs when the game is *not* suspended. --- ## 3. Game State Dispatch `RequestNMIAndDispatchGameState` ($C23C) sets `NmiFlag` and waits for the NMI to clear it, then reads the game state and dispatches through `GameStateDispatchTable` ($C253): ``` ldy GameState lda GameStateDispatchTable,y sta DispatchPtrLo lda GameStateDispatchTable+1,y sta DispatchPtrHi jmp (DispatchPtrLo) ``` The same indirect-dispatch shape appears three more times — at $D3CA (`DispatchPlayerBehaviour_Dispatch`), $D90F (`DispatchGhostToTable`) and $D9B6 (`DispatchGhostState`) — each with its own table. --- ## 4. Level Graphics | Routine | Address | What it does | |---------|---------|--------------| | `DrawMazeGraphics` | $C27D | writes `MazeTileGraphics` ($C2FD) to VRAM, 16 tiles per block, 6 screens per level | | `WriteAttributeTable` | $C2BE | writes `AttributeTableData` ($C387) — the per-screen colour attributes | | `WriteMazeAttributeTable` | $C2E2 | writes `BorderPattern` ($C432) — 40 bytes to VRAM $23C8 | --- ## 5. Ghosts, Fruit and Level Data | Routine | Address | What it does | |---------|---------|--------------| | `UpdateGhosts` | $C89B | the ghost AI: picks a target, shifts the position block, places the fruit, then `jmp UpdateMazeTiles` | | `UpdateGhostTarget` | $C8AA | finds the pellet nearest the player and records it in $028D | | `ShiftGhostPositions` | $C8ED | moves the 12-byte position block $0022–$002D to $001E–$0029 | | `UpdateFruit` | $C977 | places the fruit from `FruitTargetTable` ($C9FF) | | `GetGhostDataPointer` | $C589 | reads a pointer from `CharacterDataTable` ($C66A) | | `CopyGhostData` | $C597 | copies that data to the $024B buffer until a $FF byte | | `UpdateMazeTiles` | $DAB0 | rewrites the maze tiles through `MazeTileOverlayTable0`–`3` and `MazeTileOffsetTable` | | `ResetGhostSlots` | $D249 | clears the per-ghost slot array at $BA | `CharacterDataTable` ($C66A) holds ten pointers into `CharacterNameStrings` ($C67E), which is where the ROM names its characters: ``` $ C67E " CHARACTER" $ C68A "!M...." "!SBLINKY" $ C692 "!SBLINKY" $ C69B "!AD...." $ C6A3 "!PINKY" "\"INKY" $ C6AB "\"INKY" "\"m...." $ C6B3 "\"m...." "\"sCLYDE" $ C6BA "\"m...." "\"sCLYDE" "\" 10 PTS" $ C6C2 "\"sCLYDE" "\" 10 PTS" "\" 50 PTS" $ C6CA "\" 10 PTS" "\" 50 PTS" "#M..." ``` So the four moving entities the code drives are the four ghosts — Blinky, Pinky, Inky and Clyde — and the ROM itself supplies the names. --- ## 6. Sound and the Level Script `InitAudio` ($EE4B) enables the APU channels and sets up the level-script workspace: a source pointer at $F0/$F1, a working buffer pointer at $F2/$F3, a parameter-table pointer at $F4/$F5 and a state array at $FE. `RunLevelScript` ($EE8F) walks the 16 eight-byte records of the working buffer, copies records around, reads 16-bit parameters with `ReadScriptParam` ($F082) and dispatches a command through `ScriptCommandTable` ($EFDD) with `DispatchScriptCommand` ($EFCC): | Command | Address | What it does | |---------|---------|--------------| | `ScriptCommand_MergeMask3F` | $F00A | merges a parameter into the record with mask $3F | | `ScriptCommand_MergeMaskCF` | $F022 | the same with mask $CF | | `ScriptCommand_MergeMaskF0` | $F03A | the same with mask $F0 | | `ScriptCommand_StoreAt2` | $F052 | stores a byte at record offset 2 | | `ScriptCommand_StoreAt4` | $F062 | stores a byte at record offset 4 | | `ScriptCommand_StoreAt1` | $F072 | stores a byte at record offset 1 | | `ScriptCommand_Store` | $F001 | stores into the source buffer | --- ## 7. Pass 3 data-table inventory Every `; ---- data $XXXX-$YYYY ----` block in `prg.s` is listed here, split further where one block holds more than one table. "Read by" gives the instruction in this dump that fetches the bytes, by address; where the address is written as "none" the block is not fetched by any instruction of this disassembly and the row records its ROM provenance instead. | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | `$C000-$C032` | 51 | `CopyrightText` | ASCII "COPY RIGHT 1984 1980 NAMCO LTD. ALL RIGHTS RESERVED", no terminator (this release drops the Nintendo licence line, so the string is 3 bytes shorter than in the other dump) | none - credit text only; nothing in the dump reads it | | `$C0EB-$C0F9` | 15 | `ProgrammerSignatureString` | ASCII "HIROKI AOYAGI" followed by the sentinel `$AA $55` - the programmer's name | `lda $C0EB,y` at `$C077` (compared byte-for-byte with `$0052,y` for 15 bytes) and `lda $C0EB,y` at `$C091` (copied into `$0052`) | | `$C253-$C258` | 6 | `GameStateDispatchTable` | 3 x 16-bit little-endian code pointers (`$C259`, `$C45A`, `$C4F5`) | `lda $C253,y` at `$C246` and `lda $C254,y` at `$C24B`, index = game state `Zp3F`; the pair is loaded into `Zp10`/`Zp11` and reached by `jmp (Zp10)` at `$C250` | | `$C2FD-$C386` | 138 | `MazeScreenTileData` | 6 rows x 23 CHR tile bytes that make up the maze screen | `lda $C2FD,y` at `$C29C` - 23 bytes per row are written to `PPUDATA` with `PPUADDR` starting at `$20E5`, six rows in all | | `$C387-$C39E` | 24 | `AttractModeVramTextList` | one VRAM write list: address `$2065` then 20 tile bytes, `$FF` ends the list | `lda $C387,y` at `$C2C1`, `$C2C8` and `$C2CF` - (address high, address low, tile...) written to `PPUADDR`/`PPUDATA` | | `$C39F-$C431` | 147 | *(part of `AttractModeVramTextList`)* | ASCII strings terminated by `$FF`: `" 1 PLAYER"`, `"L2 PLAYERS"`, `"©TM AND ] 1980 1984 NAMCO LTD["`, `"©TENGEN"`, `"#&LICENSED BY NINTENDO"`, `"#iOF AMERICA INC["`, ... (the `$ED` byte is the copyright tile) | the same list as above | | `$C412-$C431` | 32 | `InitialPaletteData` | 32 palette bytes: 16 for `$3F00` (background) and 16 for `$3F10` (sprites) | `lda $C412,y` at `$C201` - copied into the palette buffer `$0299` | | `$C432-$C459` | 40 | `NametableAttributeTable` | 40 attribute bytes for VRAM `$23C8-$23EF` (the attribute table of the second nametable) | `lda $C432,y` at `$C2F1` - 40 bytes written to `PPUDATA` after `PPUADDR` is set to `$23C8` | | `$C4EA-$C4F1` | 8 | `TitleScreenVramList` | VRAM write list: address `$220A` with data `$5C $00`, then address `$224A` with data `$20`; `$FF` terminates | `lda $C4EA,y` at `$C462` - copied into the VRAM write buffer `$024B` up to the `$FF` | | `$C4F2-$C55F` | 110 | *(unnamed)* | decodes as instructions (a routine that sets `Zp3F` and enters the dispatcher) | none (unreached code) | | `$C582-$C588` | 7 | *(unnamed)* | `LDA #$01` / `STA Zp47` / `JMP $CA13` | none (unreached code) | | `$C5BB-$C63A` | 128 | *(unnamed)* | decodes as instructions (a PPU initialisation routine) | none (unreached code) | | `$C63B-$C649` | 15 | *(unnamed)* | `LDA #$3F` / `STA $2006` x5 / `RTS` - sets `PPUADDR` to `$3F00` | none (unreached code) | | `$C64A-$C699` | 80 | *(part of `AttractModeVramTextList`)* | a nametable tile stream in which `$0F` is the blank tile and the rest are maze tiles; `$7E` ends it | none in this dump - the same list format as `$C4EA` | | `$C66A-$C6E8` | 127 | `ScreenTextPointerTable` | 35 x 16-bit pointers into the string data that follows at `$C6E9` | `lda $C66A,y` at `$C58B` and `lda $C66B,y` at `$C590`, index = `Zp87`; the pair is loaded into `Zp00`/`Zp01` and the text is copied with `lda (Zp00),y` at `$C597` | | `$C6E9-$C728` | 64 | `GhostSpriteOamTemplateTable` | 16 x 4-byte records (Y, attribute, tile, X) for the ghosts shown on the title screen | `lda $C6E9,y` at `$C5B0` - copied into the OAM shadow `$0760` | | `$C729-$C89A` | 370 | *(part of `AttractModeVramTextList`)* | more VRAM write lists (the "CHARACTER" and ghost-name screens) followed by unreachable code | none in this dump | | `$C997-$C9A9` | 19 | `CharacterSelectDashVramList` | VRAM write list: address `$22AD` followed by eight `$2D` ('-') tiles and a `$00`, then address `$22ED` with six `$2D` tiles and `$FF` | none in this dump - same list format as `$C4EA` | | `$C9AD-$C9B8` | 12 | `GhostTargetAddressTable` | three groups of four bytes, one byte per ghost | `lda $C9AD,y` at `$C8D7` - copied into the per-ghost target array `$028D-$0290`; `lda $C9B1,y` at `$C98A` - four bytes copied into the VRAM write buffer `$024B` | | `$C9FF-$CA12` | 20 | `GhostTargetTileTable` | 20 eight-bit maze tile indices: 0-7 the scatter targets, 8-15 the chase targets, 16-19 the return values | `lda $C9FF,y` at `$C9CD` (index = (animation timer & 7) + entity index, result stored to `$028C`) and at `$C9E8` (index = base + 8, result copied to `$028D-$0290`) | | `$CA96-$CAA7` | 18 | `GameModeHandlerTable` | 9 x 16-bit code pointers | `lda $CA96,y` at `$CA89` and `lda $CA97,y` at `$CA8E`, index = `Zp3F`; `jmp (Zp10)` at `$CA93` | | `$CB1A-$CB25` | 12 | `PauseTextStrings` | two `$FF`-terminated ASCII strings, "PAUSE" and "-----" | `lda $CB1A,x` at `$CAE1` - six bytes copied into the VRAM write buffer `$024D` | | `$CC5B-$CC8E` | 52 | `GhostSpriteShapeData` | 18 bytes of top-row tile indices at `$CC5B`, 16 bytes of bottom-row tile indices at `$CC6D`, a 16-bit pointer at `$CC7F` and a 3-byte row-count table at `$CC85` | `lda $CC5B,x` at `$CB5E`, `lda $CC6D,x` at `$CB64`, `lda $CC7F,x`/`lda $CC80,x` at `$CB47`/`$CB4C`, `lda $CC85,x` at `$CB53` | | `$CC90-$CC9A` | 11 | *(part of `GhostSpriteShapeData`)* | 4 x 16-bit PPU addresses of the ghost eye patterns | `lda $CC90,y` at `$CBF1` and `lda $CC91,y` at `$CBF9`, index = (ghost count - 1) x 2 | | `$CC9B-$CCC4` | 42 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$CDDE-$CE7D` | 160 | *(unnamed)* | decodes as instructions (a routine that clears RAM) | none (unreached code) | | `$CE7E-$CEB1` | 52 | *(part of `GhostSpriteShapeData`)* | 18 bytes of tile indices, eight zero bytes and 12 bytes of palette/attribute data | none in this dump | | `$D0B2-$D0BD` | 12 | `FruitSpawnCounterTable` | 12 bytes copied to `$0032-$003D` | `lda $D0B2,y` at `$D03C` - 12 bytes copied to `Zp32` and up | | `$D0BE-$D0C6` | 9 | `BonusItemVramList` | VRAM write list: address `$2136` then seven tile bytes | `lda $D0BE,y` at `$D02E` - nine bytes copied into the VRAM address buffer `$026B` | | `$D0C7-$D0CF` | 9 | *(part of `BonusItemVramList`)* | nine further tile bytes | none in this dump | | `$D0D0-$D0EF` | 32 | `AlternatePaletteData` | 32 palette bytes (16 background + 16 sprite) | `lda $D0D0,y` at `$CED8` - copied into the palette buffer `$0299` | | `$D11F-$D15E` | 64 | `MazeNametableTileData` | 2 rows x 32 CHR tile bytes; `$55` is the wall tile, `$11` the corridor and `$50`/`$51` the ghost-house door | `lda $D11F,y` at `$D101` - 64 bytes written to `PPUDATA` | | `$D262` | 1 | *(unnamed)* | one byte, `$2B` | none | | `$D32B-$D34A` | 32 | `FruitDefinitionTable` | per-fruit data: the tile index at `$D32B`, the spawn-point X at `$D337`, the spawn-point Y at `$D33B`, and two further per-fruit byte tables at `$D33F` and `$D347` | `lda $D32B,y` at `$D2CE`, `lda $D33B,y` at `$D2DC`, `lda $D337,y` at `$D2E1`, `lda $D33F,y` at `$D319`, `lda $D347,y` at `$D31E`, `lda $D32F,y` at `$D323` | | `$D3CD-$D3D4` | 8 | `GhostMoveHandlerTable` | 4 x 16-bit code pointers | `lda $D3CD,y` at `$D3C0` and `lda $D3CE,y` at `$D3C5`; `jmp (Zp10)` at `$D3CA` | | `$D419-$D420` | 8 | `GhostTurnHandlerTable` | 4 x 16-bit code pointers | `lda $D419,y` at `$D40C` and `lda $D41A,y` at `$D411`; `jmp (Zp10)` at `$D416` | | `$D4C1-$D4C4` | 4 | `GhostDirectionOrderTable` | 4 bytes: the order in which the four ghosts are tried when the player changes direction | `lda $D4C1,x` at `$D362` | | `$D4DE-$D515` | 56 | `PacManAnimationTable` | 28 x 2-byte records (frame duration, direction) | `lda $D4DE,y` at `$D4D1` (the duration) and `lda $D4DF,y` at `$D4D6` (the direction, masked with 3) | | `$D555-$D55E` | 10 | `GhostBehaviourHandlerTable` | 5 x 16-bit code pointers | `lda $D555,y` at `$D548` and `lda $D556,y` at `$D54D`; `jmp (Zp10)` at `$D552` | | `$D55F` | 1 | *(unnamed)* | `RTS` | none (unreached code) | | `$D574-$D57B` | 8 | `GhostTurnDecisionHandlerTable` | 4 x 16-bit code pointers | `lda $D574,y` at `$D567` and `lda $D575,y` at `$D56C`; `jmp (Zp10)` at `$D571` | | `$D5C0-$D5C7` | 8 | `GhostSpeedHandlerTable` | 4 x 16-bit code pointers | `lda $D5C0,y` at `$D5B3` and `lda $D5C1,y` at `$D5B8`; `jmp (Zp10)` at `$D5BD` | | `$D686-$D68D` | 8 | `GhostDeltaVectorTable` | 4 x 2-byte signed (dx, dy) step vectors | `lda $D686,y` at `$D679` and `lda $D687,y` at `$D67E` -> `ZpCD`/`ZpCE` | | `$D69D-$D6A4` | 8 | `GhostFrightenedHandlerTable` | 2 x 16-bit code pointers | `lda $D69D,y` at `$D690` and `lda $D69E,y` at `$D695`; `jmp (Zp10)` at `$D69A` | | `$D6A5-$D6C8` | 36 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$D71B-$D722` | 8 | `GhostTurnOffsetTable` | 4 x 2-byte signed (dx, dy) turn offsets | `lda $D71B,y` at `$D708` and `lda $D71C,y` at `$D710` | | `$D830-$D83F` | 16 | `GhostTurnPriorityTileTable` | 4 groups of 4 bytes: the tile numbers the ghost prefers, in priority order | `lda $D830,y` at `$D7A4` - compared against the four candidate tiles at `$000C-$000F` | | `$D840-$D841` | 2 | `GhostPreferredTilePairTable` | one 16-bit value per ghost (the preferred direction) | `lda $D840,y` at `$D73D` and `lda $D841,y` at `$D742` | | `$D8AB-$D8AE` | 4 | `GhostReverseHandlerTable` | 2 x 16-bit code pointers | `lda $D8AB,y` at `$D89E` and `lda $D8AC,y` at `$D8A3`; `jmp (Zp10)` at `$D8A8` | | `$D912-$D91B` | 10 | `EntityBehaviourHandlerTable` | 5 x 16-bit code pointers | `lda $D912,y` at `$D905` and `lda $D913,y` at `$D90A`; `jmp (Zp10)` at `$D90F` | | `$D91C` | 1 | *(unnamed)* | `RTS` | none (unreached code) | | `$D985-$D98A` | 6 | `FruitScoreOffsetTable` | 6 bytes: the per-fruit score offsets added to the base score | `lda $D985,y` at `$D97C` | | `$D9B9-$D9C2` | 10 | `EntitySpriteHandlerTable` | 5 x 16-bit code pointers | `lda $D9B9,y` at `$D9AC` and `lda $D9BA,y` at `$D9B1`; `jmp (Zp10)` at `$D9B6` | | `$DBAD-$DCC8` | 284 | `CharacterSpriteTileTable` | the CHR tile indices of the five characters (Pac-Man's mouth animation and the four ghosts), 8 bytes per animation frame, with `$4C` marking an unused cell | `lda $DBAD,y` at `$DB1B` and `lda $DCAD,y` at `$DB3E` - the tile goes into the OAM buffer | | `$DCC9-$DE14` | 332 | *(part of `CharacterSpriteTileTable`)* | the per-character attribute bytes and the `$FF` fill of the unused OAM cells | `ora $DCE1,y` at `$DB29`, `ora $DDE1,y` at `$DB4C` | | `$DE15-$DE1C` | 8 | `GhostOamAttributeTable` | 4 x 2-byte (tile, attribute) records, one per ghost | `adc $DE15,x` at `$DAD4`, `ora $DE15,y` at `$DB06` and `$DB6F`, `adc $DE16,x` at `$DB81` | | `$DF2B-$DF3A` | 16 | `GhostEyeOffsetTable` | 8 x 2-byte (x, y) offsets of the ghost's eyes | `lda $DF2B,y` at `$DEE9` and `lda $DF2C,y` at `$DEEF` - written to `PPUADDR` | | `$E002-$E003` | 2 | `FruitSpriteDescriptorTable` | the tile and the repeat count of the bonus item that appears | `lda $E002,x` at `$DFAA` and `lda $E003,x` at `$DFBB` | | `$E004-$E019` | 22 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$E01A-$E021` | 8 | `BonusFruitTileTable` | the CHR tile of each of the eight bonus fruits (`$24` ... `$2B`) | `lda $E01A,y` at `$DFE4` | | `$E17F-$E18A` | 12 | `DigitTileVramList` | 4 x 3-byte records (VRAM address high, address low, tile); the tile is written four times to draw a doubled digit | `lda $E17F,x` at `$E12B`, `$E136` and `$E145` | | `$E341-$E343` | 3 | `ScreenFillTileTable` | the tile used to fill a run of eight cells | `lda $E341,y` at `$E327` - written to `PPUDATA` eight times | | `$E45E-$E46F` | 18 | `SpriteDataPointerTable` | 9 x 16-bit values (`$0061`, `$0070`, `$0080`, ...) used as an indirect pointer into RAM | `lda $E45E,x` at `$E406` and `lda $E45F,x` at `$E40B` -> `Zp02`/`Zp03`, then `lda (Zp02),y` at `$E435` | | `$E4FB-$E511` | 23 | `PlayerSelectTextVramList` | 3 VRAM writes: (address `$2076`, 8 tiles `$B4`...`$BB`), (address `$20F7`, 3 tiles), (address `$2177`, 3 tiles) | `lda $E4FB,y` at `$E4C5`, `$E4D2`, `$E4D9` and `$E4DF` | | `$E659-$E694` | 60 | `ScoreDigitGlyphData` | three tables: 13 bytes of digit-row indices at `$E659`, 8 x 16-bit digit counts at `$E66D`, 24 bytes of 4-bit digit row patterns at `$E67D` | `lda $E659,x` at `$E5D2`, `lda $E66D,y`/`lda $E66E,y` at `$E5E1`/`$E5E6`, `lda $E67D,y` at `$E5ED` | | `$E695-$E9D7` | 837 | *(unnamed)* | decodes as instructions (the score drawing and the game-state routines) | none (unreached code) | | `$EA62-$EA7B` | 26 | `FruitSpriteHandlerTable` | 3 x 16-bit code pointers | `lda $EA62,y` at `$EA55` and `lda $EA63,y` at `$EA5A`; `jmp (Zp10)` at `$EA5F` | | `$EA7C-$EB5D` | 226 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$EB75-$EBF4` | 128 | `LevelGhostConfigIndexTable` | 8 entries, each the index of one of the 33 per-level records in `$EBFF` | `lda $EB75,x` at `$CF27`, `$CF45`, `$CF65`, `$CF6E` and `$CFAA` | | `$EBF5-$EBFE` | 10 | *(part of `LevelGhostConfigIndexTable`)* | 5 x 16-bit values (`$00D0`, `$00C0`, ...) | none in this dump | | `$EBFF-$EE4A` | 332 | `LevelGhostTargetTable` | 33 records x 22 bytes of per-level ghost configuration | `lda $EBFF,y` at `$CF38` - 22 bytes copied to `Zp9F` and up | | `$EC6D-$EC8C` | 32 | `LevelGhostDirectionTable` | 4 records x 8 bytes of per-level ghost direction data | `lda $EC6D,x` at `$CF50` and `$CF55` | | `$EC8D-$EC9A` | 14 | `LevelGhostSpeedTable` | per-level ghost speed data | `lda $EC8D,x` at `$CF73` and `$CF79` | | `$EC9B-$EE4A` | 176 | `LevelGhostTileTable` | the per-level ghost tile and pattern data | `lda $EC9B,x` at `$CF88`, `$CF9A` and `$CFB2` | | `$EFDD-$EFFC` | 32 | `AudioCommandHandlerTable` | 16 x 16-bit code pointers - one handler per sound command | `lda $EFDD,x` at `$EFD0` and `lda $EFDE,x` at `$EFD5`; `jmp (ZpFE)` at `$EFDA` | | `$F0A7-$F0BC` | 24 | `AudioNotePeriodTable` | 12 x 16-bit APU period values - one octave of the note table; each value is 2^(1/12) times the one before it | `lda $F0A7,x` / `lda $F0A8,x` at `$EF73`/`$EF78`, index = (command & $F0) >> 4 | | `$F0BF-$F0C0` | 2 | `AudioSequenceDataPointer` | a 16-bit pointer to the audio sequence data, followed by the first byte of the code that follows | `lda $F0BF` at `$EE5B` and `lda $F0C0` at `$EE60` -> `ZpF4`/`ZpF5` | | `$F052-$F061` | 16 | *(unnamed)* | decodes as instructions (part of the sound engine) | none (unreached code) | | `$F0C1-$FEFF` | 3647 | *(unnamed)* | a mixture of traced code and unreached code (the sound engine and the level scripts) | none as a whole | | `$FF17-$FFF7` | 225 | *(unnamed)* | `$FF` padding filling the rest of the PRG image | none | | `$FFF8-$FFF9` | 2 | *(unnamed)* | the 16-bit pointer `$ABFF` - the start of the packed tile/attribute stream that the routine at `$E2B3` unpacks | `lda $FFF9` at `$E2B3` and `lda $FFF8` at `$E2B8` | | `$FFFA-$FFFF` | 6 | *(unnamed)* | the three 6502 vectors: NMI `$C0FA`, RESET `$C033`, IRQ `$C16A` | the CPU's hardware | ### Notes on the interpretation * The VRAM write lists (`$C387`, `$C4EA`, `$C997`, `$E4FB`, `$E17F`) all share one format: a VRAM address (two bytes) followed by the tile bytes to write there, ended by `$FF`. The routine at `$C2BE` walks the `$C387` list and the routine at `$DF07` walks any list copied into `$024B`, so the same format is used by both. * The `$E659` score table is the one place where the exact meaning of the bytes is not fully established: the 13 bytes at `$E659` select a digit row, the 8 16-bit values at `$E66D` give a cell offset and a repeat count, and the 24 bytes at `$E67D` are 4-bit-wide row patterns for the drawn digit. The three parts are separated by their own consumers, so they are labelled separately, but the digits they draw are not identified. * `$D32B` (fruit) and `$D4DE` (Pac-Man's animation) are likewise read by several instructions each; the per-fruit and per-frame record sizes above are the ones the reading code implies, not a claim about every byte. --- ## 8. Naming and Residue **Pass 1** — 4,595 lines. 1,637 instruction lines covering 3,273 code bytes (20.0%), 842 data lines covering 13,111 bytes, 25 explicit `; ---- data $XXXX-$YYYY` regions, canonical audit comments throughout. Every one of the 2,479 audit comments was checked against `prg.bin` at its stated PRG offset: **0 mismatches**. **Pass 2 complete** — 227 label definitions, **zero `Label_` residue**: 199 routine entries and 28 internal branch targets, named `_`. **No raw `$XXXX` operand** remains on any `jsr`, `jmp` or branch — the four indirect jumps carry their `IndDispatchXX`-style names. The map is in `routinemap.tsv`. **Pass 3** — `locals.inc` holds 30 semantic zero-page aliases, each applied only at the sites where its role is proven (the addresses that carry two roles keep the neutral `ZpXX` name elsewhere; the per-site choice is in `sites.tsv`). **182** alias references are in the source, the rest of the zero-page references keep their neutral names. `apply_symbols.py` reports **0 substitutions pending and 0 residue** — every hardware register is symbolic. **Residue (recorded, not guessed):** - **Pass 1 gap — the unreached main game logic.** The tracer never reaches the block at **$C45A–$C55F**, so it is still `.byte` data. It is code: the tracer's own decoder walks it cleanly, and the dispatch table at $C511 points into it (`$C529` and `$C556`). It holds the input handling, the 32-bit pointer copies, the direction/turn logic and a second dispatch. It cannot be entered without a guess, so it is left as data here — **and the same gap exists in the promoted `Pac-Man (USA) (Namco)` dump**. Other untraced regions with the same character: $C9FF–$D248, $DBAD–$DE3C, $DF2B–$E18C, $E4C1–$E9D7, $EA53–$EB5D, $EB75–$EE4A, $F0A7–$FFFF. Re-deriving them is a Pass 1 job — a Mesen Code Data Logger capture is the tool the repo uses for it (`_tools/cdl_reclass.py`). - **Six command handlers were recovered.** The table `ScriptCommandTable` ($EFDD) is read by the traced instruction at $EFCC, and six of its entries point at $F00A, $F022, $F03A, $F052, $F062 and $F072 — all of which the tracer had left as data, and two of which it had mis-split across the `jmp $EF61` at $F01F and $F04F. Those 120 bytes are code here, decoded straight from the ROM. - 92 zero-page slots keep their neutral name; `locals.inc` lists every one of them with the reason. - The remaining raw absolute operands against RAM addresses stay numeric where their role is only partly traced — the same residue class accepted in `Pinball (JU)` and `Ice Climber (U)`. **Pass 3 data tables done** — every `; ---- data ----` block in the PRG is now interpreted and named in `prg.s`; the full inventory (address, size, name, record structure and the consuming instruction, or the ROM provenance for the blocks nothing reads) is in [§7. Pass 3 data-table inventory](#7-pass-3-data-table-inventory). 58 tables were named; the blocks that no instruction of this dump reads are recorded in the same table as unreferenced data or as code the tracer could not reach. The names replace the earlier guesses in the old §7 list. --- ## 9. Promotion to `NROM/` (2026-10-20) Moved out of `_wip/NROM/` with `git mv`; the build now compares the **whole ROM**, not just the PRG: - `header.s` / `chr.s` added (one-line `.incbin`s of `header.bin` / `chr.bin`) - `linker.cfg` gained the `HEADER` ($0000, $0010) and `CHR` ($0000, $2000) areas ahead of `PRG` ($C000, $4000) - `rom.nes` — symlink to `Pac-Man (Tengen) (U) [!].nes` (the original ROM name contains spaces and brackets) - `Makefile` builds `header.o` / `prg.o` / `chr.o` into `reconstructed.nes` and compares it against `rom.nes` `reconstructed.nes` is 24,592 bytes, MD5 `d34e7a74…28ad0` — equal to the original ROM. The `header.bin` / `prg.bin` / `chr.bin` splits were checked against the original before promotion: they are the ROM's first 16 bytes, its next 16,384 bytes and its last 8,192 bytes, exactly. Build verification: `make` in `NROM/Pac-Man (Tengen) (U)/` produces `reconstructed.nes`, compared against `rom.nes` (**whole-ROM byte-identical**). ## 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 `$C033` (`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 | |---------|---------| | `$C03D` | `WaitForVBlank` (falls through) | | `$C048` | `ClearZeroPage` (falls through) | | `$C054` | `ClearHighRAMPages` (falls through) | | `$C077` | `CheckCopyrightSig` (falls through) | **NMI** is at `$C0FA` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$DE3D` | `FlushGameplayPpuUpdates` | | `$E261` | `ReadGhostPositions` | | `$C16B` | `UpdatePalettes` | **IRQ** is at `$C16A` (`IrqHandler`) - a different address from RESET. --- ## 12. Naming residue — address-derived entry points Every `jsr`/`jmp` target in this project now carries a semantic name except **13**, which keep the honest address-derived `Sub_XXXX` form because the dump does not reveal what they are for: | Label | Address | Why the dump does not settle the name | |--------|---------|-------------------------------------| | `Sub_D06A` | `$D06A` | Writes ten rows of six blank tiles and two `-` tiles into VRAM at `$2256` (`$222A` in cocktail mode). The screen that owns that VRAM block is not identified by anything in this dump, so "the screen that draws dashes" would only rename the code, not say what it is for. | | `Sub_D263` | `$D263` | Reads the entity state array at `$00B8` and compares the entity's coordinates with `$001E`. The entity role (Pac-Man, ghost, fruit) is not distinguishable from the instructions alone. | | `Sub_D91D` | `$D91D` | Scans the entity state array for the entry whose state is `$06` and stores its index at `$0608`. Which entity carries state `$06` is not established by the dump. | | `Sub_DA19` | `$DA19` | Compares the position at `$0274` with the position at `($00)`,y and computes the signed difference; the caller treats it as a distance test but the code does not name the two things being compared. | | `Sub_DE1D` | `$DE1D` | Runs only every sixteenth frame (`$004B & $0F`) and then copies a nibble into `$006C`. The period and the field it feeds are not identified. | | `Sub_DF3B` | `$DF3B` | Dispatches on the byte at `$022A` to four different continuations; the dump does not show what `$022A` counts. | | `Sub_E022` | `$E022` | Sets the three counters `$0088`, `$0089`, `$008A` to `$0F`, `$00`, `$1E`; the dump does not say what those counters measure. | | `Sub_E04D` | `$E04D` | Sets up the pair `$002F/$0002` and `$001E` and then tests the entity state array for state `$04`; the role of the entity in state `$04` is not established. | | `Sub_E3BE` | `$E3BE` | Points the score writer at VRAM `$20B6` (or `$28B6` in cocktail mode) with a stride of `$80` and then falls into the shared "write N score numbers" loop. Whether that block is the 1UP score, the 2UP score or the high score is not settled by the dump. | | `Sub_E3D8` | `$E3D8` | The same score writer, pointed at VRAM `$2083` with a stride of `$09` and at the player-2 score variable. The dump does not identify the screen row the two score blocks occupy. | | `Sub_E512` | `$E512` | Clamps a value to seven and then draws a block of doubled tiles at VRAM `$2317` (or `$231F` in cocktail mode). The value it clamps is a fruit/level index in some form, but the dump does not say which. | | `Sub_E580` | `$E580` | Prepares the digit fields (`$000A`, `$000B`, `$000C`, `$000D`) and clamps the digit count to `$0C` before the score writer runs. The digits it describes are not identified. | | `Sub_E9D8` | `$E9D8` | Adds the 16-bit word at `$001A` to the one at `$0274` and then tests the high byte against `$C0`. The two words are positions of some pair of entities, but which is not established. | All thirteen are reachable entry points whose behaviour the instructions do show; they are kept address-derived rather than given a name that would assert more than the ROM proves.