# Mario Bros (JU) — Game Flow from Power-On This document describes the execution flow of the disassembled Mario Bros (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.`). Mario Bros is a two-player platformer: Mario runs, jumps, and throws water balloons at turtles, beetles, paratroopa and piranha plants while the stage scrolls. The engine is built around three ideas that show up everywhere below: * a **frame heartbeat** — the NMI sets a flag and the main loop spends it, and a second flag stops the NMI handler from doing its per-frame work twice; * an **inline dispatch table** — the handler table sits in the middle of the code stream, right after the call that reads it, so the static tracer cannot follow it; * a **script interpreter** — the enemies are driven by byte-coded scripts that the engine walks with a 16-bit pointer. --- ## 1. Reset Vector — CPU $FFFC → $C000 **`Reset`** ($C000–$C04E) | Step | Code | Purpose | |------|------|---------| | 1 | `cld` / `sei` | Clear decimal mode, disable interrupts | | 2 | `lda PPUSTATUS` / `bpl WaitVblank1` | Wait for the first VBlank (PPU warm-up) | | 3 | `stx PPUCTRL` / `stx PPUMASK` | Blank the PPU | | 4 | `dex` / `txs` | Stack pointer to $01FF | | 5 | `ldx Zp70` | Read the warm-start signature | | 6 | $C015–$C025 | Clear RAM $0000–$07FF through the `Zp00`/`Zp01` pointer | | 7 | $C026–$C02B | If the signature was zero (a cold boot) store the marker $5F at `$0500`; a warm start writes the old marker back | | 8 | `jsr InitVramAddress` / `jsr SetVramAddressF400` | Point the PPU at the two VRAM areas the game uses | | 9 | `sta PPUSCROLL` ×2 | Scroll to zero | | 10 | `sty GameRunningFlag` (=$01) | The game is running | | 11 | `lda #$0F` / `sta APUSTATUS` | Enable all five APU channels | | 12 | `lda #$90` / `sta PPUCTRL` / `sta Zp09` | NMI on, background and sprites on; the PPUMASK shadow starts at $06 | | 13 | `lda #$06` / `sta Zp0A` | The PPUMASK shadow's second half | | 14 | `lda #$00` / `sta FramePendingFlag` | No frame is pending yet | Step 7 is the warm-start check: `$0500` 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. --- ## 2. The Main Loop and the Frame Dispatch **`ClearFramePending`** ($C04F–$C07C) is the main loop: ``` ClearFramePending: lda GameRunningFlag bne ClearFramePending_Loop ... ClearFramePending_Loop: jsr NopPadding jsr DecFrameCounter jsr UpdateGameplay lda #$01 / sta NmiPhaseFlag EndFrame: lda FramePendingFlag beq EndFrame_Loop inc FrameCounter lda #$00 / sta NmiPhaseFlag jmp ClearFramePending EndFrame_Loop: jsr TestRamFlagPair0500 jmp EndFrame ``` The loop spends the frame-pending flag the NMI sets: it advances the frame counter, runs the gameplay update, and only then clears the flag, so the NMI cannot interrupt the middle of a frame. **`UpdateGameplay`** ($C4B8) is the gameplay work list. It tests the `GameRunningFlag` and the mode index, and then copies the six per-actor parameters from the frame-countdown block into the actor block, walks the actor list, and dispatches each actor's behaviour. --- ## 3. The NMI Handler **`NmiHandler`** ($C07D–$C0B5) ``` NmiHandler: pha / txa / pha / tya / pha lda #$00 / sta OAMADDR lda #$02 / sta OAMDMA lda NmiPhaseFlag beq NmiHandler_Loop jsr UpdateOamShadow jsr DrawStageText jsr DrawHudRow jsr DrawTileRow2 jsr SetScroll jsr ReadControllerPort ``` The handler pushes every register, starts an OAM DMA from `$0200`, and then tests `NmiPhaseFlag` ($22). The flag is set while the main loop is working and cleared by it, so the per-frame work (the OAM shadow, the stage text, the HUD, the tile row, the scroll and the 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`**-style dispatch is not used here, but the *inline table* idiom is: the handler pointers sit in the middle of the code stream, so the static tracer cannot follow them, and the Code Data Logger can only tell that the bytes were fetched. The main table is `GameStateHandlerTable` ($C510) and its continuation blocks ($C520, $C527, $C595, $C599, $C5A2) — twelve 16-bit handler pointers that the engine walks with the index in `Zp41`. The table is interrupted by the four bytes at $C595–$C598, which the Code Data Logger saw as data. The other dispatch is the **script interpreter**. `SetupScriptPointer` ($CB85) takes a table base and a byte offset, `AdvanceScriptPointer` ($CBAE) steps it by $20, and `FetchScriptByte` ($E9FA) reads the next byte of the script. `RunScript` ($E9D3) is the interpreter loop, and the command byte it fetches is compared against $AA and $BB to pick the command's meaning. --- ## 5. The Controller **`ReadControllerPort`** ($CCC5) strobes `$4016` and reads the port; **`ReadControllerBits`** ($CCD3) shifts the eight bits out. The value is compared with the previous one so that a button has to be held before it is accepted, and the result lands in `Zp10`/`Zp11`, the Mario sprite state the NMI handler tests. --- ## 6. The Sound Engine The sound engine lives at the top of the PRG ($F8A7–$FD27) and works from per-note state in the zero page: | Address | What it holds | |---------|---------------| | $D0–$D3 | the per-note state | | $E0 | the envelope counter | | $E4 | the envelope value | | $E5 | the note table index | | $E6 | the note index the current note was read for | | $E9/$EA | the two values the APU sweep registers are loaded with | | $EB–$EF | the envelope state | | $F0/$F1 | the volume table indices | | $F2 | the channel state | | $F3 | the main state byte | | $F8–$FF | the eight note parameters | `SetNoteOffset00`…`SetNoteOffset04` ($F776–$F782) are the four channel entries: each loads an offset, a volume of $40 and the note's X and Y, then copies four bytes into the APU's table. `NoteParameterTable` ($FCC8) holds the note records, `NoteFrequencyTable` ($F904) the frequencies, and `PulseDutyTable` ($F992) the four pulse-channel control values. --- ## 7. Memory Map (Key Zero Page / RAM) | Address | Name | What it holds | |---------|------|---------------| | $20 | `FramePendingFlag` | Set by the NMI, spent by the main loop | | $22 | `NmiPhaseFlag` | The NMI has already run this frame | | $26 | `ModeIndex` | The mode the game is in | | $28 | `ActorActionFlag` | An actor action is running | | $2A | `FrameCountdown` | The frame countdown, reloaded with 10 | | $2C | `ActorType` | The type of the actor being worked on | | $2D | `ActorIndex` | The index of the actor being worked on | | $2F | `FrameCounter` | The frame counter; its low bit is the frame parity | | $30 | `GameRunningFlag` | The game is running | | $33 | `ScriptStepCounter` | The actor scripts count down through their steps | | $34 | `ScriptParam` | The parameter the next script step is called with | | $35 | `ScriptCommand` | The script command byte | | $3F | `PlayerSlot` | The player slot being worked on | | $45 | `ActorYPosition` | The actor's Y position | | $46 | `ActorXPosition` | The actor's X position | | $4A | `ActorYTarget` | The Y position the animation counts towards | | $A0/$A1 | `ScriptPtrLo`/`ScriptPtrHi` | The pointer to the actor script block | | $A2 | `ScriptStep` | The byte offset inside the script block | | $B4 | `ActorPalette` | The four palette bytes | | $B8 | `ActorTileIndex` | The actor's tile index | | $B9 | `ScrollX` | The horizontal scroll position | | $BB | `ActorCount` | The number of actors in use | | $BF | `ActorCommand` | The actor command byte | | $C4 | `ActorState` | The actor's state byte | 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 35 data blocks carry a name (`data_region_names.tsv`): | Address | Size | Name | |---------|------|------| | $C510 | 14 B | `GameStateHandlerTable` — the inline handler pointers | | $C520 | 5 B | `GameStateHandlerTable2` | | $C527 | 1 B | `GameStateHandlerTable3` | | $C595 | 2 B | `GameStateHandlerTable4` | | $C599 | 1 B | `GameStateHandlerTable5` | | $C5A2 | 1 B | `GameStateHandlerTable6` | | $C632 | 4 B | `ReadActorFlag51` | | $CA5A | 4 B | `SetTileIndex86` | | $D491 | 109 B | `EnemyBehaviourTable` | | $D62D | 7 B | `SkipFourBytes` | | $D9B3 | 3 B | `BranchPastEnemy` | | $DDB0 | 6 B | `WriteEnemyActiveFlag` | | $E078 | 64 B | `EnemyTileIndexTable` | | $E0D0 | 89 B | `EnemyScriptTable` | | $E16C | 4 B | `SetActorCounter3` | | $E18C | 4 B | `CheckActorIndex41` | | $E1A4 | 3 B | `WriteRam04C0` | | $E45C | 8 B | `EnemyTypePointerTable` — four 16-bit pointers | | $E494 | 6 B | `EnemyTypePointerTable2` — three pointers | | $E4A4 | 6 B | `EnemyTypePointerTable3` — three pointers | | $E4F1 | 11 B | `InitEnemySpawnBlock` | | $E558 | 118 B | `EnemySpawnScript` | | $E69E | 2 B | `IncRam04C5` | | $E6B8 | 81 B | `EnemySpawnCode` | | $ECCC | 32 B | `SoundEnvelopeTable` | | $ECF5 | 6 B | `JumpToActorScript` | | $EE53 | 23 B | `SoundRoutineTable` — pointers into the sound engine | | $EF67 | 2368 B | `GraphicsDataBlock` | | $F901 | 1 B | `NoteTableTerminator` | | $F903 | 1 B | `NoteTableStart` | | $F904 | 103 B | `NoteFrequencyTable` | | $F992 | 16 B | `PulseDutyTable` | | $FB32 | 6 B | `ReadSoundCounters` | | $FB3D | 1 B | `WriteSoundFlag` | | $FE64 | 412 B | `VectorPageAndSoundData` | --- ## 9. Naming and Residue * **1,025 labels**, no `Label_` residue and no `SubC` residue, and no raw `$XXXX` operand on any `jsr`, `jmp` or branch. * **6,210 instructions / 12,847 bytes = 78.4 %** of the PRG is code; the other 3,522 bytes are data. * Every instruction carries the canonical audit comment `; CPU:$C123 PRG:0123 AD 21 03`, and every one of them matches `prg.bin` byte for byte. * The routine names came from reading this dump's own code. 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. `UpdateGameplay_OnNonzero`. * 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 72.0 % of the PRG as code. A Mesen Code Data Logger capture was taken of this ROM (`mario_mesen.cdl`, 10,915 code bytes) and `_tools/cdl_reclass.py` rewrote the split to match what the CPU actually fetched: **1,067 bytes moved from data to code** (1,010 from the capture and 57 more forced with `--override` because two branch targets — `$E4CB` and `$E639` — are code the capture never reached) and 22 moved the other way. The three branch targets the tool could not prove were turned into labels by `_tools/resolve_raw_branches.py`. The biggest single win is `$D508`–`$D56D`, a hundred bytes the tracer had left as data that decode as a coherent routine, and `$CA3B`–`$CA5D`, which is the tile-drawing routine. --- ## 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 `Mario Bros (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.