# Galaga (U) ## Status **Pass 3 complete; promoted.** `prg.s` is a traced disassembly with all address-derived labels replaced, all `jsr`/`jmp` operands symbolic, hardware registers named via `symbols.inc`, and `apply_symbols.py` reporting 0 residue. Code coverage: 11,319 / 32,768 bytes (34.5%). `make` assembles and verifies byte-identical to `rom.nes`. **Pass 3 data tables** — all 14 data blocks are named, interpreted and listed with the instruction that reads each one in *Data-table inventory (Pass 3, re-interpreted 2026-10-21)* below. Nine blocks were renamed in that pass because the consuming code did not support their first-pass names, and the 15,872-byte block at $8000–$BDFF turned out to be erased ($FF) PRG rather than code or data. ## 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` | `DisablePpuRendering` (falls through) | | `$C00A` | `Reset_OnNonnegative` (falls through) | | `$C01C` | `Reset_Loop` (falls through) | | `$C01E` | `Reset_Loop2` (falls through) | **NMI** is at `$C0C8` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$C1EA` | `NmiHandler_Loop5` | | `$C275` | `ApplyPpuScrollAndMask` | **IRQ** is at `$C000` (`Reset`) - the same address as RESET, so the IRQ vector reaches the reset path. ## ROM | Property | Value | |----------|-------| | Format | iNES | | Mapper | 0 (NROM) | | PRG | 32 KB | | CHR | 8 KB | | Mirroring | Horizontal | | NMI vector | $C0C8 | | RESET vector | $C000 | | IRQ vector | $C000 | ## Power-on flow RESET enters at `$C000`: sei ; CPU:$C000 PRG:4000 78 cld ; CPU:$C001 PRG:4001 D8 ldx #$00 ; CPU:$C002 PRG:4002 A2 00 stx PPUCTRL ; CPU:$C004 PRG:4004 8E 00 20 stx PPUMASK ; CPU:$C007 PRG:4007 8E 01 20 lda PPUSTATUS ; CPU:$C00A PRG:400A AD 02 20 bpl Reset_OnNonnegative ; CPU:$C00D PRG:400D 10 FB ldx #$BF ; CPU:$C00F PRG:400F A2 BF txs ; CPU:$C011 PRG:4011 9A lda #$A5 ; CPU:$C012 PRG:4012 A9 A5 cmp ZpFF ; CPU:$C014 PRG:4014 C5 FF bne Reset_Loop ; CPU:$C016 PRG:4016 D0 04 NMI handler at `$C0C8`: pha ; CPU:$C0C8 PRG:40C8 48 txa ; CPU:$C0C9 PRG:40C9 8A pha ; CPU:$C0CA PRG:40CA 48 tya ; CPU:$C0CB PRG:40CB 98 pha ; CPU:$C0CC PRG:40CC 48 ldx Zp4A ; CPU:$C0CD PRG:40CD A6 4A dex ; CPU:$C0CF PRG:40CF CA bne NmiHandler_Loop ; CPU:$C0D0 PRG:40D0 D0 19 ## Address-derived entry points 81 of the 182 `jsr`/`jmp` entry points now carry a semantic name. **101 remain address-derived.** Galaga's program is built around a PPU write queue: a record is assembled in a work area, each field of the record is set by a small helper, and the finished record is queued for the NMI handler to write. Most of the residue is that machinery, and the dump does not say which screen or which entity each helper is drawing, so the `Sub_C` form is kept rather than a name that would only guess. ### The VRAM record machinery (85) `Sub_C075`, `Sub_C156`, `Sub_C18C`, `Sub_C1AA`, `Sub_C335`, `Sub_C339`, `Sub_C345`, `Sub_C38F`, `Sub_C3A1`, `Sub_C3B8`, `Sub_C4EF`, `Sub_C5D4`, `Sub_C607`, `Sub_C693`, `Sub_C6A8`, `Sub_C6E0`, `Sub_C710`, `Sub_C731`, `Sub_C782`, `Sub_C7A8`, `Sub_C7EA`, `Sub_C855`, `Sub_C85D`, `Sub_C886`, `Sub_C8AB`, `Sub_C90B`, `Sub_C997`, `Sub_C9A5`, `Sub_CA09`, `Sub_CA17`, `Sub_CA25`, `Sub_CA91`, `Sub_CAB3`, `Sub_CABB`, `Sub_CB8A`, `Sub_CBA8`, `Sub_CC38`, `Sub_CCF0`, `Sub_CD1F`, `Sub_CD3A`, `Sub_CD95`, `Sub_CD9C`, `Sub_CE4F`, `Sub_CE8B`, `Sub_CFFD`, `Sub_D033`, `Sub_D117`, `Sub_D163`, `Sub_D18B`, `Sub_D210`, `Sub_D25C`, `Sub_D274`, `Sub_D2C9`, `Sub_D56F`, `Sub_D5BE`, `Sub_D60E`, `Sub_D684`, `Sub_D6E9`, `Sub_D703`, `Sub_D736`, `Sub_D739`, `Sub_DA6D`, `Sub_DA73`, `Sub_DAE2`, `Sub_DAF4`, `Sub_DB10`, `Sub_DB35`, `Sub_DB5A`, `Sub_DB5D`, `Sub_DB93`, `Sub_E113`, `Sub_E227`, `Sub_E406`, `Sub_E540`, `Sub_E606`, `Sub_E609`, `Sub_E6F6`, `Sub_E746`, `Sub_E74A`, `Sub_E78E`, `Sub_E7B6`, `Sub_E85A`, `Sub_E86D`, `Sub_E877`, `Sub_EB08` Each of these sets one field of the PPU write record the game is building, or copies one record's field into another. The record's layout - a PPU address, a byte count, a flags byte and a data pointer - is visible in `ParseVramWriteRecord` and `QueuePpuWriteRecordData`, but which of the game's screens a given helper draws is not: the same record shape is used for the score panel, the counter panel, the attract screen and the enemies' sprites. ### The enemy flight paths and behaviours (12) `Sub_D93F`, `Sub_D4AF`, `Sub_D473`, `Sub_D81A`, `Sub_D88C`, `Sub_D893`, `Sub_DF6F`, `Sub_DDD8`, `Sub_D749`, `Sub_D7C7`, `Sub_E09F`, `Sub_E1D2` These walk the tables at `$FB85`, `$FBA1`, `$FC41`, `$FE3D` and `$FEF9` that hold the enemies' tile ids, positions and path tables. The tables themselves are named in the data-table inventory, but the code that chooses between them is the same command-queue machinery, so the role of each helper is not separable from the dump. ### The sound engine (3) `Sub_ED60`, `Sub_ED70`, `Sub_F790` The sound engine lives in the fixed bank below the enemy path tables and is reached through the same pointer table. `Sub_F790` is the only entry point that survives in the traced part of it; the rest of the engine is reached through the pointer table at `$ED9E` and is not covered by this dump. ### The random number generator (1) `Sub_C317` `Sub_C317` is the game's 16-bit LFSR: it shifts the value in `Zp6E`/ `Zp6F` left, folds the carry back in and returns a byte of it. It is used for the enemies' wandering and for the score, and the dump does not separate the uses, so the generator itself is the honest name. ## Data-table inventory (Pass 3, re-interpreted 2026-10-21) Every data block in `prg.s` is listed. "Read by" gives the address of the instruction in *this* dump whose operand lands in the block; where a block is reached through a pointer instead, the address that sets the pointer is given. The 2026-10-21 pass re-derived every row from the consuming code and renamed nine blocks whose first-pass names the code did not support. | Address | Size | Name | Structure | Read by | |---|---|---|---|---| | $8000–$BDFF | 15,872 | `ErasedPrgFill` | Every one of the 15,872 bytes is $FF. This is erased PRG, not code and not data: the Galaga program occupies only $BE00–$FFFF of its 32 KB image, and the CPU never fetches from $8000–$BDFF. | none — unreferenced data | | $BE59–$BEFF | 167 | `ErasedPrgFill2` | $FF padding between the routine that ends at $BE58 and the record table at $BF00. | none — unreferenced data | | $BF00–$BFE3 | 228 | `AttractScreenPpuRecordTable` | Fifteen variable-length PPU-write records. Each record is `[VRAM address low, VRAM address high, byte count, count data bytes]` and the next record starts count+3 bytes later. Records 0–10 write to $28C9, $28E8, $2908, $2928, $294F, $296F, $29CA, $2A0A, $2A80, $2B01 and $2B41 — the second nametable, where the attract-mode screen is laid out. Records 11–14 each write eight bytes to $3F08, i.e. the two background palettes. | the record pointer is set to $BF00 at $BE14/$BE18 and the record is queued by the VRAM-update routine at $C12D; the record walker advances the pointer at $C198–$C1A5 | | $BFE4–$BFFF | 28 | (inside `AttractScreenPpuRecordTable`) | $FF padding to the end of the image. | none — unreferenced data | | $C480–$C49F | 32 | `EnemyFormationXTable` | The 32 X positions of the Galaga formation, eight rows of four columns: $06,$07,$08,$09 / $0C,$0B,$0A,$09 / $06,$05,$04,$03 / $00,$01,$02,$03 / $12,$11,$10,$0F / $0C,$0D,$0E,$0F / $12,$13,$14,$15 / $00,$17,$16,$15. The values are in 8-pixel units. | `lda $C480,x` at $C459 (X = row×4 + column, the column chosen by comparing the sprite's row against $14) | | $CC32–$CC37 | 6 | `HudCounterPositionTable` | Six nametable byte offsets: $00, $02, $04, $60, $62, $64. Added to $F8 they give the six screen positions $21F8, $21FA, $21FC, $2258, $225A and $225C, at which the game draws its two-digit counters. | `lda $CC32,x` at $CBF4 (X counts down from 6) | | $D816–$D819 | 4 | `StageFlagCountTable` | Four byte values $04, $03, $01, $00, indexed by the stage number halved and clamped to 3. The value becomes the counter that decides how many bonus flags the stage is worth. | `lda $D816,y` at $D80D, stored to $0478; $0478 is read back at $E6E4 | | $D9CA–$D9CF | 6 | `EnemySpritePositionTable` | Two parallel one-byte arrays: the sprite attribute at $D9C9+x and the sprite Y position at $D9CC+x. The block label sits at $D9CA, the second byte of the attribute array. | `lda $D9C9,x` at $D99A (attribute) and `lda $D9CC,x` at $D9A1 (Y position) | | $E0F3–$E112 | 32 | `EnemyPathDeltaTable` | Thirty-two signed step values, symmetric about the middle: $B0,$B3,$BB,$C7,$D8,$EB,$00,$15,$28,$39,$45,$4D,$50,$4D,$45,$39,$28,$15,$00,$EB,$D8,$C7,$BB,$B3,$B0,$B3,$BB,$C7,$D8,$EB,$00,$15. Each is added to the enemy's tracked Y position and the low nibble advances the per-enemy index into the table. | `adc $E0F3,y` at $E0DD | | $E1E8 | 1 | `UnreachedByte` | One byte, $E2, in a one-byte gap between two traced routines. | none — unreferenced data | | $E69A | 1 | `UnreachedByte2` | One byte, $02, in a one-byte gap between two traced routines. | none — unreferenced data | | $E7B1–$E7B5 | 5 | `UnreachedCode` | **Not a table.** The bytes decode as instructions — `ORA $0B / ASL $0C / NOP` — so this is code the tracer could not reach. | none | | $E9AA–$EAD9 | 304 | `SpriteAttributeTable` | Several tables side by side. $E9AA–$EA49 is forty 4-byte records `[attribute, tile, tile, tile]`; $EA4A–$EA69 is the formation's 32 Y positions in pixels ($60,$50,$40,$50 / $40,$30,$50,$30 …); $EA6A–$EA87 is a second position table; $EA88–$EAD8 holds three small-value tables ($02–$05, $01–$03, $03–$07). | `lda $E9AA,x` at $E922; `lda $EA4A,x` at $E965; `lda $EA68,x` at $E96B; `lda $EA86,x` at $E97C; `lda $EAAE,x` at $E99D; `lda $EACE,y` at $E9A3 | | $EC7B–$EC84 | 10 | `EnemyBehaviourPointerTable` | Five little-endian 16-bit pointers into the $ED12–$ED47 code: $ED12, $ED23, $ED27, $ED2A, $ED47. | none directly — the block is reached through an indirect pointer | | $ED8E–$ED9D | 16 | `EnemyPathSineTable` | Twelve little-endian 16-bit values on a smooth curve: $F207, $8007, $1407, $AE06, $4306, $F405, $9E05, $4E05, $0205, $BA04, $7604, $3604 — a quarter-wave sine table used for the enemy flight paths. | `lda $ED8E,x` at $EC17 and `lda $ED8F,x` at $EC1C (X = the value masked with $F8, divided by 8) | | $ED9E–$EDAD | 16 | `EnemyPathHandlerPointerTable` | Four little-endian 16-bit code pointers: $EDFD, $F066, $F073, $EE15. | none directly | | $EDAE–$F78F | 2,546 | (inside `EnemyPathSineTable`) | Further byte-coded data. No instruction in this dump references it, so it is recorded as unreferenced rather than guessed at. | none — unreferenced data | | $F7B4–$F7E0 | 45 | `SpriteOamEntryTable` | Fifteen three-byte sprite records `[X, Y, attribute]`: ($42,$57,$1B), ($65,$58,$1B), ($60,$91,$1B), ($3D,$82,$1B), ($4A,$6F,$1B), ($51,$70,$1B), ($58,$71,$1B), ($9D,$28,$1B), ($9D,$3B,$1B), ($B0,$40,$1B), ($BA,$2F,$1B), ($C9,$32,$1B), ($DA,$3C,$1B), ($EA,$3F,$1B), ($46,$2E,$4B). | none directly | | $F7E1–$FA0B | 235 | (inside `SpriteOamEntryTable`) | $FF padding. | none — unreferenced data | | $FA0C–$FA23 | 24 | (inside `SpriteOamEntryTable`) | Twelve two-byte records `[address, length]`: ($00,$00), ($00,$03), ($1A,$02), ($1A,$00), ($34,$00), ($4E,$01), ($68,$01), ($82,$01), ($9C,$02), ($B6,$00), ($D0,$00), ($EA,$00). The address column steps by $16. | `lda $FA0C,y` at $C61B and `lda $FA0C,y+1` at $C621 (Y = twice the sprite index) | | $FA24–$FFFF | 2,092 | (inside `SpriteOamEntryTable`) | Further sprite-definition and graphic data; the tail of the block holds the three interrupt vectors at $FFFA–$FFFF. | `lda $FA28,y` at $C631, `lda $FA58,y` at $C631/$C642/$C6F6, `lda $FA0C,y` at $C61B/$C621, `lda $FA28,y` at $C682 | ### What the 2026-10-21 pass changed - `UnreachedRomData` → `ErasedPrgFill` and `UnreachedRomData2` → `ErasedPrgFill2`: the 15,872 bytes at $8000–$BDFF are **all $FF**. Galaga's program occupies only $BE00–$FFFF of its 32 KB image, so the whole first half of the PRG is erased filler, not code the tracer failed to reach and not a data table. The Mesen CDL capture in this folder agrees: it records no fetch and no read anywhere in $8000–$BDFF. - The block at $BE59–$BFFF was split: the $FF padding keeps the label `ErasedPrgFill2`, and the 228-byte table at $BF00 was named `AttractScreenPpuRecordTable`. - `SpriteHeightTable` → `EnemyFormationXTable`: the 32 values are the X positions of the formation's eight rows of four, not heights. - `SpriteStarCountTable` → `StageFlagCountTable`, `SpriteSpawnOffsetTable` → `HudCounterPositionTable`, `SpriteMovementParameterTable` → `EnemySpritePositionTable`, `ExplosionGraphicTable` → `EnemyPathDeltaTable`, `LevelStarPointerTable` → `EnemyBehaviourPointerTable`, `LevelGraphicData` → `EnemyPathSineTable` (plus a new `EnemyPathHandlerPointerTable` at $ED9E), `SpriteGraphicData` → `SpriteOamEntryTable`. - `SpriteScoreTable` → `UnreachedCode`: the five bytes decode as `ORA $0B / ASL $0C / NOP`, so they are code the tracer could not reach, not a score table. ### Still open - The routine that walks `AttractScreenPpuRecordTable` is reached only through the pointer set at $BE14/$BE18; the code that decides which of the fifteen records are used (the counter in $06 is set to $0B, so only eleven are queued by that path) was not traced. - The three tables inside `SpriteAttributeTable` ($E9AA, $EA4A, $EA88) are read by separate instructions and could be split into three labels; the block was left as one because the byte pattern alone does not show where one table ends and the next begins. - `EnemyPathSineTable`'s tail, $EDAE–$F78F (2,546 bytes), is referenced by no instruction in this dump. ## Build ``` Byte-identical ROM verified. ```