# Pac-Man (U) — Game Flow from Power-On This document describes the execution flow of the disassembled Pac-Man (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)` is an unrelated dump (different PRG). Both use horizontal mirroring. **RESET vector** — Points to **$FF00** (inside the PRG bank, valid on NROM since the full $C000–$FFFF is mapped). The code at $FF00 performs cold-start initialization: disables interrupts, silences the PPU, waits for vblank twice, then jumps to **ColdStart** at $C042. --- ## 1. Reset Vector — CPU $FFFC → $FF00 → ColdStart ($C042) **`Reset`** ($FF00–$FF14) — On power-on or reset, the 6502 jumps to **$FF00** (valid in NROM since the full $C000–$FFFF bank is mapped). The reset routine performs cold-start hardware initialization: | Step | Code | Purpose | |------|------|---------| | 1–2 | `sei` / `cld` | Disable interrupts, clear decimal mode | | 3–4 | `lda #$00` / `sta $2000` / `sta $2001` | Silence PPU (PPUCTRL=0, PPUMASK=0) | | 5–6 | `lda $2002` / `bpl` ×2 | Wait for VBlank twice | | 7 | `jmp $C042` | Jump to ColdStart | **`ColdStart`** ($C042–$C0A0) — Continues cold-start hardware initialization: | Step | Code | Purpose | |------|------|---------| | 1–2 | `ldx #$FF` / `txs` | Initialize stack pointer to $01FF | | 3–4 | `lda #$00` / `tay` | A=0, Y=0 | | 5–9 | `ClearLowRAM`: `sta a:Zp00,y` / `iny` / `cpy #$3E` / `bne` | Clear $0000–$003D (62 bytes) | | 10–17 | `ClearHighRAMPages`: clear 9 pages from $8700 via Zp00/Zp01 | Clear $8700–$8F00 | | 18–19 | `lda #$06` / `sta $2001` | PPUMASK = $06 (show BG, hide sprites) | | 20–23 | `lda #$00` / `sta $2005` ×2 | PPUSCROLL = 0,0 | | 24–25 | `sta ScrollX` / `sta ScrollY` | ScrollX=ScrollY=0 | | 26–27 | `sta $2000` / `sta $2001` | PPUCTRL=0, PPUMASK=0 | | 28 | `tay` | Y=0 | | 29–37 | `CheckCopyrightSig`: compare 15-byte signature at $C0EB with RAM at $0052 | If match, jump to PostSignatureCheck | | 38–41 | `ClearZPOnMismatch`: clear $0000–$00FF, Y=0 | If mismatch: extra ZP clear | **Copyright check:** 15-byte string "COPYRIGHT 1984 NAMCO LTD. ALL RIGHTS RESERVED" at $C0EB compared against RAM at $0052. On cold start, RAM is garbage so check fails, clearing $0000–$00FF. On warm reset, if signature persists, skips extra clear. **Note:** The code from $C042 onward is **identical** to `Pac-Man (Tengen) (U)` from its `Label_C03D` ($C03D) onward (that dump has not been through Pass 2 yet, so its labels are still address-derived). The only difference is the entry point ($C033 vs $C042) and the initial `sei`/`cld`/`PPU off` which are missing here (possibly executed before jumping to $C042). --- ## 2. Interrupt Vectors — CPU $FFFA–$FFFF Located at PRG offset $3FFA (end of 16 KB bank): | Vector | Address | Handler | |--------|---------|---------| | NMI | $C0FA | `NmiHandler` | | RESET | $FF00 | `Reset` — real boot code inside the PRG bank (see §1), not padding | | IRQ | $C16A | `IrqHandler` (single `rti` — unused placeholder) | **Actual reset entry:** `Reset` ($FF00) → `ColdStart` ($C042). --- ## 3. NMI / Frame Heartbeat — `NmiHandler` ($C0FA) Identical to `Pac-Man (Tengen) (U)` — see that document for full details. Summary: ``` NmiHandler ($C0FA): pha / txa / pha / tya / pha lda #$1E / sta PPUMASK lda #$00 / sta OAMADDR / sta NmiFlag lda OamDmaPage / sta OAMDMA jsr UpdateSprites / jsr UpdateGameLogic / jsr UploadPalette1 lda ScrollX/ScrollY → PPUSCROLL (scroll) lda ActivePlayerFlag/CocktailModeFlag/PpuCtrlShadow → PPUCTRL Controller strobe + 8-bit read (JOY1/JOY2) → Player1Input/Player2Input Select active input → ActiveInput inc FrameCounter (frame counter) GameplaySuspended=0 → jsr UpdateAttractMode (attract) pla / tay / pla / tax / pla / rti ``` --- ## 4. Key Subroutines Identical to `Pac-Man (Tengen) (U)`: - `UpdateSprites` ($DE3D) — Sprite/OAM update - `UpdateGameLogic` ($E261) — Main game logic - `UploadPalette1` ($C16B) — Palette/PPU update (uploads 16 bytes from $0299 when $0299=$0F) - `UpdateAttractMode` ($EE8F) — Attract mode logic - Controller reading: 8-bit shift from JOY1/JOY2 → Player1Input/Player2Input → ActiveInput --- ## 5. Indirect Dispatch Same as Tengen variant: - `jmp (DispatchPtrLo)` at $C250, $D90F, $D9B6, $D3CA - `jmp (AudioHandlerPtrLo)` at $EFDA Not followed statically. --- ## 6. Memory Map (Key Zero Page / RAM) | Address | Name | Use | |---------|------|-----| | $00–$01 | `WorkPtrLo`/`WorkPtrHi` (`Zp00`/`Zp01`) | Generic 16-bit pointer (RAM clear, PPU address setup) | | $10–$11 | `DispatchPtrLo`/`DispatchPtrHi` (`Zp10`/`Zp11`) | Indirect jump pointer for `jmp ($0010)` dispatch | | $12–$13 | `ScoreAccumLo`/`ScoreAccumHi` (`Zp12`/`Zp13`) | BCD score accumulator (16-bit add/sub) | | $14–$15 | `ScoreAddendLo`/`ScoreAddendHi` (`Zp14`/`Zp15`) | BCD score addend/subtrahend | | $3F | `GameState` (`Zp3F`) | Game state / mode selector (indexes dispatch table) | | $40 | `NmiFlag` (`Zp40`) | NMI frame flag (0=wait, 1=frame ready) | | $41–$42 | `ScrollX`/`ScrollY` (`Zp41`/`Zp42`) | PPU scroll X/Y shadows | | $43 | `PpuCtrlShadow` (`Zp43`) | PPUCTRL mirror | | $45 | `OamDmaPage` (`Zp45`) | OAM DMA source page ($07 = $0700 page) | | $46 | `CocktailModeFlag` (`Zp46`) | Cocktail / 2-player mode flag | | $47 | `ActivePlayerFlag` (`Zp47`) | Active player selector | | $48 | `GameplaySuspended` (`Zp48`) | $FF = demo/attract mode, 0 = gameplay active | | $4B | `FrameCounter` (`Zp4B`) | Frame counter (incremented each NMI) | | $4C | `StateTimer` (`Zp4C`) | State timer | | $4D | `Player1Input` (`Zp4D`) | Player 1 controller shift register | | $4E | `Player2Input` (`Zp4E`) | Player 2 controller shift register | | $4F | `ActiveInput` (`Zp4F`) | Currently selected player input | | $50 | `EntityIndex` (`Zp50`) | Current entity slot index (ghost 0-3) | | $51 | `OtherEntityIndex` (`Zp51`) | Other entity slot index for collision checks | | $64 | `CopyrightVerified` (`Zp64`) | Set to 1 after copyright signature copied to RAM | | $B7 | `AnimTimer` (`ZpB7`) | Animation frame timer (incremented per entity) | | $CD–$CE | `EntityDeltaX`/`EntityDeltaY` (`ZpCD`/`ZpCE`) | Entity position delta (subtraction result) | | $E3 | `AnimFrameCounter` (`ZpE3`) | Animation frame counter (decremented per frame) | | $E4 | `AnimFrameIndex` (`ZpE4`) | Animation frame index into tile table | | $EF | `AudioSeqPtr` (`ZpEF`) | Audio sequence data pointer (indirect read) | | $F0–$F1 | `AudioChanPtr1Lo`/`AudioChanPtr1Hi` (`ZpF0`/`ZpF1`) | Audio channel 1 data pointer (indirect write) | | $F2–$F3 | `AudioChanPtr2Lo`/`AudioChanPtr2Hi` (`ZpF2`/`ZpF3`) | Audio channel 2 data pointer (indirect write) | | $FC | `AudioChanIndex` (`ZpFC`) | Audio channel loop index | | $FD | `AudioFrameCounter` (`ZpFD`) | Audio frame counter (incremented by 8 per frame) | | $FE–$FF | `AudioHandlerPtrLo`/`AudioHandlerPtrHi` (`ZpFE`/`ZpFF`) | Audio handler indirect jump pointer (`jmp ($00FE)`) | | $0200 | OAM buffer | Sprite DMA source | | $023F | Palette flag | $FF = palette needs upload | | $0299 | Palette buffer 1 | 16 bytes for $3F00 upload | | $02A9 | Palette buffer 2 | 16 bytes for $3F10 upload | | $0052 | Copyright check | 15-byte RAM signature | | $F0–$FF | `ZpF0`–`ZpFF` | Audio engine workspace | | $0200 | OAM buffer | Sprite DMA source | | $023F | Palette flag | $FF = palette needs upload | | $0245–$024A | Palette buffer 2 | Secondary palette data | | $024B–$0250 | Palette buffer 1 | Primary palette data | | $026B–$0270 | PPU address buffer 1 | VRAM write address | | $0272–$0277 | PPU address buffer 2 | VRAM write address | | $028C–$0293 | Ghost target data | Ghost AI target positions | | $0299 | Palette buffer | 16 bytes for $3F00 upload | | $02A9 | Palette buffer | 16 bytes for $3F10 upload | | $0052 | Copyright check | 15-byte RAM signature | --- ## 7. Data Tables | Table | Address | Description | |-------|---------|-------------| | Copyright string | $C0EB | 15 bytes "COPYRIGHT 1984 NAMCO LTD. ALL RIGHTS RESERVED" | | Palette upload | $0299 | 16 bytes, uploaded when $0299=$0F | --- ## 8. Summary of Execution Flow ``` Power On / Reset │ ▼ (RESET vector points to $FF00 — real boot code in the PRG bank) │ ▼ (sei / cld / PPU off / wait vblank ×2, then jmp ColdStart) ColdStart ($C042) ────► stack=$FF │ ├─ Clear $0000–$003D (62 bytes) ├─ Clear $8700–$8F00 (9 pages via Zp00/Zp01) ├─ PPUMASK=$06, PPUSCROLL=0,0 ├─ ScrollX=ScrollY=0 ├─ PPUCTRL=0, PPUMASK=0 ├─ Copyright check: $C0EB vs $0052 (15 bytes) │ ├─ Match: jump to PostSignatureCheck │ └─ Mismatch: clear $0000–$00FF │ ▼ Main Initialization (PostSignatureCheck $C0A0+) │ ▼ Main Loop (VBlank synchronized) │ ▼ (NMI fires once per frame) NMI ($C0FA) [identical to Tengen variant] ├─ Preserve A,X,Y ├─ PPUMASK=$1E, OAMADDR=0, NmiFlag=0 ├─ OAM DMA from OamDmaPage ├─ jsr UpdateSprites, UpdateGameLogic, UploadPalette1 ├─ Scroll from ScrollX/ScrollY ├─ PPUCTRL from PpuCtrlShadow/CocktailModeFlag/ActivePlayerFlag ├─ Controller strobe + 8-bit read (JOY1/JOY2) → Player1Input/Player2Input ├─ Select active input → ActiveInput ├─ inc FrameCounter (frame counter) ├─ GameplaySuspended=0 → jsr UpdateAttractMode (attract) └─ Restore, rti ``` --- ## 9. Naming and Residue **Pass 1 complete** — 4,511 lines, canonical audit comments, 19.6% code (1,549 audited instruction lines covering 3,058 code bytes, plus 49 `jsr`/`jmp` call sites). 16 seeded routine entries from table references (same as Tengen variant). **Pass 2 complete** — all 194 address-derived `Label_XXXX` definitions replaced with semantic names (routine entries + `__` internal branch targets). No `Label_` remains. All 49 `jsr`/`jmp` call sites resolved from raw `$XXXX` to labels. Zero raw `$XXXX` call operands remain. **Pass 3 partial** — 15 proven semantic zero-page aliases added to `locals.inc` (GameState, NmiFlag, ScrollX/Y, PpuCtrlShadow, OamDmaPage, CocktailModeFlag, ActivePlayerFlag, GameplaySuspended, FrameCounter, StateTimer, Player1/2Input, ActiveInput, CopyrightVerified) and substituted into `prg.s`. **119 operands** substituted in total: 101 hardware registers, 11 single-alias zero-page, 7 multi-alias resolved per site via `sites.tsv` — `apply_symbols.py` now reports **0 substitutions pending and 0 residue**. Key data tables labeled: `ProgrammerSig` ($C0EB), `GameStateDispatchTable` ($C253), `TitleScreenData` ($C2FD), `EntityPtrTable` ($D912), `AudioCommandTable` ($EFDD). Remaining zero-page slots retain neutral `ZpXX` names. **Multi-alias resolution (`sites.tsv`)** — 7 zero-page addresses carry more than one name in `locals.inc`; each site was resolved from its instruction context, not guessed: | Sites | Address | Chosen | Why | |---|---|---|---| | $C250, $D3CA, $D90F, $D9B6 | `$0010` | `DispatchPtrLo` | each `jmp ($0010)` is immediately preceded by `sta DispatchPtrLo` / `sta DispatchPtrHi` loading the dispatch-table entry's address | | $EFDA | `$00FE` | `AudioHandlerPtrLo` | preceded by `sta AudioHandlerPtrLo` / `sta AudioHandlerPtrHi`, loaded from `AudioCommandTable` ($EFDD) | | $C048, $C089 | `$0000` | `Zp00` | the literal zero-page base in the two zero-page clear loops — the page base, not a pointer variable, so the neutral name is used rather than the semantic `WorkPtrLo` | The `a:` prefix survives the substitution, so `sta a:Zp00,y` still assembles to `99 00 00` (`sta abs,Y`) and not `91 00` (`sta zp,Y`). **Indirect dispatch** (not followed statically): - `jmp (DispatchPtrLo)` at $C250, $D90F, $D9B6, $D3CA - `jmp (AudioHandlerPtrLo)` at $EFDA **Unusual RESET vector** — Points to $FF00, which contains real boot code (sei/cld, PPU off, double vblank wait, then `jmp ColdStart`). This is valid in NROM since the full $C000–$FFFF bank is mapped — the vector simply doesn't point at the typical $C000+ entry. **Audit comment alignment** — re-done after the Pass 3 substitutions with the new `_tools/align_audit_comments.py`: the emitter pads every instruction to 31 characters so the audit comment starts at column 35, and a substitution that lengthens an operand pushes the comment right. The tool restores the column; it is whitespace-only, so the byte list in every comment still matches `prg.bin` at the stated PRG offset — all 2,387 audit comments checked (1,549 instruction lines covering 3,058 code bytes, 838 data lines covering 13,179 bytes, and the six vector bytes), 0 mismatches. --- ## 10. Promotion to `NROM/` (2026-10-20) Moved from `_wip/NROM/Pac-Man (U)` 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 (U) [!].nes` (the original ROM name contains a space 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 `a69d310f8cae9397fbd39f204672b5c7` — 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 (U)/` produces `reconstructed.nes`, compared against `rom.nes` (**whole-ROM byte-identical**).