# Hogan's Alley (JU) — Game Flow from Power-On This document describes the execution flow of the disassembled Hogan's Alley (JU) PRG bank (16 KB, mapped at CPU **$C000–$FFFF**), starting from power-on. It is based entirely on `prg.s` in this folder. The rebuild is verified **byte-identical** to the original ROM, header and CHR included (`make` prints `Byte-identical ROM verified.`). Hogan's Alley is a light-gun game: the player shoots at cans, birds and trick-shot targets with the Zapper while the D-pad selects the trick-shot options. The engine is built around three ideas that show up everywhere below: * a **frame heartbeat** — the NMI sets a flag, the main loop spends it; * an **inline dispatch table** — the handler tables sit *inside* the code stream, right after the call that reads them; * a **three-accumulator sound engine** — three zero-page bytes collect per-voice flags, and one routine turns them into APU writes. --- ## 1. Reset Vector — CPU $FFFC → $C000 **`Reset`** ($C000–$C063) | Step | Code | Purpose | |------|------|---------| | 1 | `cld` / `sei` | Clear decimal mode, disable interrupts | | 2 | `lda PPUSTATUS` / `bpl *-3` | Wait for the first VBlank (PPU warm-up) | | 3 | `ldx #$00` / `stx PPUCTRL` / `stx PPUMASK` | Blank the PPU | | 4 | `dex` / `txs` | Stack pointer to $01FF | | 5 | `ldx $05EC` | Read the warm-start signature (the first byte of the four-byte counter array at `$05EC`) | | 6 | $C016–$C025 | Clear RAM $0000–$07FF through the `Zp00`/`Zp01` pointer | | 7 | $C027–$C02C | If the signature was zero (a cold boot) store the marker $5F at `$05EC`; a warm start writes the old marker back | | 8 | `lda #$B6` / `sta AttractSpriteY` | The attract-mode sprite's Y position | | 9 | `jsr InitPpuAddress` / `jsr InitPpuAndOam` | Point the PPU at the OAM shadow and clear it | | 10 | `sta PPUSCROLL` ×2 | Scroll to zero | | 11 | `sty GameMode` (=$01) | Enter the game (mode 1) | | 12 | `lda #$0F` / `sta APUSTATUS` | Enable all five APU channels | | 13 | `jsr AudioInitTrampoline` | Clear the sound engine | | 14 | `lda #$90` / `sta PPUCTRL` / `sta PpuCtrlShadow` | NMI on, background and sprites on | | 15 | `lda #$02` / `sta PpuMaskShadow` | The PPUMASK shadow starts at $02 | | 16 | $C058–$C061 | Copy the 12-byte palette from `$E36A` to `$05F0` | Step 5 is the warm-start check: `$05EC` is read *before* the RAM clear, so a cold boot (signature zero) writes the marker $5F and a warm start writes the old marker back. The other three bytes of the array at `$05EC` are the attract-mode counters. --- ## 2. The Main Loop and the Frame Dispatch **`MainLoop`** ($C07E–$C085) ``` MainLoop: lda GameFramePending bne GameFrameDispatch jsr AttractModeTick jmp MainLoop ``` The loop spins on `GameFramePending` ($20). The NMI sets it; the main loop clears it at the end of the frame (see below). While the flag is clear the game is in attract mode and `AttractModeTick` ($C6BD) advances the four-byte counter array at `$05EC`: it adds the frame counter to the byte at `$05EC`, uses the sum as an index into the table at `$DE52`, and adds that table value to the array. **`GameFrameDispatch`** ($C065–$C07D) is the per-frame work list: 1. `lda GameMode` ($24) — mode 0 is the attract/demo mode. 2. `lda ModeIndex` ($26) — mode 0 runs the gameplay path. 3. `jsr AudioUpdateTrampoline` — let the sound engine advance. 4. `jsr ReadPlayerInput` — read and debounce the controllers. 5. `jsr GameStateDispatch` — run the mode's state machine. 6. `inc Zp2F` — the frame counter. 7. `sta GameFramePending` / `sta NmiPhaseFlag` — release the NMI. **`GameStateDispatch`** ($C0B8–$C0E3) reads the debounced buttons (`Player1Buttons`, $16) and tests bit 4 — `PAD_START` — to start a round. The first press is latched in `$05FC` so the button has to be released and pressed again. The sub-state index (`SubStateIndex`, $25) then selects the round's entry point: sub-states 3, 4, 7 and $0A all enter `InitGameplayRound`, and anything else falls through to the running frame. **`InitGameplayRound`** ($C0E4–$C103) copies the six round parameters from `Zp2A`–`Zp2F` to `$0430`–`$0435`, calls `CopyStackToOam` and `InitRoundState`, sets the actor state to $25, spawns the first target with `SpawnTarget`, saves the sub-state in `SavedSubState` ($1C) and switches the sub-state to 7. **`RunGameplayFrame`** ($C10E–$C117) is the gameplay heartbeat: `UpdateTargets` → `SetSoundFlagBit01` → `AudioUpdateTrampoline` → `SetSoundFlag2Bit01`. --- ## 3. The NMI Handler **`NmiHandler`** ($C088–$C0B7) ``` NmiHandler: php pha / txa / pha / tya / pha lda #$00 / sta OAMADDR lda #$02 / sta OAMDMA lda NmiPhaseFlag bne NmiHandler_Loop jsr UpdateHud jsr SetOamSlot jsr SetScroll jsr ReadControllers jsr CheckScrollFlip ldy #$01 / sty NmiPhaseFlag / sty GameFramePending ``` The handler pushes every register, starts an OAM DMA from `$0200`, and then tests `NmiPhaseFlag` ($22). The flag is set at the end of the handler and cleared by the main loop, so the per-frame work (HUD, OAM shadow, scroll, controller read) happens once per frame and not once per NMI. This is the same guard the other games in this repo use to stop a `$2002` read from re-arming the NMI inside one scanline. --- ## 4. The Dispatch Architecture The engine does **not** use a jump table in the data segment. It uses an **inline** table: the handler table is the data that follows the call, and the call itself pops the address of that data off the stack. **`IndirectCall`** ($C34D–$C362) is the trampoline: ``` IndirectCall: asl a ; the index is a word index tay iny pla ; the low byte of the return address sta HandlerPtrLo ; = the address of the table that pla ; follows the `jsr` sta HandlerPtrHi lda (HandlerPtrLo),y tax iny lda (HandlerPtrLo),y sta HandlerPtrHi stx HandlerPtrLo jmp (HandlerPtrLo) ``` `jsr IndirectCall` pushes the address of the next instruction, which is the first word of the table. The trampoline reads the word at `table + 2*index` and jumps to it. Six call sites use it, and each carries its own table inline: | Call site | Index | Table | Entries | Name | |-----------|-------|-------|---------|------| | $C121 | `SubStateIndex` ($25) | $C124 | 19 | `SubStateHandlerTable` | | $C216 | `ModeIndex` ($26) | $C219 | 4 | `ModeHandlerTable` | | $C899 | `HandlerIndex` ($41) | $C89C | 14 | `HandlerDispatchTable` | | $CDCE | `HandlerIndex` ($41) | $CDD1 | 1 | (inline) | | $D231 | `HandlerIndex` ($41) | $D234 | 2 | `ActorCommandTable` | | $D3D3 | `HandlerIndex` ($41) | $D3D6 | 14 | `TargetBehaviourTable` | Because the tables live in the middle of the code stream, the tracer cannot follow them: every entry is an address the static trace never sees. They were recovered by reading the bytes after each `jsr` and checking that they point at instruction boundaries. The sound engine has two more indirect jumps, `jmp ($0610)` at $F927 and `jmp ($0612)` at $F941, which dispatch on the sound engine's own state. --- ## 5. The Controller and the Zapper **`ReadControllers`** ($C282–$C2B3) strobes `$4016` and shifts eight bits out of the port. The value is compared with the previous one at `$0436`; only after the value has been stable for five frames is the new state written to `Player1Buttons` ($16) — so the button state is debounced, and a second slot at $17 holds the other port. **`ReadZapper`** ($CF3E) reads `$4017`, which is the Zapper port in a light-gun game: bit 3 is the light sensor and bit 4 is the trigger. The trigger bit and the sensor bit are what the shot logic tests. --- ## 6. The Sound Engine The sound engine lives at the top of the PRG ($F776–$FC48) and works from three zero-page accumulators: | Accumulator | Set by | Read by | |-------------|--------|---------| | `SoundFlag0` ($F0) | `SetSoundFlagBit01` … `SetSoundFlagBit80` ($D291–$D2AD) | `AudioCommand` ($F9FF), `AudioCommand3` ($FBF2) | | `SoundFlag1` ($F1) | `SetSoundFlag2Bit01` … `SetSoundFlag2Bit80` ($D2B4–$D2D0) | `AudioState4` ($FC48) | | `SoundFlag2` ($F2) | `SetSoundFlag3Bit01` … `SetSoundFlag3Bit80` ($D2D7–$D2EE) | `AudioState3` ($FAFC) | Each of the twenty-four stubs is a two-instruction routine that loads one bit and ORs it into its accumulator; the names carry the exact bit so the flag layout is visible in the source. `ClearSoundFlag2` ($D2F9) clears $F1. The note records live in `NoteParameterTable` ($FCC8, 812 bytes). `GetNoteIndex` ($F79B) copies nine bytes out of it into `NoteParam0`–`NoteParam7` ($F8–$FF) and `NotePitch` ($E5); `GetVolumeLevel` ($F944) looks a volume up in the table at $F766; `ApuPulse1CtrlTable` ($F992) holds the four pulse-channel control values the engine writes to `$4000`. --- ## 7. Memory Map (Key Zero Page / RAM) | Address | Name | What it holds | |---------|------|---------------| | $00/$01 | *(neutral)* | The general 16-bit working pointer — reused as the RAM-clear pointer, the OAM pointer and the VRAM write address | | $02/$03 | *(neutral)* | The script pointer; also reused for the VRAM address | | $08 | `PpuCtrlShadow` | The `$2000` shadow, written back at $C2E9 and $C312 | | $09 | `PpuMaskShadow` | The `$2001` shadow, written back at $C75F and $C76A | | $14/$15 | `HandlerPtrLo`/`HandlerPtrHi` | The 16-bit pointer the inline dispatch table hands to `jmp ($0014)` | | $16 | `Player1Buttons` | The debounced controller buttons (bit 4 is `PAD_START`) | | $1C | `SavedSubState` | The sub-state saved before a mode switch and restored afterwards | | $20 | `GameFramePending` | Set by the NMI, cleared by the main loop | | $21 | `DrawPendingFlag` | The HUD is waiting to be drawn | | $22 | `NmiPhaseFlag` | The NMI has already run this frame | | $23 | `DisplayListIndex` | The entry to draw next from the list at $E657 | | $24 | `GameMode` | The game mode, tested by the state dispatcher | | $25 | `SubStateIndex` | Selects the word read from the $C124 table | | $26 | `ModeIndex` | Selects the word read from the $C219 table | | $29 | `AttractSpriteY` | The attract sprite's Y position, written to the OAM byte 0 | | $2A | `FrameCountdown` | Counts down from 10; also the first byte of the round-parameter block | | $30 | `AttractTimer` | The attract-mode timer | | $3F | `PlayerSlot` | The player slot being worked on | | $40 | *(neutral)* | The 32-byte OAM shadow at $40–$5F; also used as a counter elsewhere | | $41 | `HandlerIndex` | The index into the inline dispatch tables | | $42/$43 | `ActorX`/`ActorY` | The actor's position, written together by `SetActorPosition` | | $45 | `ActorState` | The current actor's state byte | | $5A | `ActorIndex` | The index of the actor being drawn | | $B0/$B1 | `ActorListCursor`/`ActorFrameCounter` | The cursor into the 16-entry actor list and the counter that advances it | | $F0/$F1/$F2 | `SoundFlag0`/`1`/`2` | The three sound flag accumulators | The full zero-page map, with the reason every neutral `ZpXX` slot was left neutral, is in `zp_aliases.inc`. --- ## 8. Pass 3 Data-Table Inventory 34 data blocks carry a name (`data_region_names.tsv`): | Address | Size | Name | |---------|------|------| | $C124 | 3 B | `SubStateHandlerTable` — the first word of the inline sub-state table | | $C127 | 35 B | `SubStateHandlerTable2` — the rest of it | | $C219 | 2 B | `ModeHandlerTable` — the first word of the inline mode table | | $C21B | 6 B | `ModeHandlerTable2` — the rest of it | | $C89C | 28 B | `HandlerDispatchTable` — 14 handler pointers, inline after the call at $C899 | | $C965 | 13 B | `ActorTileData` | | $C9FB | 5 B | `UnreachedCode01` | | $CB34 | 28 B | `UnreachedCode02` | | $CDF5 | 14 B | `UnreachedCode03` | | $D0DC | 22 B | `UnreachedCode04` | | $D234 | 4 B | `ActorCommandTable` — two pointers, inline after the call at $D231 | | $D2F5 | 4 B | `UnreachedCode05` | | $D3D6 | 28 B | `TargetBehaviourTable` — 14 behaviour pointers, inline after the call at $D3D3 | | $D494 | 2 B | `UnreachedCode06` | | $D6B7 | 3 B | `UnreachedCode07` | | $D894 | 287 B | `UnreachedCode08` | | $DBEC | 36 B | `UnreachedCode09` | | $DC5C | 10 B | `UnreachedCode10` | | $DC91 | 3424 B | `ActorDataBlock` — the actor/tile data, mostly 16-bit pointers | | $E9F1 | 13 B | `ActorEventTable` — read with `lda $E9F1,x` at $D6ED | | $E9FE | 21 B | `ActorEventTable2` | | $EA13 | 2 B | `ActorEventTable3` | | $EA15 | 3425 B | `UnreachedCodeBlock` | | $F7F6 | 25 B | `ApuInstrumentData` | | $F8A5 | 4 B | `UnreachedCode11` | | $F992 | 8 B | `ApuPulse1CtrlTable` — read with `lda $F992,y` at $F9B7 | | $FA44 | 4 B | `VolumeLevelTable` | | $FAB4 | 37 B | `UnreachedCode12` | | $FB3D | 12 B | `UnreachedCode13` | | $FB89 | 8 B | `SoundRateTable` | | $FBEE | 4 B | `SoundRateTable2` | | $FC27 | 8 B | `SoundRateTable3` | | $FCC8 | 812 B | `NoteParameterTable` — the note records the sound engine reads | | $FFFA | 6 B | `Vectors` — NMI $C088, RESET $C000, IRQ $C000 | The `UnreachedCode*` blocks are regions the static trace never reached and the Code Data Logger never observed. They decode as plausible 6502 code, so they are code the game simply never runs in the captured play session; they stay as `.byte` rows with their ROM provenance, which is the Pass 1 convention. --- ## 9. Naming and Residue * **681 labels**, no `Label_` residue, no raw `$XXXX` operand on any `jsr`, `jmp` or branch. * **3,950 instructions / 8,041 bytes = 49.1 %** of the PRG is code; the other 8,343 bytes are data. * Every instruction carries the canonical audit comment `; CPU:$C123 PRG:0123 AD 21 03`, and all 4,490 of them match `prg.bin` byte for byte. * The routine names came from reading this dump's own code. Where a routine's role could not be established, the honest address-derived `SubC` form is used rather than a guess — but no routine needed it: every entry that the engine reaches is named after what it does. * The internal branch targets are derived from the branch that reaches them (`beq` → `OnZero`, `bne` → `OnNonzero`, a backwards branch → `Loop`) and are prefixed with the owning routine, e.g. `UpdateTargets_OnZero`. * The zero-page slots that keep their neutral `ZpXX` name are listed with the reason in `zp_aliases.inc`. ### The Pass 1 correction The static trace alone classified only 41.8 % of the PRG as code. A Mesen Code Data Logger capture was merged with the four captures already in the folder (`hogans_merged.cdl`, the OR of five per-byte flag arrays) and `_tools/cdl_reclass.py` rewrote the split to match what the CPU actually fetched: **1,216 bytes moved from data to code**, five moved the other way, and 59 labels were emitted. The four branch targets the tool could not prove were turned into labels by `_tools/resolve_raw_branches.py`. --- ## 10. Promotion to `NROM/` (2026-10-20) * `git mv` from `_wip/NROM/` to `NROM/`. * `header.s` and `chr.s` added; `linker.cfg` now links the HEADER, PRG and CHR areas, so the whole ROM is compared against `rom.nes`. * `rom.nes` is a symlink to `Hogan's Alley (JU).nes`. * `make` prints `Byte-identical ROM verified.` — the header and the 8 KB of CHR are proven, not just the PRG. There is deliberately **no `make regen` target** here. The Pass 1 split in this project is the static trace *corrected* by a Code Data Logger capture, and the base the correction was applied to is not what the current `_tools/nesdis.py` produces, so a regen pipeline could not reproduce `prg.s` byte for byte. `prg.s` is the maintained source of truth; the tools that produced it are named above.