Loading…
gathering yarn balls…
Cat Game Research
Frontier
Dev-log transmissions from the expedition

Where Super Mario World Hides Its Samples: Reverse-Engineering the SNES APU at Runtime

We built a native Snes9x devkit, traced the SPC700 for 1000 frames, and discovered that Super Mario World loads its BRR samples from ROM bank $07—not from the static disassembly at $0E. Here is how runtime diffing beats dead disassembly.

Static disassembly told us that the Super Mario World SPC700 program lives at LoROM bank $0E. It does. But that 32 KiB block is mostly other ROM data that the APU never touches. The actual samples the S-DSP plays back are not there. We traced the chip for 1000 frames with a native snes9x-devkit and found them hiding in ROM bank $07 — loaded by the driver at runtime, not by the static extraction we had been reading for weeks.

Bottom of a Super NES motherboard showing the S-APU, 5A22 CPU, PPU chips, and CIC
The S-APU (Sony SPC700 + S-DSP) lives next to the Ricoh 5A22 CPU on the Super NES motherboard. Photo by Evan-Amos, public domain.

What the APU actually is

The SNES Audio Processing Unit is a separate computer. It has a Sony SPC700 8-bit CPU running at roughly 1.024 MHz (the S-SMP's 24.576 MHz master clock divided by 24), a Nintendo S-DSP that mixes eight stereo voices at 32 kHz, and 64 KB of APU RAM (ARAM) shared between the SPC700 and the DSP. The main 5A22 CPU talks to the SPC700 over four 8-bit I/O ports, and the SPC700 keeps its own 64 KB memory map. It cannot read the cartridge ROM directly. Every byte the DSP plays has to be copied into APU RAM, or ARAM, first.

The DSP does not play raw PCM. It plays BRR — Bit Rate Reduction — a 9-byte block format that packs sixteen 4-bit nibble samples plus a header (range, filter, loop, end). BRR gives a 32:9 compression ratio, which is why 32 KB of ARAM can hold several seconds of sample data. We use the SnesLab BRR reference and the Wikipedia BRR overview as ground truth for the format; our decoder in sn-brr-audio.sh matches the exact integer filter coefficients from the Snes9x SPC_DSP.cpp source.

32:9
BRR Compression
8
Stereo Voices
64 KB
APU RAM (ARAM)
~1.024 MHz
SPC700 Core Clock

The static story, and why it is incomplete

Super Mario World uses the N-SPC Prototype v0.0 sound engine — also called Kankichi-kun — the earliest variant of Nintendo's common SPC driver, used only in SMW and Pilotwings. SnesLab documents the engine's Prototype variant with a VCMD table at $0F90 relative to the driver base and the ROM program at $0E8000. That is where our disassembly started.

Using snes2asm and spcdas, we extracted 32 KiB from $0E8000. It looks like an audio driver. It has the right reset vector, the right APUIO port references, and a VCMD dispatch table. But when we ran sn diff-firmware to compare the static extraction against a runtime ARAM dump, only about 12 KiB matched. The rest — roughly 20 KiB of the 32 KiB bank — was ROM data the driver never uploaded.

Static bank $0E vs. runtime ARAM dump
# 32 KiB extracted from ROM bank $0E:8000 (file offset 0x070000)
# Runtime ARAM dump after 60/300/1000 frames
# diff-firmware classes: CODE/DATA/RELOCATED/ZERO_INIT
Driver region (ARAM $0200-$30D4): ~11.9 KiB
Static 32 KiB bank contains ~17 KiB that was never loaded

ZERO_INIT does not mean silence. It means that address range in ARAM was never written by the SPC700. The gap is not a timing issue: we captured 60 frames, 300 frames, and 1000 frames, and the non-driver portion of the bank stayed identical. The static extraction simply overestimates the driver because it includes unrelated ROM data from the same bank.

Where the samples actually come from

The BRR data in ARAM starts around $8000. To find its source, sn-analyze-spc.sh takes 64-byte signatures from runtime ARAM at 1 KiB intervals and searches the ROM. The result is unambiguous: for Super Mario World, ARAM $8000 matches ROM $078004 and ARAM $9000 matches ROM $078F58. Both are in ROM bank $07, not bank $0E.

ROM BANK $0E$0E:8000-$0E:FFFF (32 KiB)Driver codeOther ROM dataROM BANK $07$07:8004+ BRR samples20 BRR sample blocks (512-byte matches)APU RAM (ARAM)$0000-$FFFF addressable, $0000-$7FFF usable$0200-$0210 src dir$0210-$30D4 driver$8000+ BRR sample dataIPL uploadDriver sample loaderDriver boundary $30D4 from runtime diff-firmware. BRR origin $078004 from signature scan.
Figure 1 — The SNES APU does not read ROM directly. The driver is uploaded from bank $0E to ARAM, then it copies BRR samples from bank $07 into ARAM $8000+ before the DSP can play them.

The source directory — a small table that maps each instrument number to a BRR start address and loop address — is placed at $0200. The driver itself sits just above it. The BRR samples sit far above, at $8000+. The driver is only about 145 bytes in our homebrew sys_test ROM and a few KiB in SMW; the samples are the bulk of the audio data, and they come from a different bank than the disassembly we started with.

The tools that made this visible

All of this work depends on the native snes9x-devkit binary we built from the CatGame sn/emu module. Unlike the old AppImage, it runs headless, dumps audio as WAV, and can trace the SPC700 program counter per instruction. The relevant commands are now part of the SNES games page and the reverse-engineering CLI.

Runtime capture and diff pipeline
./scripts/build.sh snes --emu
./build/linux-debug/bin/snes9x-devkit \
  --rom data/sn/roms/snes/oem/Super\ Mario\ World\ \(USA\).sfc \
  --frames 300 --trace-spc /tmp/spc.trace --trace-max 500000 \
  --dump-audio /tmp/smw.wav

sn analyze spc --capture 60 rom.sfc
sn diff-firmware static.bin runtime-aram.bin
sn brr-audio --verify /tmp/smw.wav

The trace output lets us see which code blocks execute, where branches go, and which VCMDs the music parser actually hits. The diff-firmware pass gives us a relocation map: which bytes in the static bank match ARAM, which are relocated, and which were never written. Combining the two tells us the true driver boundary and the BRR origin.

1000
Frames Captured
$30D4
Driver/Data Boundary
$078004
First BRR Origin
20
Verbatim BRR Matches

What this means for original driver design

We are not just documenting SMW. We are designing our own SPC700 driver for homebrew SNES ROMs built with llvm-mos. If we copied the static extraction from bank $0E blindly, we would ship 20 KiB of junk and miss the actual sample loader. Worse, we would not understand the contract between the 65816 CPU and the SPC700: the 65816 must upload the driver, then the driver must upload samples from ROM bank $07. That two-stage copy is the architecture.

Our homebrew sys_test ROM now uses a compact resident driver. The layout is the same pattern: a source directory at $0200, a driver at $0210, four tiny BRR samples at $02B0, and the DSP registers controlled through a latched command protocol. The 65816 side writes $2140-$2143 (CPU-side APUIO), the SPC700 side sees $F4-$F7, and the driver echoes the command back after latching parameters to avoid a race. This is the same pattern the N-SPC engine uses, simplified for our test scenes.

Legal / IP guardrail
We never publish ROM bytes, verbatim code, or copyrighted sample data from Super Mario World. The numbers in this post are memory addresses, file offsets, and our own measurements. The BRR origin was verified by matching 64-byte signatures, not by redistributing Nintendo's audio. The homebrew driver, samples, and tools are original. See the project security subdomain and our secure boot post for the broader hardware trust model.

Coverage, caveats, and what is next

SMW's title screen music only exercises a fraction of the N-SPC engine. Our 2000-frame trace covers 101 of 1748 branch/call edges (about 5%) and 401 unique program counters. Most of the unexecuted paths are instrument setup, echo, and other VCMD handlers that trigger in levels, the world map, or boss fights. The coverage is frame-count dependent: 300 frames gives 61 edges, 2000 frames gives 101. To get full coverage we need save-state captures at multiple scenes, which is a roadmap feature for the devkit.

CapabilityStatusNotes
Native snes9x-devkit buildimplemented>60 FPS on SMW, SDL3 display mode, headless capture
Per-instruction SPC traceimplementedConfigurable SPC_TRACE_MAX, call graph output
Diff-firmware relocation mapimplementedIdentifies static vs. runtime, CODE/DATA/ZERO_INIT/RELOCATED
BRR origin signature scanimplementedVerified for SMW bank $07
BRR roundtrip verificationroadmapCross-correlate decoded BRR against emulator WAV
Multi-scene coverageroadmapSave states at title, map, level, boss

The BRR roundtrip verifier is the next logical step. It would decode every BRR block back to PCM, capture the emulator output for the same SRCN, and run a spectral cross-correlation to prove our decoder matches the S-DSP. If you want the engineering context for the web side of this work, the companion post on running Snes9x at 60 FPS in WebAssembly covers the browser half of the same toolchain.

Cross-links

References

← All transmissions