Files
disasm/MAPPER8/Mermaids of Atlantis (U)
..
2026-10-21 11:36:32 -04:00
2026-10-21 11:36:32 -04:00
2026-10-21 11:36:32 -04:00
2026-10-21 11:36:32 -04:00
2026-10-21 11:36:32 -04:00
2026-10-21 11:36:32 -04:00
2026-10-21 11:36:32 -04:00

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.