ExHiROM 48 Mbit Cartridge Test

Cartridge & Mapping Tests

A 48 Mbit (6 MiB) ExHiROM cartridge (map mode $25), physically two mask ROMs — 32 Mbit + 16 Mbit — the same configuration as Tales of Phantasia. Ordinary LoROM and HiROM both stop at exactly 4 MiB; padding an image past that just makes a file whose top megabytes no address reaches. The extended map splits the image in two and selects between the halves on the bank's high bit, inverted.

loading core…

Self-running — no controls. The screen turns green when every canary, mirror and span read the byte the model predicted, red if any did not. (It spends a few seconds folding spans before the verdict appears.)

Click the screen, then play. (Tab away and it pauses.)

The mapping

map mode      $25  (ExHiROM, slow ROM)
header        file $40FFB0            <- in the SECOND region, not the first
image         6,291,456 bytes  =  48 Mbit
physical      32 Mbit @ $000000  +  16 Mbit @ $400000
region A      $C0-$FF:0000-FFFF   <- file $000000-$3FFFFF
region B      $40-$5F:0000-FFFF   <- file $400000-$5FFFFF
holes         none

The consequence that catches everyone: the 65816 resets with PBR=$00 and fetches RESET from $00:FFFC — and bank $00's upper half belongs to the second region. So the header, the vectors and the near-code window live at file $408000$40FFFF: the boot code is in bank $40, not $C0. An unmodified crt0 still runs, because all it needs is for $00:8000$FFFF to be ROM.

That also makes the file non-monotonic in CPU space: file $3FFFFF is $FF:FFFF, and the very next byte, file $400000, is $40:0000. Anything crossing that boundary has to be consumed as an ordered segment list — a single incrementing pointer simply cannot make the jump. That is what the EDGE_4M fixture on this cartridge exercises.

ROM map

Every half-bank cell the decoder maps, generated from tools/snes_cartmap.py: the two physical mask ROMs colour-keyed, region A/B and their mirrors, and the seam where the file offset keeps climbing through $400000 while the canonical CPU bank drops from $FF to $40.

ExHiROM 48 Mbit Cartridge Test — CPU bank decode mapmapping exhirom (map mode $25, slow ROM) — ROM 1 = 32Mbit @ file $000000, ROM 2 = 16Mbit @file $4000000123456789ABCDEF$00$10$20$30$40$50$60$70$80$90$A0$B0$C0$D0$E0$F0ROM 1 canonical — 32Mbit @ file $000000–$3FFFFFROM 1 mirror — same bytes, alternate CPU addressROM 2 canonical — 16Mbit @ file $400000–$5FFFFFROM 2 mirror — same bytes, alternate CPU addressWRAM ($7E/$7F) — hard-wired, never cartridgesystem area / I-O — bank low half below $C0, not cartridge512 half-bank cells (256 banks × 2 halves): 380 decode to cartridge ROM (192 canonical +188 mirror), 4 WRAM, 128 system-area.canonical windows$C0-$FF:0000-FFFF <- file $000000-$3FFFFF (ROM 1)$40-$5F:0000-FFFF <- file $400000-$5FFFFF (ROM 2)file→CPU seam(s)seam at file $400000: CPU bank drops $FF→$40 while the file offset keeps increasing — a segmentcursor must reload the bank byte here, never increment across it.device split2 physical mask ROMs: ROM 1 = 32Mbit @ file $000000–$3FFFFF, ROM 2 = 16Mbit @ file$400000–$5FFFFF

Physical size versus logical size

The smaller device is mirrored across its slot until it is as large as the bigger one, so the address decoder sees a logical 8 MiB image. Both the ROM-size header byte ($FFD7 = $0D, i.e. 213 KiB) and the checksum describe that logical size, not the 6 MiB file length — the checksum is sum(first 4 MiB) + 2 × sum(last 2 MiB). Summing each byte once gives the wrong answer, and every heuristic emulator's "checksum ok" indicator will disagree.

What it checks

CheckWhat it proves
8 canary bytesThe first and last byte of every decoded CPU window, every physical mask ROM, and each 1 MiB divider — each read through a 24-bit far pointer (lda [dp]) and compared against the byte the host model says lives there
4 mirror probesEach accepted mirror returns the same byte as its canonical window. The padding pattern is a non-linear hash of the full 24-bit file offset, so a decoder that folded a mirror to a different offset is caught immediately
4 folded spansByte runs crossing one 64 KiB CPU bank, several banks, and — the fixture that matters here — one crossing the physical 4 MiB device boundary. Each is walked as an ordered list of bank-bounded segments, re-establishing the bank byte at every segment start — never by incrementing a pointer across a boundary
Rotate-add foldThe oracle is order-sensitive and non-linear over GF(2). An earlier XOR fold over an XOR-linear pattern collapsed to $0000 — and to $0000 for the wrong bank too, proving nothing. A bank off-by-one, a flipped byte, a dropped byte, a duplicated byte and a reversed order each change it

How it is built

Every address this ROM tests comes from one authoritative host model, tools/snes_cartmap.py — a direct port of the bsnes-jg cartridge bus (Database/boards.bml plus Bus::map/mirror/reduce), not a paraphrase of a wiki page. A host test greps the vendored boards.bml so a transcription drift fails on the desktop instead of in an emulator. The linker script, the canary addresses, the segment lists, the expected bytes and the final oracle are all generated from that model; the C source contains no addresses of its own.

The model exposes the two directions deliberately asymmetrically. decode(bank, addr) is mirror-aware emulator truth. file_to_cpu(offset) returns only the one canonical CPU address and raises on an addressing hole. A descriptor that named a mirror would still read the right byte — which is exactly the bug class this cartridge exists to catch — so the emitting side is restricted and the reading side is permissive.

Written in C with the llvm-mos 65816 toolchain under +mos-a16 (a runtime far pointer is a 32-bit value, so the cross-bank cursor needs native 16-bit mode). Hit Verify fidelity to reproduce the build gate's WRAM assert live in this tab — the same oracle the host model and the cycle-accurate bsnes-jg core agree on.

ROM SHA-256 bffc0b0a1ffc1e7e0c413b1bc54578dfffed32f922acbd3da83466575ffdd7e1. Verified on bsnes-jg (native and this in-browser core). MAME was not available on the build machine — it needs the SPC700 IPL, which is not distributable — so the emulator evidence here is bsnes-jg only.