RustyN64 v0.3.0
RustyN64 v0.3.0 "Microcode"
The RSP works, and it runs real game microcode. Phase 2 is complete: the Reality Signal
Processor executes — the scalar unit, the full 8-lane vector unit, and SP DMA — and libdragon's
real rdpq microcode boots on it and emits an RDP command list. Both of the phase's exit criteria
are met.
Every accuracy claim below is an oracle result with a committed runner, not a self-assessment,
and every one is reproducible from a clean checkout. As with v0.2.0, no chip beyond the two now
running (the CPU and the RSP) executes — docs/STATUS.md is authoritative.
Accuracy statement
| Criterion | Result |
|---|---|
| n64-systemtest, RSP category | Failed: 0 — 0 RSP-prefixed failures of 917 tests started |
| A real graphics microcode emits an RDP command list | held — libdragon's rdpq boots and emits, witnessed byte-for-byte |
cargo test -p rustyn64-test-harness --release --test systemtest -- --ignored
cargo test -p rustyn64-test-harness --test microcodeBoth run from a clean checkout with no external assets, ROM paths, or environment variables —
the n64-systemtest ROM and the rdpq microcode blob are committed to the repository.
Phase 1's criteria remain met (CPU/COP0/TLB/COP1 Failed: 0 and the ares golden-log 0-diff). The
suite-wide failure count fell from 413 to 93; every remaining assertion is cart/PIF (Phase 5)
or the RDP rasteriser (Phase 3), each that phase's criterion. The runner matches on the categories
to exclude, so a new RSP-side category added upstream lands inside the gate rather than being
silently skipped.
RSP
The scalar unit is close enough to a 32-bit R4000 that standard MIPS documentation covers most
of it, so what the code records is the differences — no multiply/divide unit, no 64-bit anything,
no traps, and the two rules that catch a CPU core reused wholesale:
- The PC is 12 bits and wraps. Every high bit of a branch or jump target is discarded, and
running off the end of IMEM at0xFFCcontinues at0x000rather than faulting. - Misaligned data accesses are correct, not faults. A
LWat0x001returns the four bytes at
0x1..=0x4; each byte wraps inside DMEM independently. On the VR4300 the same access is an
AddressError— the single easiest place to get the RSP wrong by reusing CPU code.
DMEM and IMEM are a Harvard pair (4 KiB each, mirrored across the CPU-visible window). BREAK
halts the core — including in a taken branch's delay slot, where it parks at the branch target.
The vector unit is the full 8-lane COP2: the register file and SU/VU moves, the 48-bit-per-lane
accumulator and the multiply family (single and accumulating forms), add/subtract/carry, the
compare/select group, the clip compares (VCL/VCH/VCR), VRND/VMULQ/VMACQ, and the
reciprocal/rsqrt units driven from committed ROM tables. The whole vector load/store family
(LQV/SQV, LRV/SRV, LPV/LUV, SWV) with its element-wrap edge cases is in, as are the
19 reserved "VZERO" opcodes that all write ACC_LO = vs + vt, zero vd, and touch no flags.
The microcode boot harness and the RSP → RDP seam
Phase 2's second exit criterion is that a real graphics microcode boots and emits a plausible
RDP command list. Rather than a toy, the harness runs libdragon's real combined RSPQ + rdpq blob
(vendored under third_party/libdragon-rsp/, Unlicense; assembled with mips64-elf-gcc, with the
blob, symbol map, and SHA256SUMS committed and gated in CI). Golden references are grounded in
hardware documentation, never another emulator (ADR 0008).
- The microcode boots to its idle
break(T-24-002), reproducing therspq_startboot state
in Rust, witnessed from a baseline that is itself unreachable as a pass. - It DMAs and dispatches a command queue (T-24-003 foundation).
- It emits an RDP command list (T-24-003/004): an
rdpqoverlay command is dispatched to its
resident handler — the overlay "registered" with three DMEM writes reproducing
rspq_overlay_register_internal— the command bytes are DMA'd to an RDRAM output buffer, and
DP_ENDis advanced through the DPC seam.
The seam itself: the RSP's COP0 registers c8–c15 are the RDP command registers, but the
rustyn64-rsp crate may not name rustyn64-rdp (the crate-graph rule in docs/architecture.md).
So an MTC0 to those registers is reported as su::StepResult::dp_write and Bus::rsp_tick
forwards it to Rdp::dpc_write — the same DPC register file the CPU drives at 0x0410_0000. Two
golden cases witness the emission: RDPQCmd_Passthrough8 (a raw 8-byte forward) and
RDPQCmd_SetFillColor32, where the microcode generates a SET_FILL_COLOR command byte-compared
against N64brew's documented 0x37 encoding — the 0xD6 → 0xF7 opcode transform is itself the
witness that the microcode generated, rather than passed through, the command.
Hardware behaviours worth naming
Several of these look like bugs on every reading and are not. Each is recorded in
docs/rsp.md and, where a constant is involved, docs/accuracy-ledger.md.
- The VU reads before it writes. Hardware reads the whole broadcast
vt/vsbefore writing any
lane, so a destructive broadcast (a lane whose source it will overwrite) must be snapshotted first
— otherwise a self-referential broadcast corrupts itself. - The 12-bit wrapping PC and correct misaligned accesses (above) are behaviours, not oversights;
both are pinned by n64-systemtest (RSP Wrap around, thesp_memoryout-of-bounds cases). c16–c31do not exist. The COP0rdfield is 5 bits but the RSP has only sixteen COP0
registers; the non-existent indices read zero and must not alias into the DP registers.- The reserved COP2 encodings are not no-ops. Nineteen undocumented "VZERO" opcodes have a
defined effect (ACC_LO = vs + vt,vd = 0), which the suite checks.
Verification infrastructure
The microcode test suite is committed (crates/rustyn64-test-harness/tests/microcode.rs) and
needs no toolchain to run — the blob and its symbol map are committed, and the tests parse the
linker's own symbol map rather than hand-copied constants. Each emission test starts from a state
unreachable as a pass and witnesses execution (BROKE at the idle PC, DP_END advanced past the
command) before trusting the captured bytes. Both are mutation-checked: severing the Bus→RDP
forwarding, or corrupting the overlay command_base, turns them red.
Blob reproducibility is a separate CI gate (sha256sum -c + assemble.sh) that needs the
mips64-elf toolchain; the test suite deliberately does not.
Known limitations
- The RDP and AI are LLE-shaped stubs; only the CPU and RSP execute. The RDP's DPC command
registers receive command lists, but nothing downstream rasterises them, and the AI plays no
audio. A greencargo testdoes not mean a subsystem works —docs/STATUS.mdis authoritative. - 93 n64-systemtest assertions fail, all cart/PIF (Phase 5) and the RDP rasteriser (Phase 3).
- An
MFC0of DP state the RDP mutated on its own is not yet reflected in the RSP'sc8–c15
shadow — a Phase 3 concern, when the RSP pollsDP_STATUSon the buffer-switch path. - The
test-roms/commercial-roms/ per-cratestdfeature flags are internal placeholders
that still gate zero code — nocfg(feature = …)exists for them yet, so--features test-roms
runs the same tests as a barecargo test. They reserve the wiring for later phases, not a public
toggle. - Several timing constants remain unmeasured —
M(memory access time), the exception-epilogue
cost, CP0I, RDRAM bank-state costs. Listed in the accuracy ledger as unmeasured, never fitted. - No save states, no netplay, no RetroAchievements. Later phases.
Compatibility
No save-state or public-API format exists yet, so nothing in this release can break one. The RSP↔RDP
seam is internal (a new StepResult field). ADR 0005's sub-cycle bus-timing refactor remains the
only anticipated MAJOR candidate, and only if Phase 7's accuracy triage concludes it is warranted.