References: ref-docs/2026-06-24-coprocessors.md (the primary source for this doc);
ref-docs/research-report.md §§6–7; docs/cartridge-format.md (the header bytes);
docs/adr/0003 (tiering honesty gate); docs/adr/0004 (RTC determinism). Cited inline:
SNESdev Memory map / ROM header, SnesLab, Fullsnes, bsnes State-of-Emulation IV.
This doc is the SPEC, not history — update it in the same PR as the code. Pin behavior against the test ROMs first.
The cart crate (rustysnes-cart) owns the ROM/SRAM memory model and the coprocessor
families — each coprocessor is a "mapper-equivalent" with its own bus window and clock
(docs/architecture.md §4). It exposes a Cart trait with default-no-op hooks so the CPU
and PPU never special-case a board.
Per ref-docs/2026-06-24-coprocessors.md §A (SNESdev Memory map):
| Model | $FFD5 | Layout | Header offset | Max |
|---|---|---|---|---|
| LoROM | $20 | 32 KiB windows in $8000–$FFFF of each bank; A15 skipped; A16–A21 → ROM A15–A20 | $007FC0 |
4 MiB |
| HiROM | $21 | 64 KiB linear banks, full ROM at $C0–$FF; data crosses banks freely | $00FFC0 |
4 MiB |
| ExHiROM | $25 | >4 MiB (≤~8 MiB): $80–$FF = first 4 MiB, $00–$7D = extra; Tales of Phantasia, Star Ocean | $40FFC0 |
~8 MiB |
| ExLoROM | — (unofficial, no dedicated value) | LoROM extension for >4 MiB titles (mostly homebrew / flashcart) | $407FC0 |
~8 MiB |
SRAM mapping is board-dependent — no single canonical table. LoROM SRAM typically banks
$70–$7D/$F0–$FF $0000–$7FFF; HiROM SRAM typically banks $20–$3F/$A0–$BF $6000–$7FFF. Battery
is flagged in $FFD6 low nibble ($2). Build per-board windows from the cartridge database +
ares board definitions during Phase 4 (ref-docs/research-report.md "Open questions" #3).
SRAM size is bounded, and the bound is load-bearing (v1.26.0). $xFD8 declares 1 << N KiB
with N an arbitrary byte out of an untrusted image, and header::MAX_SRAM_SIZE caps the result at
512 KiB — the smallest value that cannot refuse a real cartridge, since LoROM's window reaches
448 KiB, HiROM's 256 KiB, and SA-1 BW-RAM tops out at 256 KiB. A header claiming more is describing
memory the console has no way to address.
Without the cap, 1024 << N panicked in debug builds for N >= 64 and in release masked the
shift, handing board::select a vec![0u8; header.sram_size] of up to 4 GiB from any downloaded
ROM (wasm32 was worse: usize is 32 bits, so the panic began at N >= 32). Found by
fuzz/fuzz_targets/rom_header.rs; see fuzz/README.md.
It clamps rather than rejects on purpose. The field is an allocation hint, not a contract — every
board already wraps its accesses to sram_size (% sram_size in the table below) — and real dumps
do carry garbage here, so rejecting would refuse images that load correctly today. bsnes and ares
avoid the question entirely by resolving size from a board database instead of the header; absent
that database, a bound is the equivalent.
Base LoROM/HiROM/ExHiROM now decode against real rom: Box<[u8]> + zeroed sram: Box<[u8]>
storage. The (bank, addr) → backing-store math (bank = addr24 >> 16, addr = addr24 & 0xFFFF):
| Model | ROM region(s) | ROM offset formula | SRAM window | SRAM index |
|---|---|---|---|---|
| LoROM | every bank, $8000–$FFFF |
((bank & 0x7F) << 15) | (addr & 0x7FFF) |
banks $70–$7D / $F0–$FF, $0000–$7FFF |
(lo-0x70)*0x8000 + addr, % sram_size |
| HiROM | $40–$7D / $C0–$FF full 64 KiB; $00–$3F / $80–$BF $8000–$FFFF |
((bank & 0x3F) << 16) | addr |
banks $20–$3F / $A0–$BF, $6000–$7FFF |
(lo-0x20)*0x2000 + (addr-0x6000), % sram_size |
| ExHiROM | same regions as HiROM | high | ((bank & 0x3F) << 16) | addr, where high = (bank & 0x80 != 0) ? 0 : (1<<22) |
banks $20–$3F (low half), $6000–$7FFF |
as HiROM |
| ExLoROM | every bank, $8000–$FFFF |
high | ((bank & 0x7F) << 15) | (addr & 0x7FFF), where high = (bank & 0x80 != 0) ? 0 : (1<<22) |
banks $70–$7D / $F0–$FF, $0000–$7FFF (as LoROM) |
as LoROM |
The ExHiROM/ExLoROM high bit is A23-inverted: banks $80–$FF (A23=1) select the first 4 MiB;
banks $00–$7D (A23=0) select the extra 4 MiB. ROM offsets are folded to rom_size by the
mirror helper (the standard cartridge address-mirroring rule): power-of-two sizes mask, non-power-of-
two sizes split the largest power-of-two block linear + mirror the remainder. SRAM size is
if $FFD8 == 0 { 0 } else { min(MAX_SRAM_SIZE, 0x400 << min($FFD8, 16)) } — see the bound above,
which is load-bearing and not a formatting detail; ROM and open-bus regions are read-only.
ExLoROM provenance. Unlike LoROM/HiROM/ExHiROM, ExLoROM has no dedicated $FFD5 mode
value — ares/bsnes both document it as unofficial (ref-proj/ares/mia/medium/super-famicom.cpp:
"ExLoROM mode is unofficial, and lacks a mapping mode value"; real carts often report plain
LoROM's $20 there). The decode formula above is not a guess from the header-detection
heuristic — it's sourced directly from bsnes's own runtime board database
(ref-proj/bsnes/bsnes/target-bsnes/resource/system/boards.bml, board: EXLOROM /
EXLOROM-RAM: map address=00-7d:8000-ffff mask=0x808000 base=0x400000 / map address=80-ff:8000-ffff mask=0x808000 base=0x000000), decoded against bsnes's Bus::reduce
bit-packing algorithm (sfc/memory/memory.cpp) — which, for that mask, is exactly the LoROM
packed offset ((bank & 0x7F) << 15) | (addr & 0x7FFF) with the same A23-inverted 4 MiB
half-select ExHiROM already uses. No real ExLoROM ROM (commercial or homebrew) exists in this
project's local corpus, so this board has no golden-framebuffer validation — only the
formula-level unit tests in board.rs (docs/adr/0003's honesty gate: this is flagged, not
silently presented as hardware-proven).
Per ref-docs/2026-06-24-coprocessors.md §§B–C. Emulation-approach key: the NEC DSP
family / ST01x / ST018 / CX4 use LLE (run the dumped chip program ROM — the user supplies
it); Super FX and SA-1 run their program from cart ROM (no chip dump).
| Chip | Core | Clock | ~Games | Shares core? | Emu | Tier |
|---|---|---|---|---|---|---|
| DSP-1/1A/1B | µPD77C25 | ~7.6–8 MHz | 15+ | µPD77C25 family | LLE (prog ROM) | Core/Curated |
| DSP-2/3/4 | µPD77C25 | ~8 MHz | 1 each | µPD77C25 | LLE | BestEffort (shared) |
| Super FX / GSU-1/2 | Argonaut RISC | 10.74 / 21.47 MHz | ~8 | no | cycle-accurate (cart ROM) | Core/Curated |
| SA-1 | 65C816 | 10.74 MHz | ~35 | (65C816) | cycle-accurate (cart ROM) | Core/Curated |
| S-DD1 | Nintendo ASIC | — | 2 | no | algorithm-exact | BestEffort |
| SPC7110 (+RTC-4513) | Hudson ASIC | — | 3 | no | algorithm + frozen RTC | BestEffort |
| CX4 | Hitachi HG51B169 | 20 MHz | 2 | no | LLE (prog ROM) | BestEffort/Curated |
| OBC1 | simple ASIC | — | 1 | no | HLE | BestEffort |
| ST010 / ST011 | µPD96050 | ~10 / 15 MHz | 1 each | µPD96050 (≈77C25) | LLE (shared) | BestEffort (shared) |
| ST018 | ARMv3 | ~21.44 MHz | 1 | no | LLE ARM core | BestEffort (implemented, coproc::armv3) |
| S-RTC | Sharp S-RTC | — | 1 | no | HLE + frozen time | BestEffort |
One µPD77C25 / µPD96050 LLE engine covers DSP-1/2/3/4 and ST010/011 — six chips, one
engine. Implement it once in rustysnes-cart and drive each chip's program/data ROM through
it. This is the single biggest economy in the coprocessor breadth phase.
An implementation of the NEC uPD7725/uPD96050 DSP from its documented architecture (cross-checked against ares, ISC, as a behavioural oracle), parameterized by Revision (Upd7725 =
DSP-1..4, 2 K×24 program + 1 K×16 data ROM; Upd96050 = ST010/011, 16 K×24 + 2 K×16). The full
NEC DSP instruction set is decoded from the 24-bit word (OP / RT / JP / LD): the K×L signed
multiplier pipeline, the dual accumulators + 6-flag condition sets, the 16-deep call stack, the
program/data ROM + data RAM, and the DR / SR / DP host ports. Registers wrap at the revision's
PC/RP/DP widths.
Host synchronization (the only cross-clock coupling), pin-exact / master-clock-stepped: the chip
free-runs on its own oscillator (~7.6 MHz µPD7725; µPD96050 at 11 MHz on the ST010 but 15 MHz on
the ST011 — a wired board selects its own rate via Upd77c25::with_rate, since the two share the
Upd96050 register-width revision but not the clock) and hand-shakes the CPU solely
through the RQM ("request for master") status bit — DSP-1 games always poll SR.rqm, never a
wall-clock cycle count. The engine models that literally as a free-running core clocked off the
master scheduler: Upd77c25::tick_master, driven once per master clock from each NEC-DSP board's
Board::coprocessor_tick, advances the DSP on its own gcd-reduced fractional divisor
(Revision::rates → 760_000/2_147_727 for the µPD7725, 1_100_000/2_147_727 for the µPD96050),
using the same integer accumulator (dsp_accum) the SPC700 uses (bus.rs, docs/adr/0004) — no
floats, fully deterministic, serialized in the NDSP save-state section. Host reads/writes
(read_dr/write_dr) just exchange the current DR/SR; the DSP has already produced the value
on its own clock, exactly as hardware does, so the RQM handshake now takes a hardware-realistic
number of cycles rather than resolving in zero emulated time. run_until_rqm survives only for the
one-time firmware-load prime (the power-on head-start, before the game's first poll) and for
standalone unit tests that have no bus to tick the chip; the live bus path never calls it.
This replaced an earlier catch-up-on-host-access model that drained the DSP synchronously inside
read_dr/write_dr. Its one subtle misuse — stopping at the first RQM=set (which a host-input
read, src == 8, raises as a side effect while the firmware still has pending setup, notably
clearing DRC to 16-bit) instead of at the firmware's genuine JRQM/JNRQM host-wait spin —
mis-framed the DSP-1 continuous-mode parameter block by one host write and was the Mode-7 flat-floor
bug. A continuously-clocked free-runner structurally cannot stop early, so that entire hazard class
is gone (v1.22.0 fixed the visible symptom value-exact; the free-running rewrite retired the model
debt and aligned the DSP with the SPC700/GSU/ST018 pattern).
Phase 4's second Core/Curated coprocessor. Unlike the NEC DSP family there is no chip-ROM
dump — the GSU program lives in the cartridge ROM the user already owns — so the board is
functional the moment a Super FX cart loads (docs/adr/0003: never silently degraded, and here
nothing to degrade).
The GSU core (coproc::gsu::Gsu) is a clean-room port of ares' GSU + SuperFX components
(ISC). It implements the full Argonaut RISC: R0–R15 (R15 = PC) with the FROM/TO/WITH source/dest
register-select prefixes; the ALT1/ALT2/ALT3 composite-mode machine that re-skins each opcode
(e.g. add→adc→add #N→adc #N); the ALU + signed/unsigned mult/umult + the
fmult/lmult 16×16 multiplier; the ROM buffer (ROMBR:R14 with R-flag busy + latency) and the
RAM buffer (RAMBR/RAMADDR with the deferred-write latency); the 256-byte / 32-line opcode cache
(CACHE, cbr, the $3100–$32FF cache window); the 1-instruction pipeline that gives the
GSU its branch delay slot (peekpipe/pipe); the PLOT/RPIX pixel-plot pipeline with the two-deep
pixel cache, the color/cmode colour logic (dither / freeze-high / high-nibble / transparent),
and the SCBR/SCMR screen-base + 2/4/8 bpp character-format addressing; and the SFR status flags
(Z/CY/S/OV, Go, R, ALT1/2, B, IRQ).
Host-sync (the only cross-clock coupling). The GSU is started by the CPU writing R15's high
byte at $301F, which sets Go and begins execution at (PBR:R15); the chip free-runs until
STOP clears Go (and, unless CFGR masks it, raises the cart IRQ), and software polls SFR for Go.
Like the SPC700, ST018, and (since v1.23.0) the NEC DSP, the GSU is a free-running,
master-clock-stepped cothread (ares' SuperFX::main): the Bus advances it one master clock per
Board::coprocessor_tick, and Gsu::tick paces each instruction's real clock cost out one tick at a
time (its owed counter over Gsu::step_one's per-access checkpoints). So a Go burst does not
drain to completion atomically inside the arming bus write — the CPU keeps running in between, and a
two-pass render split across multiple Go bursts within a frame stays in step. Integer-only and
fully deterministic (docs/adr/0004). (An earlier run-to-completion model — the DSP-1 run_until_rqm
analogue — was retired; the mid-run interleave is what the two-pass-render case needs.)
The Super FX board (coproc::superfx::SuperFxBoard) owns the ROM (shared, read-only) and the
Game Pak RAM (the GSU plot bitmap, sized from the header clamped to a 64 KiB minimum, power-of-two
masked), intercepts the GSU register window, and decodes the LoROM Super FX CPU map:
| Region (banks : addr) | Target |
|---|---|
$00–$3F,$80–$BF : $3000–$32FF |
GSU registers + opcode-cache window |
$00–$3F,$80–$BF : $8000–$FFFF |
Game Pak ROM (LoROM windows) |
$40–$5F,$C0–$DF : $0000–$FFFF |
Game Pak ROM (linear) |
$70–$71,$F0–$F1 : $0000–$FFFF |
Game Pak RAM (the plot bitmap) |
$00–$3F,$80–$BF : $6000–$7FFF |
Game Pak RAM low window (8 KiB) |
Bus arbitration (not simultaneous — edge case #3). While Go is set the GSU owns whichever of
ROM/RAM its SCMR RON/RAN bits grant; a CPU ROM read then returns the hardware "snooze vector"
(ares CPUROM::read) and a CPU RAM read returns open bus. Run-to-completion-on-Go serialises this
naturally; the checks are kept for fidelity. The GSU and the CPU share the same ROM/RAM bytes,
so a GSU plot into $70:xxxx is exactly what the CPU then DMAs to VRAM.
Coprocessor::SuperFx routes through board::select to this board (the base board is never
built — Super FX re-decodes the map itself). Tier stays Curated and is in the honesty
oracle set. Validated by the superfx_oncart harness gate (58 Krom GSU ROMs: detection +
GSU-executed liveness + a FillPoly-into-RAM plot-pipeline assertion + deterministic golden;
see the test plan) plus engine unit tests (a hand-assembled ibt/stop program through the full
host-sync path, plus the per-instruction Krom GSUTest suite booted on the System).
Phase 4's third Core/Curated coprocessor and the most complex: the SA-1 is a second WDC
65C816 @ ~10.74 MHz (master clock / 2) plus a support ASIC. Like Super FX it carries no chip-ROM
dump — the SA-1 program lives in the cartridge ROM — so the board is functional the moment an
SA-1 cart loads (docs/adr/0003).
Why it spans two crates. The one-directional crate graph forbids rustysnes-cart from
depending on rustysnes-cpu. So coproc::sa1::Sa1Board owns the entire SA-1 system state and
exposes the SA-1 CPU's memory view + control lines through the Board second-CPU hooks
(has_second_cpu / second_cpu_read / second_cpu_write / second_cpu_running /
second_cpu_take_reset / second_cpu_poll_nmi / second_cpu_poll_irq / second_cpu_tick);
rustysnes-core owns the second rustysnes_cpu::Cpu and steps it (see docs/scheduler.md §SA-1).
The board is a clean-room port of ares' sfc/coprocessor/sa1 (ISC).
The SA-1 system (coproc::sa1) implements: the $2200–$23FF register file (SA-1 control/reset,
the bidirectional S-CPU↔SA-1 IRQ/NMI/message lines, the S-CPU NMI/IRQ vector redirect SNV/SIV); the
Super-MMC ROM banking (CXB/DXB/EXB/FXB — four selectable 1 MiB blocks projected into the LoROM
$8000–$FFFF windows and the HiROM $C0–$FF banks); BW-RAM (shared battery RAM, the $2224
8 KiB-block S-CPU window + the $40–$4F linear image + the SA-1 $60–$6F 2/4 bpp bitmap and
$40–$5F linear projections, with the SWEN/CWEN/BWPA write-protect); I-RAM (2 KiB internal,
SIWP/CIWP per-256-byte write-protect); the arithmetic unit ($2250–$2254: signed multiply /
unsigned divide / cumulative-sum sigma with the 40-bit accumulator + overflow); the
variable-length bit processor ($2258–$225B, $230C/$230D); the H/V timer (the linear /
HV counter that raises the SA-1 timer IRQ); and the DMA unit (normal ROM/BW-RAM/I-RAM transfer
plus the type-1 and type-2 character-conversion DMA that transcodes linear BW-RAM ↔ planar
I-RAM).
S-CPU (main) memory map handled by Board::read24/write24:
| Region (banks : addr) | Target |
|---|---|
$00–$3F,$80–$BF : $2200–$23FF |
SA-1 registers (S-CPU side) |
$00–$3F,$80–$BF : $3000–$37FF |
I-RAM (2 KiB) |
$00–$3F,$80–$BF : $6000–$7FFF |
BW-RAM (8 KiB block, $2224 BMAPS) |
$00–$3F,$80–$BF : $8000–$FFFF |
ROM (Super-MMC blocks C/D) |
$40–$4F : $0000–$FFFF |
BW-RAM (linear) |
$C0–$FF : $0000–$FFFF |
ROM (Super-MMC blocks) |
The reset/interrupt handshake. The SA-1 powers up held in reset (RESB asserted). The S-CPU
programs the SA-1 reset vector (CRV) + Super-MMC banks, then clears RESB ($2200) — the board
latches a reset edge that rustysnes-core consumes to reset the second CPU (its reset/NMI/IRQ
vector fetches are redirected to CRV/CNV/CIV inside second_cpu_read, since the SA-1 uses its own
vectors, not the ROM $FFEx vectors). The SA-1→S-CPU IRQ is the board's irq_pending(), ORed into
the main bus IRQ line; the S-CPU→SA-1 IRQ/NMI drive the second CPU's poll_irq/poll_nmi.
Coprocessor::Sa1 routes through board::select to this board (the base board is never built —
SA-1 owns its own Super-MMC decode). Tier stays Curated and is in the honesty oracle set.
Validated by the sa1_oncart harness gate (18 staged commercial SA-1 carts: per-ROM SA-1 detection
- S-CPU↔SA-1 register traffic, an aggregate "the SA-1 CPU executed millions of cycles" liveness floor — Super Mario RPG, both Kirby titles, PGA Tour 96, Power Rangers Zeo, … — and a deterministic golden framebuffer) plus board unit tests (decode regions, reset+vector handshake, arithmetic unit, I-RAM/BW-RAM round-trips, Super-MMC ROM windows).
Dsp1Board wraps a base LoROM/HiROM board (ROM + SRAM decode delegated) and intercepts only the
DR/SR window. There is no canonical per-game window table; the board picks the de-facto window
from map mode + ROM size — the heuristic snes9x/bsnes use absent a cartridge DB, which coincides
with every ares DSP-1 board definition:
| Map mode / size | DSP window (banks : addr) | DR / SR split |
|---|---|---|
| HiROM | $00–$1F,$80–$9F : $6000–$7FFF |
DR $6xxx, SR $7xxx |
| LoROM, ROM ≤ 1 MiB | $30–$3F,$B0–$BF : $8000–$FFFF |
DR $8000–$BFFF, SR $C000–$FFFF |
| LoROM, ROM > 1 MiB | $60–$6F,$E0–$EF : $0000–$7FFF |
DR $0000–$3FFF, SR $4000–$7FFF |
Firmware is user-supplied, never committed (docs/adr/0003, edge case #2). The µPD77C25 runs a
fixed 8 KiB chip-ROM dump (dsp1.rom or the revised dsp1b.rom): the program ROM (2048 LE 24-bit
words) followed by the data ROM (1024 LE 16-bit words). Place it at the gitignored
tests/roms/external/firmware/dsp1*.rom and install it via
Cart::install_coprocessor_firmware(&bytes). Absent the dump the board is inert — SR/DR read
as open bus, the game wedges on its first DSP poll — it is never silently degraded.
- DSP-1 (
Core/Curated): NEC µPD77C25, Mode-7 3D math; 15+ games (Super Mario Kart, Pilotwings); memory-mapped DR/SR command ports. - Super FX / GSU (
Core/Curated, implemented —coproc::gsu+coproc::superfx): Argonaut RISC plotting into bitmap RAM; 10.74 MHz (Mario Chip 1) or 21.47 MHz (CLSR); 32/64/128 KB cart RAM arbitrated with the SNES CPU (not simultaneous, the snooze-vector/open-bus model); runs its program from cart ROM (no chip dump); host-synced on the Go flag; Star Fox, Yoshi's Island (GSU-2), Doom. See "The GSU core + the Super FX board" above. - SA-1 (
Core/Curated): a second 65C816 @ 10.74 MHz — the most complex coprocessor. Registers $2200–$230E; I-RAM $3000–$37FF; shared BW-RAM (8-bit half-speed, 1-cycle stall per access); Character-Conversion DMA + arithmetic unit; ~35 games (Super Mario RPG, Kirby Super Star). Reuses the 65C816 core fromrustysnes-cpu. - RTC chips (S-RTC, SPC7110's RTC-4513): the determinism hazard — HLE backed by
frozen / seeded host time, never live wall-clock (
docs/adr/0004). The RTC-4513 (coproc::epsonrtc::EpsonRtc) is implemented as a 3-register ($4840chip-select/$4841data/$4842ready) handshake over a 16-nibble register file, seeded to an all-zero epoch and never advanced except by explicit register writes. - DSP-2 / DSP-3 / DSP-4 (
BestEffort, implemented —coproc::necdsp_variant): the same µPD77C25 LLE engine as DSP-1, title-detected and wired viaNecDspVariantBoard. DSP-2 uses the generic bit-0 DR/SR split; DSP-3 (v1.24.0, SD Gundam GX) shares that split over the full$8000–FFFFwindow (snes9xM_DSP3_LOROM) — its Shift-JIS title decodes to an empty UTF-8 string, so it is detected by raw title bytes, which also corrects its prior mis-detection as DSP-1; DSP-4 needed a DSP-1-style half-window-boundary split instead (found by tracing a real Top Gear 3000 boot-time hardware check that expects both bytes of a 16-bit compare to come from the same port). Validated against real Dungeon Master / SD Gundam GX / Top Gear 3000 (detection + chip liveness). - ST010 / ST011 (
BestEffort, implemented —coproc::necdsp_variant): the µPD96050 LLE engine (alsocoproc::upd77c25), bit-0 DR/SR split + the DP battery data-RAM window. ST011 (v1.24.0, 2-dan Morita Shougi) is the identical board to ST010 but runs at 15 MHz (vs ST010's 11 MHz — the engine takes an explicit rate viaUpd77c25::with_rate), and declares the$F"custom" chipset nibble so it is routed to the DSP family by title. Validated against real F1 ROC II (ST010) and 2-dan Morita Shougi (ST011). - S-DD1 (
BestEffort, implemented —coproc::sdd1): a Golomb-code + adaptive-binary- probability decompressor that streams during a fixed-address DMA transfer (a newBoard::notify_dma_channelhook lets the cart snoop$43n2-$43n6DMA-register writes, sincerustysnes-core::Dmaowns those registers directly). No chip dump — decompresses the cart's own ROM. Validated against real Star Ocean / Street Fighter Alpha 2. - CX4 (
BestEffort/Curated, implemented —coproc::hg51b+coproc::cx4): a clean-room Hitachi HG51B S169 core (sequential mask/value opcode decode transcribed from ares'pattern(...)strings). No chip dump for the program (runs from cart ROM); only a 3 KiB data-ROM constant table (cx4.rom) needs external supply. Validated against real Mega Man X2 / X3. - OBC1 (
BestEffort, implemented —coproc::obc1): dedicated 8 KiB RAM behind a reprogrammable cursor register. Validated against real Metal Combat: Falcon's Revenge. - SPC7110 (
BestEffort, implemented; the local ROM dump used to test it turned out to be a fan-translation, not the original cartridge — see below —coproc::spc7110): a decompression unit (Hudson adaptive binary range coder over 1/2/4bpp planes), data-port unit, ALU, and memory-control unit (four independently-bankable 1 MiB data-ROM windows). Paired with the RTC-4513 above on its one commercial title, Far East of Eden Zero. Cartridge geometry note: unlike every other coprocessor here, SPC7110 carts physically carry a separate small PROM (program) chip plus a much larger DROM (data) chip, concatenated in a raw dump;coproc::spc7110::selectguesses the split (1 MiB PROM) from Far East of Eden Zero's documented physical geometry — there is no header field or generic formula that recovers this split for an arbitrary SPC7110 title. Confirmed and fixed (v0.4.0):datarom_read/mcurom_read's PROM/DROM lookups used a plainoffset % lenfold; real hardware (aresBus::mirror,sfc/memory/inline.hpp) instead repeatedly strips the largest power-of-two block that keeps the address in range — the two agree only when the buffer size is itself a power of two, which Far East of Eden Zero's 6 MiB DROM (7 MiB image − 1 MiB PROM) is NOT. A register-selected read past the physical chip size but inside the addressable window (r4830-r4833select up to 8 MiB) silently returned the WRONG byte, corrupting whatever data-ROM-resident table the game read through it. Ported the realBus::mirroralgorithm (spc7110::bus_mirror) and applied it to every PROM/DROM lookup; the wild-PC excursion this caused moved from ~20-30 frames into boot (BRK-storming into unmapped low banks, per the original diagnostic) to ~90+ frames, and it now self-recovers via a BRK/RTI oscillation instead of a permanent crash.v0.8.0: ported ares' SPC7110 cothread timing exactly — the DCU-begin-transfer ($4806)/multiply ($4825)/divide ($4827) triggers are deferred one master-clock tick (dcu_pending/mul_pending/div_pending, consumed in a newcoprocessor_tickoverride), not completed synchronously within the register write — a real, independently-verified accuracy fix (9/9 unit tests), but watchpoint-based tracing (T-81-001b) confirmed it does not fix the boot gap: those triggers are never written at all during this boot's crash path. Still open, substantially narrowed: the same watchpoint trace shows$7E0800-08FF(containing the crashingRTI's$0848target) is written exactly once at reset and never again across 60 real seconds of boot; no SPC7110 register is ever touched again either; and holding Start the whole time changes nothing. A newrustysnes_cpu::disasmdisassembler + branch trace then found the$00:F416/$20:20xxframing was itself incomplete: the CPU actually spends most of its time in a real, coherent VRAM-upload loop in bank$4F(STA $2118/$2116), not stalled — until it hits a literalJSL $4FFB80(confirmed present in the raw dump, not a read artifact). Bank$4Fis in$40-$7D, which two more real bugs (found by cross-checkingref-proj/ares's own board database,board: SHVC-LDH3C-01, the exact board this title uses) turned out to mishandle: (1)$40-$7Dshould be unmapped (MappedAddr::Open), not a$C0-FFmirror — an earlier session's claim otherwise was never checked against this database; fixed inread24/map. (2) the DROM buffer was 2 MiB oversized — the committed dump is 7 MiB but the real physical chips total 5 MiB (1 MiB PROM + 4 MiB DROM per the same database), soselectwas treating 2 MiB of trailing dump padding as real DROM and feedingbus_mirrorthe wrong fold length; fixed by slicing exactlyPROM_SIZE + DROM_SIZE. Both fixes are independently verified. A fourth, systemic bug found alongside these: the cart layer's open-bus fallback returned a hardcoded0forMappedAddr::Openinstead of echoing the Bus's real open-bus latch (ares'Bus::read(address, data)pattern) — fixed viaCart::read24now taking the caller's open-bus byte as a parameter (rustysnes-cart/src/lib.rs), benefiting every board, not just SPC7110. With this fix, theJSL $4FFB80dead end now lands on a stable, harmless open-bus spin loop (AND $3D3D,Xechoing the last-latched byte forever) instead of a deterministicBRK— a more honestly-modeled failure, but still not a fix on its own — until a follow-up session asked the question the "shipped commercial title cannot legitimately jump into unmapped space" observation was pointing at: is this actually the commercial title's ROM? It is not. Three independent checks (a SHA256 mismatch againstref-proj/ares's own database entry for this exact board; a header checksum that only self-validates against this file's non-standard 7 MiB size, not the real cartridge's 5 MiB; and a public forum thread documenting this exact fan-translation's memory map) confirm the local dump is the English fan-translation, which adds a 1 MiB "Expansion ROM" region mapped at CPU banks$40-$4Fthat exists only in the patch, never on real hardware — precisely the bank ($4F) thisJSLtargets. RustySNES's mapping of$40-$7Das unmapped is correct for the real cartridge; it simply doesn't (and was never meant to) implement a fan-patch-only memory region. This is a ROM-sourcing gap, not an open emulation bug — full evidence chain indocs/audit/spc7110-boot-crash-2026-07-08.md, which every fix above (root cause #1, the DCU/ALU timing, the$40-$7Dmapping, the DROM-size fix, and the systemic open-bus fix) remains a real, independently-verified accuracy improvement regardless of this finding. - S-RTC (
BestEffort, implemented —coproc::sharprtc): a standalone Sharp S-RTC real-time clock (Daikaijuu Monogatari II, an ExHiROM title; ares boardEXHIROM-RAM-SHARPRTC). A DIFFERENT chip/protocol from SPC7110's paired Epson RTC-4513 despite the similar name: a 2-register ($2800data,$2801unused) handshake that walks a 13-slot decimal clock file (second/minute/hour/day/month/year + an auto-computed weekday) through aReady -> Command -> Read/Writestate machine driven by magic values written to$2800($0D=enter read,$0E=enter command, then$00=write /$04=reset-to-epoch as the command byte). Wraps a baseExHiRomboard (SharpRtcBoard::new); ROM/SRAM delegate to it unchanged. Like the Epson RTC-4513, this port seeds a fixed epoch and never advances the clock other than via explicit register writes (docs/adr/0004's determinism contract). No commercial Daikaijuu Monogatari II dump exists in this project's local corpus, so this board has unit-test-level coverage only, not golden-framebuffer validation (docs/adr/0003); header detection is a best-effort title match ("DAIKAIJUU MONOGATARI"/"DAIKAIJU MONOGATARI"), the same posture already carried openly for CX4/SPC7110's own$F-nibble disambiguation. - ST018 (
BestEffort, implemented —coproc::armv3): a full ARMv3 (ARM6-class, pre-Thumb) CPU core, comparable in scope torustysnes-cpu's 65C816, not a small register-file port like this project's other BestEffort coprocessors. Implemented from the published ARM architecture definition, with reference emulators as behavioural oracles only — Mesen2 is GPLv3 and no code from it is incorporated (docs/originality-and-provenance.md§4). The scope decision was to model the ST018's actual ARMv3/ARM6-class part rather than the ARM7TDMI superset that ares'armdspreuses, since the real chip predates Thumb and never needed it. Built bottom-up: the barrel shifter/condition-codes/ ALU core, the register file + mode banking, the 3-stage pipeline (whose exact timing implicitly produces ARM's well-known "PC reads as address+8" quirk), the full instruction set (data processing, branch, MSR/MRS, exception entry,LDR/STR,LDM/STM, multiply/multiply-long,SWP/SWPB), and finally the SNES-side board wrapper (St018Board) — driven byBoard::coprocessor_tick(the same host-sync hook GSU/Super FX use) rather than the SA-1 second-CPU hooks, since this ARM core (unlike SA-1's second 65C816) is entirely self-contained withinrustysnes-cartand doesn't cross the one-directional crate graph.Coprocessor::St018is detected via a title match on the confirmed real cart, Hayazashi Nidan Morita Shogi 2 (NIDAN MORITASHOGI2) — an earlier version of this doc wrongly assumed Star Ocean, which uses S-DD1 only, no ARM coprocessor; no commercial dump exists in this project's local corpus to verify the exact title string against, the same honesty gap already carried openly for the other title-matched$F-nibble customs. Seedocs/st018-arm-notes.mdfor the full architecture notes, detection research, and build order.
The internal header ($FFC0–$FFDF) and the score heuristic live in docs/cartridge-format.md.
The cart crate scores the candidate header at $7FC0 / $FFC0 / $40FFC0 and picks the highest;
the $FFD6 high nibble selects the coprocessor family.
// rustysnes-cart
pub trait Cart {
fn read(&mut self, addr: u32) -> u8;
fn write(&mut self, addr: u32, value: u8);
/// Coprocessors that tick on the master clock advance here (default no-op).
fn tick(&mut self, master_cycles: u32) {}
fn sram(&self) -> &[u8]; // for battery save
fn tier(&self) -> CoprocessorTier; // Core | Curated | BestEffort (honesty gate)
}- DMA cannot cross a bank — relevant to LoROM/HiROM bank wiring
(
ref-docs/2026-06-24-ppu.md§5). - Chip-ROM-dump dependence (DSP/ST01x/CX4/ST018) must be feature-gated with an honesty
caveat — without the dump the board is non-functional, and it never backs the oracle
(
docs/adr/0003). - Super FX / SA-1 RAM arbitration is not simultaneous; model the access stalls.
- ExHiROM split addressing ($80–$FF first 4 MiB, $00–$7D extra) is the only >4 MiB case.
- RTC freeze — see
docs/adr/0004.
- Memory map / header: gilyon + undisbeliever ROMs boot under each map model; auto-detect picks the right one for the canonical commercial set.
- Coprocessors: Krom/PeterLemon GSU ROMs (reference-only); commercial dumps booted locally
with committed screenshots /
.snaponly (never the ROM —tests/roms/external/is gitignored). Tier each board and assert the honesty gate (docs/adr/0003). - Super FX (
superfx_oncart, featuretest-roms): boots the staged Krom GSU test ROMs (tests/roms/external/krom/CHIP/GSU/, CC0/homebrew, gitignored) — the2/4/8 bppPlotPixel/PlotLine/FillPoly demos + the per-instructionGSUTestsuite — on the full System and asserts (a)Coprocessor::SuperFxdetection, (b) the GSU actually executed its program out of cart ROM (non-zero coprocessor-activity count — only possible if the$3000–$32FFwindow is mapped right and the host-sync run path works), (c) theFillPolysuites plot a substantial bitmap into the Game Pak RAM (read back viaBoard::sram), proving the whole plot pipeline — opcode-cache fetch, thegetbl/getbhROM-buffer scan-table reads,ldw/stwRAM, and the PLOT pixel-cache → character-format flush — end-to-end at the cart boundary (PPU-independent), and (d) a deterministic committed golden framebuffer hash. A 4 bppFillPolypolygon also reaches the framebuffer; full PPU BG-mode coverage for 2/8 bpp is a PPU concern. The GSU instruction set is additionally exercised by theGSUTestper-opcode ROMs. - DSP-1 (
dsp1_oncart, featuretest-roms): boots the staged DSP-1 dumps on the full System with the user-supplied (gitignored)dsp1*.rom, asserting (a)Coprocessor::Dspdetection, (b) a non-zero RQM-handshake access count on both the LoROM (Pilotwings) and HiROM (Super Mario Kart) windows — only possible if the window is mapped right and the µPD77C25 returns RQM, (c) a committed deterministic golden framebuffer hash, and (d) the firmware-differential (the Mode-7 titles render differently with the chip installed). Engine decode/ALU/multiplier are unit-tested against a hand-assembled synthetic firmware (no copyrighted bytes).
- Per-board SRAM / coprocessor bus windows (no canonical table) — Phase 4 build-out.
- DSP nominal clock range (~7.6–8 MHz) — gated by test ROMs, not the number
(
ref-docs/2026-06-24-coprocessors.md"Flagged discrepancies").