# Popeye (JU) (PRG 1) — Game Flow from Power-On This document describes the execution flow of the disassembled Popeye (JU) (PRG 1) 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 whole original ROM (`reconstructed.nes` vs `rom.nes`, SHA-256 `00160651…78bd8b4` — promoted from `_wip/NROM/` 2026-10-20). **Companion note:** `Popeye (JU) (PRG 0)` holds a different PRG image of the same title (different mirroring flag: Vertical vs Horizontal). Neither is currently known to be a superset of the other. --- ## 1. Reset Vector — CPU $FFFC → $C649 **`Reset`** ($C649–$C683) **Identical to PRG 0** — On power-on or reset, the 6502 jumps to **$C649** (non-$C000 entry, Donkey Kong Jr. style). The reset routine performs cold-start hardware initialization: | Step | Code | Purpose | |------|------|---------| | 1–2 | `sei` / `cld` | Disable interrupts, clear decimal mode | | 3–4 | `lda #$10` / `sta $2000` | PPUCTRL = $10 (NMI off, sprite $0000, BG $0000, 8×8) | | 5–6 | `ldx #$FF` / `txs` | Initialize stack pointer to $01FF | | 7–11 | `SubCC653_OnZero`: poll `$2002` via `and #$80` / `beq` | Wait for VBlank (bit 7 of PPUSTATUS) | | 12–13 | `ldy #$07` / `sty Zp01` / `ldy #$00` / `sty Zp00` | Zp00/Zp01 = $0700 (RAM clear pointer) | | 14 | `tya` | A = 0 | | 15–19 | `SubCC663_OnNonzero`: `sta (Zp00),y` / `dey` / `bne` / `dec Zp01` / `bpl` | Clear $0000–$07FF (8 pages) | | 20 | `jsr InitPlayerState` | Initialize player/object state at $0500–$0504 | | 21 | `lda #$01` / `sta Zp60` | Zp60 ($60) = 1 (game state) | | 22 | `lda #$00` / `sta Zp63` | Zp63 ($63) = 0 | | 23 | `lda Zp10` / `eor #$80` / `sta $2000` / `sta Zp10` | PPUCTRL ^= $80 (toggle NMI enable) + shadow | | 24 | `IdleWaitForFirstNmi`: `jsr PollJoypadButtonEdge` / `jmp $C680` | Infinite loop: waits for first NMI | **`InitPlayerState` ($C686)** — Player state init (identical to PRG 0): ``` lda #$11 / sta $0500 / sta $0502 lda #$03 / sta $0501 lda #$01 / sta $0503 ldx #$19 / lda #$24 SubCC686_Block_C69C: sta $0504,x ; fill $0504–$051C with $24 (25 bytes) dex / bne SubCC686_Block_C69C ldx #$00 ``` Then falls through to `$C6A4` (main loop entry). --- ## 2. Main Loop — `$C6A4` onward Identical to PRG 0 — main loop runs synchronized with NMI via Zp10/Zp60/Zp63 flags. --- ## 3. Interrupt Vectors — CPU $FFFA–$FFFF Located at PRG offset $3FFA (end of 16 KB bank): | Vector | Address | Handler | |--------|---------|---------| | NMI | $C701 | `NmiHandler` | | RESET | $C649 | `Reset` | | IRQ | $FFF0 | `IrqHandler` (single `sta (Zp16,x)` — undocumented opcode $81, effectively unused) | --- ## 4. NMI / Frame Heartbeat — `NmiHandler` ($C701) **Identical to PRG 0** — The NMI fires once per frame and implements a state machine dispatching on Zp30/Zp31/Zp60/Zp63: ``` NmiHandler ($C701): lda Zp10 ($10) / and #$7F / sta $2000 / sta Zp10 ; PPUCTRL &= ~$80 (NMI off during frame) lda Zp11 ($11) / eor #$18 / sta $2001 ; PPUMASK ^= $18 (toggle bits 3,4) jsr UploadOamViaDma ; sprite/OAM update jsr ApplyVramUpdateQueue ; PPU/scroll update lda $0400 / bne SubCC6FC_Block_C724 ; check $0400 lda $0401 / bne SubCC6FC_Block_C724 ; check $0401 jsr UpdateAttractTimers ; attract mode audio SubCC6FC_Block_C724: jsr UpdateSoundEngine ; gameplay audio jsr ReadControllers ; sound effects lda Zp43 ($43) bne SubCC6FC_Block_C732 lda #$FF / sta Zp43 ; init Zp43 = $FF SubCC6FC_Block_C732: lda Zp30 ($30) beq SubCC6FC_Block_C73C jsr RunAttractDemo / jmp FinishNmiFrame ; state: Zp30≠0 SubCC6FC_Block_C73C: lda Zp31 ($31) beq SubCC6FC_Block_C746 jsr RunEnemyEncounterUpdate / jmp FinishNmiFrame ; state: Zp31≠0 SubCC6FC_Block_C746: lda Zp60 ($60) bne SubCC6FC_Block_C754 lda Zp63 ($63) beq SubCC6FC_Block_C75A jsr RunStageTransitionUpdate / jmp FinishNmiFrame ; state: Zp60=0, Zp63≠0 SubCC6FC_Block_C754: jsr RunGameplayScreen / jmp FinishNmiFrame ; state: Zp60≠0 SubCC6FC_Block_C75A: jsr RunTitleScreen ; state: Zp60=0, Zp63=0 FinishNmiFrame: ... (common NMI exit → rti) ``` **NMI State Machine:** | Condition | Handler | Description | |-----------|---------|-------------| | $0400≠0 or $0401≠0 | `UpdateAttractTimers` ($EE5E) | Attract-mode timers | | Zp30≠0 | `RunAttractDemo` ($CA8B) | attract-mode demo play | | Zp31≠0 | `RunEnemyEncounterUpdate` ($CAD4) | enemy encounter | | Zp60≠0 | `RunGameplayScreen` ($C76A) | the game screen | | Zp60=0, Zp63≠0 | `RunStageTransitionUpdate` ($CB12) | stage transition | | Zp60=0, Zp63=0 | `RunTitleScreen` ($C89B) | title screen | --- ## 5. Key Subsystems ### 5.1 Sprite/OAM System — `UploadOamViaDma` ($ED34) / `ApplyVramUpdateQueue` ($ED20) - `UploadOamViaDma` ($ED34): copies the 256-byte OAM shadow at $0200 to the PPU with OAMDMA - `ApplyVramUpdateQueue` ($ED20): applies the queued VRAM update records and clears the queue ### 5.2 Audio Engine - **Attract:** `UpdateAttractTimers` ($EE5E) (called when $0400/$0401 set) - **Gameplay:** `UpdateSoundEngine` ($F970) (music), `ReadControllers` ($EE89) (controller latch + sound enable) - APU registers at $4000–$4013 ### 5.3 Gameplay Logic - `RunAttractDemo` ($CA8B) / `RunEnemyEncounterUpdate` ($CAD4) / `RunGameplayScreen` ($C76A) / `RunStageTransitionUpdate` ($CB12) / `RunTitleScreen` ($C89B): the five NMI state handlers - Player (Popeye), enemies (Bluto, Sea Hag), projectiles, collectibles (spinach, hearts, letters) ### 5.4 Level Data — $C000–$C648 region - 1609 bytes classified as `.byte` data but referenced by 148+ code instructions - Contains level layouts, enemy paths, platform positions - Referenced via `lda $Cxxx,x`, `bit $Cxxx` — this is **level/map data**, not code ### 5.5 Indirect Dispatch — `$ED03` (`jmp ($0002)`) `jmp ($0002)` at $ED03 — target address in ZP $0002/$0003, set by caller. Not followed statically. (Address differs slightly from PRG 0's $ECFF.) --- ## 6. Memory Map (Key Zero Page / RAM) | Address | Name | Use | |---------|------|-----| | $00–$01 | `Zp00`/`Zp01` | 16-bit pointer (RAM clear) | | $02–$03 | `Zp02`/`Zp03` | Indirect dispatch pointer (`jmp ($0002)`) | | $10 | `Zp10` | PPUCTRL shadow (bit 7 = NMI enable) | | $11 | `Zp11` | PPUMASK shadow (toggled via EOR #$18) | | $30–$31 | `Zp30`/`Zp31` | Gameplay state selectors | | $43 | `Zp43` | Audio state (init $FF) | | $60 | `Zp60` | Game state (set 1 at reset) | | $63 | `Zp63` | Sub-state (cleared at reset) | | $0200 | OAM buffer | Sprite DMA source | | $0400–$0401 | Attract flags | Checked in NMI for audio path | | $0500–$051C | Player state | Initialized at $C686 ($11,$03,$11,$24×25) | --- ## 7. Data Tables | Table | Address | Description | |-------|---------|-------------| | Level/object data | $C000–$C648 | 1609 bytes of level layouts, enemy paths (referenced by code) | | Level VRAM stream | $F08C–$F832 | the byte-code that draws the level into the nametable and the sprite pattern table | | Audio sequences | $F970+ | Music/SFX data | See the Pass 3 data-table inventory below for what every byte of these blocks is and which instruction reads it. --- ## 7a. 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. | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|----------| | $C000–$C648 | 1609 | `LevelObjectDataTable` | the level data: a byte stream of [count][payload] records that places the objects, the enemies (Bluto and the Sea Hag) and the collectibles (spinach, hearts and the treasure letters) on the stage. Three sub-tables sit inside the stream: `$C06D` and `$C06E` are single-byte masks, `$C50F` holds the sprite Y positions and `$C62B` holds 16-bit little-endian pointers. The record layout itself is not established: the routine that walks the stream reaches it through a pointer, never through an absolute operand | `lda $C50F,x` at `$C786` and `$C7FB` (the sprite Y positions), `bit $C06E` at `$C7D7` and `bit $C06D` at `$C7DC` (the masks), `lda $C62B,x` at `$C893` (the pointer pair), `lda $C643,x` at `$C6A4` | | $E3E3–$E3F9 | 23 | (unnamed) | code the tracer could not reach - it decodes as twelve consecutive instructions | none | | $F015–$F029 | 21 | (unnamed) | code the tracer could not reach - it decodes as fourteen consecutive instructions | none | | $F08C–$F832 | 1959 | `LevelVramUpdateStream` | a chain of VRAM update records - [PPU address high][PPU address low][count][count bytes of tile and attribute data] - each chain closed by a `$00` first byte. The first record writes 29 bytes to PPU `$3F00`, the sprite pattern table; the next writes 72 bytes to PPU `$23C0`, the start of nametable 0 | no absolute operand points into the block; the stream is reached through a pointer the level setup routine builds | | $F900–$F96F | 112 | `PulseNotePeriodTable` | the APU pulse channel timer periods: the high byte of each period is at `$F900` and the low byte at `$F901`, so the table is read as two interleaved byte arrays. Further arrays start at `$F950`, `$F958` and `$F967` | `lda $F901,y` at `$F83B` -> `APUPULSE1TIMERLO`, `lda $F900,y` at `$F843` -> `APUPULSE1TIMERHI`, `lda $F950,y` at `$F85F` and `$F8F1`, `lda $F958,y` at `$FB9C`, `lda $F967,y` at `$FBC0` | | $FCCE–$FFEF | 802 | `SoundEngineDataTable` | the sound engine's data: three parallel byte arrays at `$FCCE`, `$FCCF` and `$FCD0`, and further arrays at `$FF09` and `$FF33` | `lda $FCCE,y` at `$FBDB` and `$FBDF`, `lda $FCCF,y` at `$FBE5`, `lda $FCD0,y` at `$FBEA`, `lda $FF09,y` at `$F8C4`, `lda $FF33,y` at `$FB7A` | | $FFF2–$FFF9 | 8 | (unnamed) | eight bytes in front of the vectors (`$14`,`$14`,`$12`,`$12`,`$3A`,`$3A`,`$00`,`$FF`); no instruction in this dump reads them | none | | $FFFA–$FFFF | 6 | `CpuVectors` | the NMI, RESET and IRQ vectors | none - the CPU reads them through `$FFFA`–`$FFFF` | --- ## 8. Summary of Execution Flow **Identical to PRG 0** — See `Popeye (JU) (PRG 0)/README.md` for full flow diagram. The only differences between PRG 0 and PRG 1 are: 1. **Mirroring:** PRG 0 = Vertical (header byte 6 = `$01`), PRG 1 = Horizontal (header byte 6 = `$00`) 2. **iNES header cleanup:** PRG 0 carries `NI2.1` junk in header bytes 10–15; PRG 1's header is clean zeros 3. **PRG bytes differ slightly** — Different build/revision 4. **Indirect dispatch address:** PRG 0 at $ECFF, PRG 1 at $ED03 5. **Seeded routine entries:** PRG 0 has `$EF19, $EF3A, $EFBF, $EFE0, $F002, $F071`; PRG 1 has `$EF1D, $EF3E, $EFC3, $EFE4, $F006, $F057` (offset by a few bytes) --- ## 9. Naming and Residue **Pass 1 complete** — 14,855 lines, canonical audit comments, 72.3% code. 6 seeded routine entries from table references. **Pass 2 complete** — all 876 `Label_` definitions renamed: 2 semantic entry names (`Reset`, `NmiHandler`) + 315 `SubC` entry points + 561 `_Block_` internal branch targets. No `Label_` remains. Every `jsr`/`jmp` target is a named routine. **Entry-point naming (2026-10-21)** — all **297** address-derived entry points (`SubC`) have been given semantic names; **0 remain address-derived**. This bank is the same game as `Popeye (JU) (PRG 0)` and carries the same code with a few addresses shifted by +3 to +6 bytes in the `$C7xx`–`$CFxx` range, so the names are the ones worked out on PRG 0 and transferred routine by routine. The main groups are: | Area | Names | |------|-------| | reset / NMI | `InitPlayerState`, `ResetVramUpdateQueue`, `ClearScreenAndSpritePalettes`, `ResetPpuControlAndScroll`, `DisableRendering`, `ClearOamBuffer`, `FinishNmiFrame`, `IdleWaitForFirstNmi` | | NMI state handlers | `RunGameplayScreen`, `RunTitleScreen`, `RunAttractDemo`, `RunEnemyEncounterUpdate`, `RunStageTransitionUpdate` | | level / screen setup | `BeginLevelLoad`, `LoadLevelLayout`, `LoadLevelPalette`, `BuildTitleScreenOne`, `BuildTitleScreenTwo`, `BuildLevelScreenZero`, `BuildLevelScreenOne`, `BuildLevelScreenTwo`, `StartNextLevel` | | object / sprite system | `AddOamSprite`, `WriteOamEntries`, `ReserveSpriteSlots`, `ReserveObjectSlot`, `HidePlayerSprites`, `HidePlayerSpriteSlots`, `PlaceAndActivateObject`, `ResetObjectAnimationState` | | VRAM | `QueueVramBlock`, `QueueVramBlockFromPointer`, `QueueVramBlockFromLevelTable`, `QueueVramTileRecord`, `QueueLevelTileBlock`, `QueueLevelTextRow`, `QueueLevelTextTable`, `QueueTextRowFromTable`, `ApplyVramUpdateQueue`, `ApplyVramUpdateStream` | | collision | `TestObjectBoxOverlap`, `TestObjectBoxOverlapRelative`, `TestObjectBoxOverlapAbsolute`, `CheckBoxOverlap`, `TestHitbox`, `TestHitboxTable0`…`TestHitboxTable7`, `CheckCollectiblePickup`, `CheckObjectOverlap`, `CheckWallCollision`, `IsPositionOffScreen` | | player | `UpdatePlayerMovement`, `UpdatePlayerCarryAnim`, `StartPlayerThrow`, `StartPlayerThrowAnim`, `StartCarryAnimation`, `CancelPlayerAction`, `AdvancePlayerAnimationFrame`, `FacePlayerRight`, `FacePlayerLeft`, `FaceTowardTarget` | | screen state machine | `RunScreenStateMachine`, `HandleScreenState01`…`HandleScreenState80`, `HandleScreenStateBit40`, `HandleScreenStateDefault` | | PPU / hardware | `UploadOamViaDma`, `ClearNameTableTopAndPalettes`, `ReadControllers`, `PollJoypadButtonEdge`, `UpdateAttractTimers`, `MergeJoypadDirections`, `SaveJoypadButtons`, `ToggleAttractMode` | | sound | `UpdateSoundEngine`, `UpdateSoundEngineFrame`, `AdvanceSoundSequence`, `AdvanceSoundEffectSequence`, `SetPulse1ControlAndNote`, `WritePulse1NoteTimer`, `WritePulse2NoteTimer`, `WriteTriangleNoteTimer`, `LookUpNotePeriod`, `ShiftBitIndex` | The remaining `SubCC653_OnZero` / `SubCC663_OnNonzero` / `…_Block_Cxxx` labels are *internal* branch targets of `Reset` and of the routines above, not entry points; they keep the Pass-2 `_Block_` spelling. **Pass 3 complete** — 4 semantic zero-page aliases (PtrLo/Hi, Ptr2Lo/Hi) added to `locals.inc`; 80 operands substituted via `apply_symbols.py` (15 single + 65 hardware). Every data block in the PRG is now interpreted: the inventory in section 7a gives each block's structure and the instruction that reads it, and the blocks that no instruction reaches are left unnamed and recorded there. The data tables are named `LevelObjectDataTable` ($C000–$C648), `LevelVramUpdateStream` ($F08C–$F832), `PulseNotePeriodTable` ($F900–$F96F), `SoundEngineDataTable` ($FCCE–$FFEF) and `CpuVectors` ($FFFA–$FFFF). Remaining zero-page slots retain neutral `ZpXX` names. **Indirect dispatch** (not followed statically): `jmp ($0002)` at $ED03. The operand keeps its literal because the ROM's form is the *three-byte* absolute indirect jump (`6C 02 00`), and `ca65` cannot spell that with a name: a named zero-page address assembles as the two-byte `jmp (zp)`, and the `a:` prefix that forces the long form is not accepted inside the parentheses. `Ptr2Lo` stands for the same `$02` everywhere else, and the build prints **Byte-identical ROM verified.** **Large data region** — $C000–$C648 (1609 bytes) classified as data but referenced by 148+ code instructions. This is level/enemy pattern data. **PRG 0 vs PRG 1** — Same vectors, same structure. PRG 0: Vertical mirroring. PRG 1: Horizontal mirroring. PRG bytes differ slightly (different build). **Promotion (2026-10-20)** — moved out of `_wip/NROM/` after Pass 2/3: `header.s`/`chr.s` added (`header.bin`/`chr.bin` two-line `.incbin`s), `linker.cfg` extended with the `HEADER` memory area, `rom.nes` added, and `make` switched to a whole-ROM `cmp`. Build verified: `reconstructed.nes` = SHA-256 `0016065132a2dc98afb1181a735edcb9c656b0b89af4e0771e94807ba67bd8b4`, identical to `rom.nes` and the original `Popeye (JU) (PRG 1) [!].nes`. ## 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 `$C649` (`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 | |---------|---------| | `$C653` | `SubCC653_OnZero` (falls through) | | `$C663` | `SubCC663_OnNonzero` (falls through) | | `$C686` | `InitPlayerState` | | `$C680` | `IdleWaitForFirstNmi` (falls through) | | `$C69C` | `SubCC686_Block_C69C` (falls through) | | `$EE78` | `PollJoypadButtonEdge` | | `$C680` | `IdleWaitForFirstNmi` | **NMI** is at `$C701` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$ED38` | `UploadOamViaDma` | | `$ED24` | `ApplyVramUpdateQueue` | | `$EE62` | `UpdateAttractTimers` | **IRQ** is at `$FFF0` (`IrqHandler`) - a different address from RESET.