# Magmax (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,613 / 32,768 bytes (35.4%). `make` assembles and verifies byte-identical to `rom.nes`. **Pass 3 data tables done.** Every `; ---- data ----` block in the PRG is now interpreted and named in `prg.s`; the full inventory (address, size, name, record structure and the consuming instruction, or the ROM provenance for the blocks nothing reads) is in [Pass 3 data-table inventory](#pass-3-data-table-inventory). 48 tables were placed; the blocks that no instruction of this dump reads are recorded there as unreferenced data or as code the tracer could not reach. ## 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 `$BC33` (`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 | |---------|---------| | `$BC38` | `Reset_OnNonnegative` (falls through) | | `$BC44` | `Reset_OnNonnegative2` (falls through) | | `$BC91` | `Sub_BC91` | | `$BC95` | `Reset_OnNonzero2` (falls through) | | `$CD78` | `Sub_CD78` | **NMI** is at `$BD33` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$CEAB` | `Sub_CEAB` | **IRQ** is at `$BC33` (`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 | $BD33 | | RESET vector | $BC33 | | IRQ vector | $BC33 | ## Power-on flow RESET enters at `$BC33`: lda #$00 ; CPU:$BC33 PRG:3C33 A9 00 sta PPUCTRL ; CPU:$BC35 PRG:3C35 8D 00 20 lda PPUSTATUS ; CPU:$BC38 PRG:3C38 AD 02 20 bpl Reset_OnNonnegative ; CPU:$BC3B PRG:3C3B 10 FB sei ; CPU:$BC3D PRG:3C3D 78 cld ; CPU:$BC3E PRG:3C3E D8 ldx #$FF ; CPU:$BC3F PRG:3C3F A2 FF txs ; CPU:$BC41 PRG:3C41 9A ldx #$7F ; CPU:$BC42 PRG:3C42 A2 7F lda $057E,x ; CPU:$BC44 PRG:3C44 BD 7E 05 cmp $BC33,x ; CPU:$BC47 PRG:3C47 DD 33 BC bne Reset_Loop ; CPU:$BC4A PRG:3C4A D0 06 NMI handler at `$BD33`: lda #$07 ; CPU:$BD33 PRG:3D33 A9 07 sta OAMDMA ; CPU:$BD35 PRG:3D35 8D 14 40 lda #$00 ; CPU:$BD38 PRG:3D38 A9 00 sta PPUMASK ; CPU:$BD3A PRG:3D3A 8D 01 20 jsr Sub_CEAB ; CPU:$BD3D PRG:3D3D 20 AB CE lda Zp6B ; CPU:$BD40 PRG:3D40 A5 6B sta PPUMASK ; CPU:$BD42 PRG:3D42 8D 01 20 lda Zp6C ; CPU:$BD45 PRG:3D45 A5 6C ## Pass 3 data-table inventory Every `; ---- data $XXXX-$YYYY ----` block in `prg.s` is listed here, split further where one block holds more than one table. "Read by" gives the instruction in this dump that fetches the bytes, by address; where the address is written as "none" the block is not fetched by any instruction of this disassembly and the row records its ROM provenance instead. | Address | Size | Name | Structure | Read by | |---------|------|------|-----------|---------| | `$8000-$BC32` | 15411 | `TerrainLevelData` | the level and terrain data that fills the first 15 KB of the PRG: a stream of control bytes with 16-bit offsets, the screen text, and a number of small tables embedded in it. The sub-tables that have their own consumers are listed in the rows below | the whole block is read from many places; the individual tables are listed below | | `$8000-$8057` | 88 | `TerrainLevelData` | the head of the terrain stream: records of (command byte, 16-bit offset) such as `01 09 02 94`, `01 0A 02 74`, `04 49 50` - the `IP` and `u` bytes are the start of the text that is mixed into the same stream | `lda $8000,x` at `$DB19`, `$DC6C`, `$DEA6`, `$DF52` and `$E2B1`; `lda $8001,x` at `$DA9E`, `$DB34`, `$DC83` and `$E2B7`; `lda $8002,x` at `$DAA7` and `$DB3D`; `lda $8003,y` at `$D784` and `$EBDA` | | `$80CC-$80D3` | 8 | `ObjectSpeedTable` | 8 bytes of per-object speed values | `lda $80CC,x` at `$E00F`, `$E088`, `$E116`, `$E273`, `$E337` and `$E48C` | | `$8115-$811C` | 8 | `ObjectMovementTable` | 8 bytes of per-object movement values | `lda $8115,x` at `$DA13`, `$DEFE`, `$DF91`, `$E024`, `$E038`, `$E09D`, `$E0B1`, `$E130`, `$E28F`, `$E35F` and `$E376` | | `$815E-$8165` | 8 | `ObjectHandlerPointerTable` | 4 x 16-bit code pointers | `lda $815E,y` at `$F8BB` and `lda $815F,y` at `$F8C1` | | `$81AA-$81B1` | 8 | `ObjectSpritePointerTable` | 4 x 16-bit code pointers | `lda $81AA,y` at `$EAAF` and `lda $81AB,y` at `$EAB5` | | `$941A-$9429` | 16 | `SpriteTileTable` | 16 CHR tile indices, one per sprite, written straight to `PPUDATA` | `lda $941A,y` at `$D13D` and `$D164` and `$D2DD` | | `$9504-$9513` | 16 | `BackgroundTileTable` | 16 CHR tile indices for the background layer | `lda $9504,y` at `$D18E` and `$D1B5` and `$D36C` | | `$95EE-$95FD` | 16 | `SpriteAttributeTable` | 16 attribute bytes, one per sprite, written to `PPUDATA` after the tile | `lda $95EE,y` at `$C5A6`, `$D143`, `$D16A` and `$D2E3` | | `$96D8-$96E7` | 16 | `BackgroundAttributeTable` | 16 attribute bytes for the background layer | `lda $96D8,y` at `$D194`, `$D1BB` and `$D372` | | `$A9CA-$A9D3` | 10 | `SoundHandlerPointerTable` | 5 x 16-bit code pointers | `lda $A9CA,y` at `$BEEE` and `lda $A9CB,y` at `$BEF3` | | `$ACC9-$ACD2` | 10 | `MusicHandlerPointerTable` | 5 x 16-bit code pointers | `lda $ACC9,y` at `$BF1F` and `lda $ACCA,y` at `$BF24` | | `$AEF4-$AEFD` | 10 | `LevelHandlerPointerTable` | 5 x 16-bit code pointers | `lda $AEF4,y` at `$BED2` and `lda $AEF5,y` at `$BED7` | | `$AF2C-$AF35` | 10 | `EnemyHandlerPointerTable` | 5 x 16-bit code pointers | `lda $AF2C,y` at `$BF03` and `lda $AF2D,y` at `$BF08` | | `$AF4C-$AF55` | 10 | `SoundCommandDataPointerTable` | 5 x 16-bit pointers to the sound data itself (the code reads them with `lda (Zp62),y`) | `lda $AF4C,y` at `$BF6F` and `$C302`; `lda $AF4D,y` at `$BF74` and `$C307` | | `$BC4C-$BC51` | 6 | *(unnamed)* | bytes that decode as instructions (`JSR $10CA` / `JMP $BC91` / ...) | none (unreached code) | | `$BDC2-$BDD7` | 22 | `ObjectHandlerTable` | 11 x 16-bit code pointers - the per-object dispatch | `lda $BDC2,x` at `$BDA8` and `lda $BDC3,x` at `$BDAD`; `jmp (Zp99)` at `$BDB2` | | `$BDF3-$BDF6` | 4 | *(unnamed)* | bytes that decode as instructions | none (unreached code) | | `$BEA7-$BEAE` | 8 | `ObjectInitData` | 8 bytes of initial values for the object slots (`$1E $1C $1D $0E $10 $12 $0E $16`), copied into `$0616` | `lda $BEA7,x` at `$BC79` | | `$C471-$C47C` | 12 | `EnemyControlTable` | 12 bytes (`00 FF FF FF 00 00 00 02 01 03 01 FF`) read with an index derived from a value in `$0366` | `lda $C471,y` at `$C4CC` | | `$C47D-$C485` | 9 | `EnemySpawnTable` | 9 bytes (`00 FF 00 FF FF 01 01 01 03`) read with an index derived from a value in `$0446` | `lda $C47D,y` at `$C540` | | `$C7C3-$C7F2` | 48 | `SpriteOamTemplateTable` | 16 records of three bytes - the CHR tile and the screen position of each of the 16 OAM entries the game sets up - copied into the OAM buffer at `$0200` | `lda $C7C3,x` at `$C79B`, `lda $C7C4,x` at `$C7A1` and `lda $C7C5,x` at `$C7A7` | | `$C7F3-$C800` | 14 | `TerrainHeightThresholdTable` | 12 threshold bytes (`$10 $1A $2B $2E $20 $26 $34 $3E $20 $26 $34 $3E`) that the terrain height is compared against, then the first two bytes of the next table | `lda $C7F3,x` at `$C5FF` and `lda $C7FF,x` at `$C5DA` | | `$C801-$C860` | 96 | `TerrainSegmentOffsetTable` | 48 records of two bytes - a terrain value and the offset to add to it - used to build the collision list in `$0219`-`$0239` | `lda $C801,x` at `$C686`, `$C69F` and `$C6C4`; `sbc $C802,x` at `$C68F`, `$C6B4` and `$C6CD` | | `$C8AD-$C9AC` | 256 | `TerrainPatternTable` | 16 blocks of 16 CHR tile indices - the 4x4 terrain patterns the game stamps onto the landscape; the block is indexed at 4 x the pattern number, so one 16-byte pattern is picked per terrain type | `lda $C8AD,x` at `$D39D` and `$D3A2` | | `$C9AD-$C9BA` | 14 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$C9BB-$C9BE` | 4 | *(unnamed)* | 2 x 16-bit code pointers (the routine that runs when the pattern table is set up) | `lda $C9BB` at `$C9C7`, `lda $C9BC` at `$C9CC`, `lda $C9BD` at `$C9D8` and `lda $C9BE` at `$C9DD` | | `$C9E6-$C9F7` | 18 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$C9F8-$CA01` | 10 | `GameModeHandlerTable` | 5 x 16-bit code pointers - the game-mode dispatch | `lda $C9FA,y` at `$CA0F` and `lda $C9FB,y` at `$CA14`; `lda $C9FE,y` at `$CA22` and `lda $CA00,y` at `$CA27`; `lda $C9F8` at `$CA86` and `lda $C9F9` at `$CA8C` | | `$CA9A-$CAAF` | 22 | `EnemyPlacementTable` | four interleaved 16-entry byte tables (a stride of two: `$CA9A`, `$CA9C`, `$CA9E`, `$CAA0`) giving the per-enemy placement, speed and pattern values | `lda $CA9A,y` at `$CAEE` and `lda $CA9C,y` at `$CAF3`; `lda $CA9E,y` at `$CB0D`; `lda $CAA0,y` at `$CB2B` | | `$CAB0-$CADF` | 48 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$CB63-$CB66` | 4 | `EnemyModeTable` | 4 bytes read as a per-mode parameter table; the same bytes also decode as instructions, so the block is most probably a mis-split of unreached code | `lda $CB63,y` at `$CB77` | | `$CC02-$CC8E` | 141 | `VramWriteList` | a VRAM write list in the form (address high, address low, count, data...) | `lda $CC02,x` at `$CDDE`, `$CDE5` and `$CDF0`; `ldy $CC02,x` at `$CDEC` | | `$CC8F-$CC9E` | 16 | `ScreenAttributeRow` | 16 attribute bytes written straight to `PPUDATA` | `lda $CC8F,x` at `$CE0B` | | `$CD50-$CD57` | 8 | `TitleScreenTextData` | 8 bytes of the title-screen text stream | `lda $CD50,y` at `$CE21` | | `$CD58-$CD77` | 32 | `ObjectPaletteData` | 32 bytes of palette data, copied into `$009C` | `lda $CD58,y` at `$CE48` and `$E587` | | `$D07B-$D08C` | 18 | `GameStateParameterTable` | 9 x 16-bit little-endian values (`$841A`, `$9BC2`, `$A34D`, `$A523`, `$A611`, `$A6D4`, `$A792`, `$9DA6`, `$9FC2`) | `lda $D07B` at `$D114` and `$D2BB`; `lda $D07C` at `$D11B` and `$D2C2`; `lda $D07D` at `$D0CA` and `$D279`; `lda $D07E` at `$D0D3` and `$D281`; `lda $D07F` at `$D9DA`; `lda $D080` at `$D9DF`; `lda $D081` at `$D3D2`; `lda $D082` at `$D3D7`; `lda $D083` at `$D9E4`; `lda $D084` at `$D9E9`; `lda $D085` at `$D3DC`; `lda $D086` at `$D3E1`; `lda $D087` at `$D448`; `lda $D088` at `$D44D`; `lda $D089` at `$D555`; `lda $D08A` at `$D55A`; `adc $D08B` at `$D1EB` and `$D312`; `adc $D08C` at `$D1F2` and `$D319` | | `$D24D-$D257` | 11 | `EnemySpawnIndexTable` | 11 bytes (`00 0B 0C 03 04 0D 06 07 08 09 0A`) - the index of the enemy type that appears in each wave | `lda $D24D,y` at `$D266` | | `$D8FA-$D912` | 25 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$D913-$D918` | 6 | `LevelScriptPointerTable` | 3 x 16-bit code pointers | `lda $D913` at `$D92C` and `$E5CE`; `lda $D914` at `$D931` and `$E5D3`; `lda $D915` at `$D93B`; `lda $D916` at `$D940`; `lda $D917` at `$D948`; `lda $D918` at `$D94D` | | `$DBBA-$DBBF` | 6 | `SpritePairData` | 6 bytes = three pairs of equal bytes (`BA BA`, `BC BC`, `32 32`); the individual values are not further identified | `lda $DBBA,y` at `$DBA3` | | `$DC5C-$DC67` | 12 | `ObjectParameterTable` | 12 bytes (`01 09 01 00 02 09 05 09 06 00 06 09`) read as a per-object parameter table | `lda $DC5C,y` at `$DC2A` and `$DC31` | | `$DF9D-$DFA4` | 8 | `ObjectSpeedCurveTable` | 8 bytes (`00 0C 2C 4C 2C 0C 00 00`) - a symmetric run of speed values | `lda $DF9D,x` at `$E356` | | `$E4D2-$E4D3` | 2 | *(unnamed)* | two bytes that decode as instructions (`ASL $26` / `ROL $26`) | `lda $E4D2,x` at `$E4DC` - a byte of the unreached code is picked up here | | `$E73A` | 1 | *(unnamed)* | one byte, `$D8` (`CLD`) | `lda $E73A,y` at `$E760` - a byte of the unreached code is picked up here | | `$E792-$E799` | 8 | `ScreenDataConstantTable` | 8 bytes read as two 16-bit values; the same bytes also decode as instructions, so the block is most probably a mis-split of unreached code | `lda $E792,x` at `$E7BE` and `$E7FD`; `lda $E796,x` at `$E7C9` and `$E808` | | `$E939-$E964` | 44 | `EnemyStatusTable` | 44 bytes of per-enemy status data, read as two 18-byte halves (`$E939` and `$E951`) | `lda $E939,x` at `$E19B` and `$E3E1`; `lda $E951,x` at `$E1CA` and `$E410` | | `$EA1C-$EA27` | 12 | `ObjectSpawnParameterTable` | 12 bytes (`FF 00 00 02 00 04 FF 00 00 01 00 01`) - the per-object spawn parameters | `lda $EA1C,x` at `$EA74` | | `$EABC-$EABF` | 4 | *(unnamed)* | 4 bytes of enemy status data (`07 06 05 00`); nothing in this dump reads them | none (unreferenced data) | | `$EAC0-$EB94` | 213 | *(unnamed)* | decodes as instructions (the routine that walks the enemy status table) | none (unreached code) | | `$EC6C-$EC99` | 46 | `SoundEffectHandlerTable` | 23 x 16-bit code pointers - the sound-effect dispatch | `lda $EC6C,y` at `$ECC2` and `lda $EC6D,y` at `$ECC7` | | `$ECF5-$F7DD` | 2793 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$F7DE-$F7E5` | 8 | `LevelEntryPointer` | 4 x 16-bit code pointers | `lda $F7DE,y` at `$F7F2` and `lda $F7DF,y` at `$F7F7` | | `$F809-$F810` | 8 | `EventHandlerPointerTable` | 4 x 16-bit code pointers | `lda $F809,y` at `$F83C` and `lda $F80A,y` at `$F841`; `lda $F80D,y` at `$F910` and `lda $F80E,y` at `$F93C` | | `$F814-$F836` | 35 | `MotionPatternTable` | 35 bytes: a run of small values that step up and then back down (`08 07 08 07 06 05 06 05 04 03 04 03 02 01 02 00 03 07 04 01 02 06 05`) - the shape of a per-frame envelope; whether it drives a movement or a volume is not established | `lda $F81F,y` at `$F90B`; `lda $F82F,y` at `$F88D`; `lda $F833,y` at `$F884` | | `$F849-$F863` | 27 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$F981-$F9F2` | 114 | *(unnamed)* | decodes as instructions | none (unreached code) | | `$FA11-$FA50` | 64 | `SoundEffectProgramData` | a stream of (duration, APUPULSE1CTRL value) pairs, `$FF` terminating the effect | `lda $FA11,x` at `$FA57` and `$FA65` - the second byte of each pair is written to `APUPULSE1CTRL` | | `$FA6F-$FA88` | 26 | `SoundSequencePointerTable` | 13 x 16-bit pointers to the sound data (`$0000`, `$06AF`, `$064E`, `$05F4`, `$059E`, `$054E`, `$0501`, `$04BA`, `$0476`, `$0436`, `$03F9`, `$03C0`, `$038A`) - the values fall off roughly with 1/n^2 | `lda $FA6F,y` at `$FABD` and `lda $FA70,y` at `$FAC2` | | `$FC4B-$FC4E` | 4 | `Pulse1VolumeTable` | 4 APUPULSE1CTRL values (`$82 $84 $88 $8F`) - the volume settings the game selects between | `lda $FC4B,x` at `$FC21` and `$FC30` | | `$FC4F-$FC52` | 4 | `Pulse2VolumeTable` | 4 APUPULSE2CTRL values (`$20 $30 $40 $7F`) | `lda $FC4F,x` at `$FC3F` | | `$FC53-$FFFF` | 939 | `SoundSequenceDataPointerTable` | 16-bit pointers to the sound data, then the sound engine's code, then the three 6502 vectors at `$FFFA`-`$FFFF` | `lda $FC53,y` at `$FAEE` and `lda $FC54,y` at `$FAF3` | ### Notes on the interpretation * Magmax keeps its level, its screen text and a dozen small tables in one 15 KB block at the head of the PRG, so the block is split here into the parts that have their own consumers. The terrain stream itself uses control bytes with 16-bit offsets; the exact grammar of the stream is not established, so no finer split is claimed. * Every table that is fetched as a 16-bit code address is written in the `lda $XXXX,y` / `sta Zp5E` / `lda $XXXX+1,y` / `sta Zp5F` / `jmp (Zp5E)` shape, which is what fixes the pointer-table reading of `$A9CA`, `$ACC9`, `$AEF4`, `$AF2C`, `$AF4C`, `$BDC2`, `$EC6C`, `$F7DE`, `$F809` and `$FC53`. * `$E4D2`, `$E73A`, `$A8DA`-like fragments and the 2-byte blocks are recorded as code the tracer could not reach: their bytes decode cleanly as 6502 instructions and the instruction that appears to read them is only picking up a stray operand of that code. ## Address-derived entry points 70 of the 162 `jsr`/`jmp` entry points now carry a semantic name. **92 remain address-derived.** Magmax's program is built around five object pools - at `$0366`, `$0446`, `$0246`, `$02E6` and `$0219` - and the same small helpers serve all of them, so the dump often does not say which pool a routine works on. The `SubC` form is kept for those rather than a name that would only guess. ### The object pools, the terrain and the sound engine (92) `Sub_BCC4`, `Sub_BCDA`, `Sub_BCF0`, `Sub_BD01`, `Sub_BD17`, `Sub_BD2D`, `Sub_BEAF`, `Sub_BF29`, `Sub_BFE6`, `Sub_C03A`, `Sub_C18E`, `Sub_C1E0`, `Sub_C35D`, `Sub_C465`, `Sub_C486`, `Sub_C49A`, `Sub_C4CF`, `Sub_C521`, `Sub_C592`, `Sub_C5D8`, `Sub_C6DC`, `Sub_C762`, `Sub_C76B`, `Sub_C86F`, `Sub_C9BF`, `Sub_CA20`, `Sub_CA6F`, `Sub_CAE0`, `Sub_CB85`, `Sub_CBE1`, `Sub_CBEC`, `Sub_CDB0`, `Sub_CE87`, `Sub_CF48`, `Sub_CFC1`, `Sub_D07A`, `Sub_D0C6`, `Sub_D0CA`, `Sub_D1CD`, `Sub_D258`, `Sub_D270`, `Sub_D383`, `Sub_D3BF`, `Sub_D478`, `Sub_D4C9`, `Sub_D507`, `Sub_D585`, `Sub_D665`, `Sub_D668`, `Sub_D66A`, `Sub_D708`, `Sub_D7D1`, `Sub_D85E`, `Sub_D919`, `Sub_DA2D`, `Sub_DABB`, `Sub_DD2D`, `Sub_DE4F`, `Sub_DE80`, `Sub_DF2E`, `Sub_DF30`, `Sub_DFC9`, `Sub_E042`, `Sub_E0BB`, `Sub_E21B`, `Sub_E22F`, `Sub_E2A6`, `Sub_E306`, `Sub_E362`, `Sub_E463`, `Sub_E495`, `Sub_E532`, `Sub_E60B`, `Sub_E64B`, `Sub_E6CF`, `Sub_E701`, `Sub_E72E`, `Sub_E81C`, `Sub_E85C`, `Sub_E965`, `Sub_EB95`, `Sub_EC9A`, `Sub_F7E6`, `Sub_F8A4`, `Sub_F8CD`, `Sub_F922`, `Sub_F9F3`, `Sub_FA57`, `Sub_FAC7`, `Sub_FB1A`, `Sub_FBF9`, `Sub_C861` The game's objects live in five pools of eight-byte slots, and the terrain's collision list lives in `$0219`; the routines above read one slot, check it against the terrain or the other pools, and write it back. Which pool a given helper serves is the thing the dump does not record: the same code shape appears at `$C08B`/`$C0AF` (the first pool), `$C0DC`/`$C102` (the second) and at `$DFD8`/`$E061` (the third and fourth). The sound engine's voices are started by `PlaySoundEffectOne` … `PlaySoundEffectFour` and `PlaySoundEffectFromTable`; the residue here also includes the frames of the little sequencer that reads the command stream and the two counters it runs on. ## Build ``` Byte-identical ROM verified. ```