# Volleyball (U) ## Status **Pass 3 complete.** The project builds byte-identical to the original ROM and `prg.s` carries a full disassembly: every byte of the 32 KB PRG image is accounted for as either an instruction or an explicit `.byte` block, with the canonical audit comment on every line. ``` Byte-identical ROM verified. ``` | | | |---|---| | Source | `prg.s` (31,567 lines) | | Instructions | 11,395 (23,926 code bytes, 73.0 % of the PRG image) | | Data blocks | 37 (8,842 bytes) | | Labels | 1,989, all semantic (no `Label_XXXX` left) | | Raw `$XXXX` call/branch operands | 0 | | Raw `$2000-$4017` hardware literals | 0 | | Multi-alias residue (`apply_symbols.py --report`) | 0 | | Neutral `ZpXX` zero-page references | 135, over 29 bytes, each with a recorded reason in `locals.inc` | ## ROM | Property | Value | |----------|-------| | Format | iNES | | Mapper | 0 | | Board | NROM-256 | | Mirroring | Vertical | | Four-screen VRAM | no | | Battery | no | | PRG | 32 KB (2 x 16 KB), mapped at CPU $8000-$FFFF | | CHR | 8 KB (1 x 8 KB) | | Trainer | none | | Footer | none | | NMI vector | $8053 | | RESET vector | $8000 | | IRQ vector | $FFFF | | ROM file | `Volleyball (U).nes` | | SHA-256 | `5c3baa82f28963fc258c238c82b628dedafdc1342d201721ced196437c983c4d` | ### Header notes - Byte 6 is `$01`: mapper bits 0-3 are in the *upper* nybble, so the mapper is 0 and bit 0 selects vertical mirroring. - The IRQ vector is `$FFFF`, which is the last byte of the PRG image and therefore not a handler at all; the APU frame counter is never IRQ-enabled. ## Power-on flow ### RESET ($8000) ``` $8000 Reset A = $FF, $DF (NmiInProgress) = $FF, X = $FF, TXS (stack at $01FF) PPUMASK = $00 ; rendering off $800B WaitForVblankStart LDA PPUSTATUS / BPL *-4 ; wait for bit 7 to set $8010 WaitForVblankEnd LDA PPUSTATUS / BPL *-4 ; wait for bit 7 to clear $8015 JMP InitGameAndEnterMainLoop ($803E) ``` `$8018-$803D` is an **unreachable** block: it reads the PPU palette at `$0178`, compares it with `$1B` and spins forever if it matches - a TV-standard probe that no branch in the ROM ever reaches. The only instruction boundary inside it is `$801A`; `$8018-$8019` (`$3C $80`) does not decode as an instruction. It is kept as `.byte` data with the note in the source. ### Init ($803E) ``` $803E InitGameAndEnterMainLoop JSR ClearGameRam ($845D) ; wipe $0001-$00FF and $0300-$04FF JSR InitPpuForPowerOn ($8335) ; APU/DMC off, PPUCTRL = $10 $EB = $01, $DE = $00, $DF = $00 JMP MainIdleLoop ($81B5) ``` ### Main loop ($81B5) The main loop does almost nothing: ``` $81B5 MainIdleLoop $DF = 0 ; re-arm the NMI handler LDA $00 .. LDA $07 ; refresh eight zero-page bytes JMP $81B5 ``` **All of the game runs from the NMI handler.** The loop only exists to give the CPU something to do between frames and to re-arm `$DF`. ### NMI ($8053) ``` $8053 NmiHandler PHA LDA $DF ; set while an NMI is being serviced BNE $81CC ; a nested NMI is dropped: PLA / RTI INC $DF LDA $40 ; a VRAM update is pending? BNE $81FF ; -> FlushVramUpdateScript, which ends in DEC $DF / PLA / RTI ``` The frame itself: 1. `INC $2D` - the frame counter every other routine reads. 2. Rendering is turned off (`$19` loses bit 7, `PPUMASK = $00`). 3. `$84DF` uploads the OAM shadow at `$03FF`, `$8424` writes the queued attribute bytes. 4. State-specific work: the state machine is dispatched further down, but `$807A-$8115` runs the per-state extras first (the `LDA $2B / CMP #$01..$08` chain, the whistle, the audience animation). 5. `$811E` animates the crowd, `$8132` updates the indicator board. 6. `$8315` writes the scroll and the nametable bits, `LDA PPUSTATUS` resets the PPU latch and `EOR #$80` on the PPUCTRL shadow re-enables the NMI for the next frame; `PPUMASK` is written again to turn rendering on. 7. `$82F5` copies the freshly built OAM buffer to the PPU with `OAMDMA`, `$82AC` reads the controller of the player whose turn it is. 8. The four-byte command block at `$05C0-$05C3` is filled in. 9. `$85B1` **DispatchGameState** runs the active state. 10. `$C5EF` draws the crowd, the sprite buffers at `$0200`/`$0300` are cleared, `PLA / RTI`. ### The state machine The game state lives in `$2B` and the sub-state in `$2C`. ``` $85B1 DispatchGameState $25/$26 = $85E1 ; the dispatch table $28/$29 = $2B * 2 ; the index LDA ($25),Y / STA $04 ; the 16-bit entry LDA ($25),Y / STA $05 LDX $2C ; the sub-state travels in X JMP ($0004) ; and off it goes ``` `GameStateDispatchTable` at `$85E1` holds the eleven state handlers: | State | Handler | What it does | |-------|---------|--------------| | `$00` | `StateTitleScreenInit` | clears the state, queues the intro VRAM script, loads the sprite tables, starts the match structures | | `$01` | `StateTitleScreen` | the "Volleys Ball" title screen; waits for START | | `$02` | `StateModeSelect` | the PLAY / SELECT screen: 1PLAYER/2PLAYER, WOMEN/MEN, and the ten nations | | `$03` | `StateModeSelectToMatch` | the fade to black between the menus and the court | | `$04` | `StateCourtSetup` | the teams walk on to the court | | `$05` | `StateRally` | the rally: the players move, the ball is simulated, points are scored | | `$06` | `StateNewPoint` | the break between two points | | `$07` | `StateTeamSelection` | picking the nations | | `$08` | `StateServeSetup` | placing the ball and starting the serve | | `$09` | `StateAttractDemo` | the attract demo, where the computer plays a whole match | | `$0A` | `StateMatchResults` | the result of the match | Every handler is a `switch` on the sub-state in X, written as a chain of `DEX / BEQ` (or `INX / DEX / BEQ`, which is the same test). A case ends in one of: | Routine | Effect | |---------|--------| | `AdvanceSubState` (`$8600`) | `INC $2C` - the next phase of the same state | | `AdvanceGameState` (`$8603`) | `$2C = 0`, `INC $2B` - the next state | | `SetGameState` (`$860A`) | `STA $2B`, `$2C = 0` - a state chosen by the code | | `DecrementStateTimer` (`$85F7`) | counts the 16-bit timer in `$02/$03` down and advances the sub-state when it expires | The state flow was confirmed against the running ROM with a Mesen CE debugger: reading `$2B`/`$2C` every 50 ms while the attract demo plays gives `$00/$06 -> $01/$01 -> $02/$00 -> $03/$01 -> $04/$01 -> $05/...`, with `$05` (the rally) and `$08` (the serve) alternating. ## Subsystems | Area | Where | What it does | |------|-------|--------------| | VRAM updates | `$81FF`, `$8210`, `$8245` | a byte-code interpreter that walks a script at `($41),Y`; `$AD` means "copy N bytes", `$AE` means "the script is done" | | Attribute writes | `$8424`, `$8440` | the same idea for the attribute table, with the script in `$0180` | | Controllers | `$82AC`, `$82C2` | strobes `$4016`, shifts eight bits per player, and derives the newly-pressed mask | | OAM | `$83D8`, `$82F5`, `$84DF` | the sprite list is built in `$0200`/`$0300` and copied with `OAMDMA` | | Drawing | `$C2A6`..`$CFAB` | the court, the players, the flags and the score board | | The ball | `$9EB0`..`$C1E6` | the sub-pixel position, the velocity, the spin and the bounce | | The players | `$97F9`..`$B96E` | ten near-identical copies of the same movement routine, one per nation, indexed by `$5C`/`$5D` | | Sound | `$F800`..`$FC7E` | the APU driver, the effect dispatcher and the music byte-code player; the note tables and programs are in `$DC54`..`$FFF6` | ## 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 `$8000` (`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 | |---------|---------| | `$800B` | `WaitForVblankStart` (falls through) | | `$8010` | `WaitForVblankEnd` (falls through) | | `$803E` | `InitGameAndEnterMainLoop` | | `$845D` | `ClearGameRam` | | `$8335` | `InitPpuForPowerOn` | | `$81B5` | `MainIdleLoop` | | `$81B5` | `MainIdleLoop` | **NMI** is at `$8053` (`NmiHandler`). It is the per-frame handler; what it calls: | Address | Routine | |---------|---------| | `$81CC` | `NmiIgnoreAndReturn` | **IRQ** is at `$FFFF` (`$FFFF`) - a different address from RESET. ## Pass 3 data-table inventory | Label | Address | Size | Contents | |-------|---------|------|----------| | `UnreachedCode_8018` | `$8018` | 38 | the unreachable TV-standard probe | | `UnreachedCode_8118` | `$8118` | 6 | two unreachable `JMP $811E` tails | | `UnreachedCode_81CE` | `$81CE` | 49 | an unreachable copy of the VRAM flush routine | | `UnreachedCode_8253` | `$8253` | 22 | an unreachable earlier controller read | | `UnreachedCode_8409` | `$8409` | 27 | two unreachable VRAM helpers | | `PlayerSpriteTables` | `$8511` | 160 | five 32-byte sprite tables, one per player slot | | `GameStateDispatchTable` | `$85E1` | 22 | eleven 16-bit state handler addresses | | `UnreachedCode_8D52` | `$8D52` | 3 | an unreachable `JMP $8600` tail | | `UnreachedCode_8DFD` | `$8DFD` | 12 | four unreachable shared jump tails | | `UnreachedCode_8FFE` | `$8FFE` | 3 | the unreachable tail of the command stream decoder | | `ScoreDigitAddressTable` | `$9252` | 16 | eight 16-bit PPU addresses of the score digits | | `TeamNumberDelayTable` | `$941A` | 12 | twelve per-team delay values | | `BallHitTypeTable` | `$C10F` | 6 | hit-type data for the ball code | | `UnreachedCode_B5A0` | `$B5A0` | 7 | an unreachable earlier copy of the ball-reset code | | `BallHitOffsetTable` | `$B6B6` | 14 | the ball rebound offsets | | `UnreachedCode_BFF7` | `$BFF7` | 7 | an unreachable earlier copy of the ball-reset code | | `PlayerSpeedTable` | `$CAD5` | 12 | six 16-bit player movement speeds | | `PlayerSpeedTableCopy` | `$CB4F` | 12 | the same table for the other side of the court | | `NationCodeTable` | `$CD78` | 64 | the sprite code of each of the ten nations | | `DigitGlyphTable` | `$D57B` | 80 | the ten digit glyphs, 0..9 | | `CrowdTileTable` | `$D642` | 64 | 32 (tile, attribute) pairs for the spectators | | `SpriteTileTable` | `$DAD9` | 96 | 48 four-byte OAM entries for the players, the ball and the umpire | | `BackgroundTilemapData` | `$DB7E` | 36 | nametable data for the court | | `NationNameTable` | `$DC26` | 40 | USA, URS, CHN, CUB, JPN, BRA, KOR, TUN, YOU, COM | | `MusicAndSoundData` | `$DC54` | 6,778 | the note tables, the tone tables and the music byte-code streams | | `MusicPulseTableStart` | `$F6CE` | 9 | the first pulse-channel note entries | | `MusicPulseTableEnd` | `$F759` | 7 | the last pulse-channel note entries | | `MusicPulseTable` | `$F6D7` | 130 | the pulse-channel note data | | `MusicPulseTableB` | `$F760` | 160 | the second half of the pulse-channel note data | | `NoisePeriodTable` | `$F87F` | 32 | the noise-channel period values | | `UnreachedCode_F84D` | `$F84D` | 3 | an unreachable `JMP $F83F` tail | | `UnreachedCode_F864` | `$F864` | 3 | an unreachable `JMP $F846` tail | | `CrowdNoisePatternTable` | `$FAAC` | 24 | the crowd-noise pattern | | `SoundEngineData` | `$FC7F` | 461 | the music player's note tables and byte-code program | | `SoundEngineDataC` | `$FE4C` | 7 | constants used by the music player | | `SoundEngineDataB` | `$FE59` | 414 | the second part of the music byte-code program | | `CpuVectors` | `$FFFA` | 6 | NMI $8053, RESET $8000, IRQ $FFFF | ## How the split was established The static tracer finds 73 % of the PRG image on its own, but it cannot see code that is reached only through computed dispatch and it cannot tell inline data from an instruction that happens to decode from the same bytes. The Mesen Code Data Logger is the oracle for both. 1. **Pass 1** - `_tools/nesdis.py --undocumented --auto-seed --table 85E1:11`. The dispatch table has to be declared: the tracer cannot infer that `$85E1` is a table of eleven entry points, and without it only 66 % of the image is traced. 2. **Reclassification** - `_tools/cdl_reclass.py` rewrote the split to match a Code Data Logger capture taken while a random-input bot played the game for about twelve minutes (Mesen CE 2.2.1, the ROM at `$8000-$FFFF`). The capture saw 17,105 code bytes, 5,876 data bytes and left 9,787 bytes unobserved; the 9,787 are the unexecuted paths and the graphics, music and table data. 3. **Verification** - `_tools/cdl_check.py` (added for this project) compares the finished `prg.s` against the CDL: every byte the CPU fetched as an opcode has to be covered by an instruction, and every byte the CPU read as data must not start one. Both counts are zero. ``` $ python3 _tools/cdl_check.py "_wip/NROM/Volleyball (U)/prg.s" vb.cdl 0x8000 CDL: code=17105 data=5876 unknown=9787 prg.s: 11395 instruction starts, 32768 bytes covered CDL-Code bytes not covered by prg.s: 0 CDL-Data bytes that start an instruction in prg.s: 0 ``` ## Open work - The ten copies of the per-nation player movement routine (`$958C`..`$B96E`) and the per-nation ball code (`$97F9`..`$A8F2`) are named after their position in the ROM (`TeamPlayerMove0`..`TeamPlayerMove5`, `BallUpdateTeamA`..`BallUpdateTeamN`). Which nation each copy belongs to has **not** been pinned down; the copies differ only in a few constants, so the mapping would need a per-copy behavioural trace. - `BallHitOffsetTable` and the speed tables hold values whose exact meaning is still inferred from the code around them. - The state names come from the screens the states draw; the naming of the sub-states inside each handler is the mechanically derived `_OnZero` / `_Loop` form. ## Files | File | Purpose | |------|---------| | `prg.s` | the disassembly | | `routinemap_hand.tsv` | the hand-written Pass 2 routine names (731 of them) | | `routinemap.tsv` | the same map after `derive_branch_names.py` added the 1,221 derived branch names | | `volleyball_mesen.cdl` | the Mesen Code Data Logger capture the split was checked against | | `locals.inc` | the semantic zero-page and RAM aliases | | `symbols.inc` | NES hardware register names | | `header.s` / `chr.s` | the other ROM segments, as ca65 includes | | `prg.bin` / `header.bin` / `chr.bin` | the binary splits | | `rom.nes` | a space-free copy of the original ROM, for `cmp` | | `linker.cfg` | the HEADER / PRG / CHR memory areas | | `Makefile` | build and byte-identity check |