Mermaids of Atlantis (U)
Imagineering, 1991 - "The Riddle of the Magic Bubble"
Status
Pass 3 complete. The code/data split comes from a Mesen Code Data
Logger capture, every routine and branch target carries a name, the
zero-page aliases that the dump proves are applied, and make rebuilds the
whole ROM byte for byte:
Byte-identical ROM verified.
prg.s is 8 075 bytes of disassembled code out of the 64 KB PRG image
(12.3 %), in 494 routines. The rest of the image stays as .byte blocks
with their ROM provenance: the bytes the emulator never reached are not
guessed at.
ROM
| Property | Value |
|---|---|
| Format | iNES |
| Mapper | 8 (MMC3) |
| PRG | 64 KB (8 x 8 KB banks), at file offset $0010 |
| CHR | 64 KB (8 x 8 KB banks) |
| Mirroring | Vertical |
| Trainer | none |
| Reset vector | $FF00 |
| NMI vector | $808F (see "The two vector tables") |
| IRQ vector | $8036 |
| ROM file | Mermaids of Atlantis (U).nes |
| SHA-256 | 53f5d22650063aba89b91762f133d1b6b2680c505b193d88a472580d09701122 |
The banking
The cartridge is an MMC3 with 64 KB of PRG - eight 8 KB banks. The file is laid out in banks, and the game uses them like this:
| Bank | File | CPU window | Role |
|---|---|---|---|
| R0 | $0000-$1FFF | $8000-$9FFF | the game code, first half |
| R1 | $2000-$3FFF | $A000-$BFFF | the game code, first half |
| R2 | $4000-$5FFF | $C000-$DFFF | the game code, first half |
| R3 | $6000-$7FFF | $E000-$FFFF | the fixed bank: the reset stub, the vectors, the text table |
| R4 | $8000-$9FFF | $8000-$9FFF | the game code, second half |
| R5 | $A000-$BFFF | $A000-$BFFF | the game code, second half |
| R6 | $C000-$DFFF | $C000-$DFFF | unused |
| R7 | $E000-$FFFF | $E000-$FFFF | a second copy of the fixed bank's tail |
The mapper register is written through the fixed bank: SwitchPrgBank
($FF70) does ora #$30; sta $4100, so the mode bits are forced on and the
bank number is the low three bits of A. BankedCall ($8004) is the game's
own far-call: it stores the handler index in HandlerIndex ($2D), switches
the PRG bank to the one in PrgBankNumber ($2C) and lets the caller fall
into the new bank's code at the same CPU address.
Because two banks can be visible at the same CPU address, the disassembly
gives each bank its own label namespace: bank 0, 1, 2 and the fixed bank use
plain names, banks 4 to 7 use the Bank4_ .. Bank7_ prefixes. Every bank
has its own segment (PRG0 .. PRG7) and its own .org, so the assembler
knows the CPU address of every label while the linker still writes the
bytes to the bank's file offset.
The two vector tables
The image carries the reset stub and the vectors twice:
| File | NMI | RESET | IRQ |
|---|---|---|---|
| $7FFA (R3, the fixed bank) | $808F | $FF00 | $8036 |
| $FFFA (R7, the last bank) | $8189 | $FF00 | $8036 |
The fixed bank is the one the CPU sees, so the live NMI vector is $808F
and the NMI handler is NmiHandler at $808F in bank 0. The copy at the end
of the image is what a mapper that fixes the last 8 KB of the PRG would
use; it names an NMI handler at $8189, which is a BRK in the first half of
the image. The two stubs are byte identical from $FF00 to $FF7F, and the
fixed bank carries one routine the last bank does not: SwitchPrgBankKeepA
at $FF80.
Power-on flow
$FF00 Reset ldx #$FF / txs / sei / cld
clears PPUCTRL and PPUMASK
SwitchPrgBank ($30) - the first PRG bank
WaitForBankSwitch - the bank switch counter
$FF2F SelectChrBanks picks the CHR banks of the first stage
$FF3E StartGame SwitchPrgBank ($30), StartBankSwitchDelay,
jmp $8036
$8036 InitializeGame sei / cld / WaitForVBlank
clears the PPU and the work RAM
InitGameVariables - the sound, scroll and sprite registers
$8083 IdleWaitLoop inc IdleCounter / jmp $8083 - the game waits for the NMI
From then on the frame handler does the work:
$808F NmiHandler StartVramWrite when the tile set changed
UpdateNametable / UploadOam when the screen changed
writes PPUCTRL, PPUSCROLL and PPUMASK
ReadControllers - the pad bits
UpdateGameState - the mode machine
UpdateActorSprites / UpdateSprites
BankedCall ($02) - the sound engine
writes PPUCTRL and counts the frame
$8036 is also the IRQ vector, so an interrupt re-runs the power-on path.
Data tables
| Table | Where | What it holds |
|---|---|---|
| the digit and tile table | $F1D6, in the fixed bank | "0123456789" and the tile indices of the font |
| the bank switch table | $8026, in banks 0 and 4 | the bank numbers, eight per half |
| the handler jump tables | $8095 (bank 4), $9234, $C2AC | the addresses the dispatchers jump to |
| the sprite table | $0338 | the 16 sprite slots: tile, attributes, position |
| the actor table | $0358, $03A8 | the actors and the bubble slots |
| the player table | $0400 | the player's position and direction |
| the level script | $05E2-$05E9 | the stage, the wave and the timers |
| the sound tables | $0700-$07FF | the sound engine of the second half |
Files
| File | Purpose |
|---|---|
prg.s |
the disassembly, one segment per bank |
header.s |
the iNES header |
chr.s |
the CHR image |
prg.bin |
the 64 KB PRG image |
chr.bin |
the 64 KB CHR image |
rom.nes |
a copy of the original ROM |
linker.cfg |
one memory area per bank, at the file offsets |
symbols.inc |
the NES hardware register names |
locals.inc |
the zero-page aliases |
routinemap.tsv |
the hand-written Pass 2 names |
routinedesc.tsv |
the hand-written routine descriptions |
labelmap.tsv |
the full Pass 2 map (generated) |
regen.sh |
rebuilds prg.s from the ROM and the CDL capture |
Makefile |
the build and the byte-identity check |
Rebuilding
make # build and verify against rom.nes
./regen.sh # re-derive prg.s from the ROM and a Mesen CDL capture
regen.sh needs a Mesen CDL capture of the game; the file is not kept in
the repository. Take it with Mesen (load the ROM, play for a while, then
mesen_cdl_file(action='save', memoryType='NesPrgRom', filepath=...)) and
point CDL= at it.