Skip to content

Releases: doublegate/RustyN64

RustyN64 v0.8.0

Choose a tag to compare

@github-actions github-actions released this 29 Jul 18:52
5a18d53

v0.8.0 "Breadth" — Phase 7: the accuracy battery, and the first commercial pictures

Commercial cartridges render. Super Mario 64 draws its title screen, and 29 of
66 staged titles render through their own graphics microcode. Fourteen frames
are committed under screenshots/, every one rendered and looked at.

CUT CRITERIA (to-dos/VERSION-PLAN.md), both verified rather than assumed:

  • The battery reports a real pass rate: 54 probes, 100%, across two Angrylion
    oracle suites — 41 RDP rasteriser vectors + 13 VI scan-out vectors. Expected
    values are the oracle's, never our own output.
  • Every residual R-1 through R-23 carries a ledger disposition, and no
    residual is referenced anywhere in docs/ or to-dos/ without an entry.

THE TWO DEFECTS THAT UNBLOCKED IT WERE IN THE CPU AND THE PIF, NOT THE RDP

R-18 had been treated as a rasteriser gap for months. It was not.

  • ERET interrupt livelock (#189). ERET resolves in EX, clearing Status.EXL
    and pointing next_pc at EPC. The DC interrupt check runs one cycle later,
    saw EXL clear, and charged the interrupt to whatever sat in ex_dc — the
    ERET itself. EPC was overwritten with the ERET's own address, destroying
    the return address it was about to consume, so the handler returned to the
    ERET, which resumed at itself. Banjo-Tooie went from 1 to 80 distinct PCs
    and programmed the VI for the first time.

  • PIF "no device" flag (#190). Empty joybus ports never set the RX byte's
    bit 7, so osContInit reported four controllers on a one-pad console and
    Super Mario 64 halted in its own assert at 0x80246DD8. A second,
    pre-existing defect sat beside it: mark_no_device wrote the flag to
    resp - 1, the last TX data byte, never the RX byte software reads — it had
    never once landed correctly. SM64 went from 0 to 125,278 RDP commands and a
    title screen; Banjo-Kazooie from 0 to 133,625.

Oracle unchanged throughout: n64-systemtest Phase 1 Failed: 0, suite-wide 90.

T-71-003 — THE ONE CUT CRITERION CI CAN NEVER RUN

F3DEX and its per-studio variants are proprietary and ship inside commercial
ROMs that ADR 0008 forbids committing; rdpq is a genuine CI-gated LLE microcode
test but is not F3DEX and cannot substitute. By maintainer decision the
criterion closes on a reproducible local census (microcode_families.rs,
screenshots. VERSION-PLAN records it as the single documented exception rather
than an unstated weakening of the gate. The census asserts only that staged
titles render, never a pass rate — the corpus is whatever a machine happens to
hold, so a percentage from it would describe a ROM collection while looking
like an accuracy metric.

WHAT THE EVIDENCE STANDARD COST, AND WHY IT STAYS

"Lit pixels" was cited for weeks as proof of rendering and was wrong:
uninitialised RDRAM is non-black. The census proves it on its own data —
Rayman 2 and Namco Museum 64 report ZERO RDP commands with 123,540 and 137,681
lit pixels. A lit-pixel rule would have committed two frames of pure garbage as
evidence. Every screenshot in this release was rendered to PNG and viewed;
frames that scored well but looked wrong were rejected, and the rejections are
recorded because they localise real defects.

ALSO IN THIS RELEASE

  • mirror_s pinned against Angrylion (tex_tri_mirror_s_16) — which refuted the
    hypothesis it was written to confirm: the S-axis mirror path is correct, so
    the mirrored text in GoldenEye and WCW is something else.
  • tlut_en (Set Other Modes bit 47) decoded; the TLUT lookup gated on it
    rather than on the tile format.
  • commercial_boot measures through Bus::scanout_scaled, the path the frontend
    actually presents, and reports scan-out dimensions so a blanked VI is
    distinguishable from a frame that is genuinely black.

NOT CLAIMED

24 titles still issue zero RDP commands. They do not stall — they halt, each
for its own reason, exactly as SM64 halted before the PIF fix. Banjo-Tooie sits
in a B -1 with interrupts enabled and nothing pending; Donkey Kong 64 spins on
BNE v1, v0, -1 with IE = 0 and a NOP delay slot, unexitable by construction;
Jet Force Gemini halts with IP2 pending but masked. Each is a separate
root-cause hunt. Those, the mirrored-text cause, the ledger-extraction refactor
and 1080's IPL3 fault are v0.9.0 "Threshold" work.

RustyN64 v0.7.0

Choose a tag to compare

@github-actions github-actions released this 24 Jul 18:47
484a457

RustyN64 v0.7.0 "Shell" — Phase 6 (the first playable release)

Phase 6 wires the egui/wasm frontend to the real machine. RustyN64 now presents
a picture, plays sound, and takes controller input — end to end through the LLE
core — and can save, load, rewind, and run-ahead. This is the first release that
is playable.

== Executive summary ==

The shell presents the REAL machine, not a test pattern:

  • The window is driven by the VI scan-out (Bus::scanout, the LLE RDP/VI path).
  • The audio ring is fed by the real AI drain (the libdragon mixer microcode
    runs on the LLE RSP; the frontend resamples to the host device).
  • The pad reaches the game through the SI joybus.

New frontend capabilities, all frontend-side and off by default (ADR 0004, so
output stays byte-identical when unused):

  • Save-states: the whole System is serde-serialisable; restore is bit-identical.
  • Rewind: a bounded ring capturing a snapshot every N frames.
  • Run-ahead: present a speculatively-advanced frame, then restore the real
    timeline (speculative audio discarded).
  • A wasm browser entry point: a #[wasm_bindgen(start)] 2D-canvas demo that boots
    a homebrew ROM and blits the VI scan-out via web-sys, built with trunk.

== Honest scope of "playable" ==

Picture, sound, and control are demonstrated NATIVELY on the committed homebrew
ROMs through the real LLE VI/AI/SI paths, and save-state restore is bit-identical
(proven on a booted COMMERCIAL ROM). A commercial title boots and executes real
code but does NOT yet reach a rendered frame — the cross-subsystem VI-vblank /
RI-register / F3DEX gap tracked as accuracy-ledger R-18, deferred to the Phase
3/7 rasteriser + microcode work (v0.8.0 "Breadth"). This ships on the
demonstrated-playable path plus an honest ledgered gap, per the plan's escalation
gate — never a faked commercial pass.

== Exit criteria (oracle results, committed runners) ==

  • Native picture + sound + control on a real ROM (Phase 6 committable gate): MET
    — homebrew renders a verified golden frame via the VI (real_rom_frame), plays
    deterministic PCM via the AI (audio_play_rom), and reads the pad via the SI.
  • Save-state restore continues bit-identically (Phase 6 capstone): MET — a
    two-run trace compare (tests/savestate.rs) on a homebrew ROM (committable) and
    a booted commercial ROM (full RSP/RDP/AI/cart machine, local-only).
  • A commercial ROM playable WITH A PICTURE (VERSION-PLAN's literal criterion):
    DEFERRED (ledger R-18), validated when the Phase 3/7 work closes it.

== Technical details ==

  • Retail boot moved into the core (rustyn64_core::boot, ADR 0010): hle_boot,
    real_pif_boot, cic_seed, BootError are core APIs; the frontend and harness both
    consume them. EmuCore::load_rom now HLE-boots, so the shell runs a game.
  • Save-state serialisation: serde derives across every state type in
    rustyn64-cpu/-rsp/-rdp/-audio/-cart/-core; a new rustyn64-snapshot leaf crate
    provides #[serde(with)] helpers for the boxed-array backing stores; the
    cartridge ROM is #[serde(skip)]'d and re-attached on restore. Chip crates stay
    #![no_std] (serde's alloc feature).
  • wasm: native host deps (cpal/gilrs/rfd/directories) gated to
    cfg(not(target_arch = "wasm32")); wasm-bindgen pinned =0.2.126 to match
    web/Trunk.toml's CLI pin (the wasm-bindgen-pin CI gate). The load offset is
    derived from the ROM entry (entry & 0x1FFF_FFFF), matching real IPL3 semantics.

== Testing evidence ==

  • 712 workspace tests pass.
  • Full CI matrix green: macOS, Windows, ubuntu, and the test-roms job, plus fmt,
    clippy (native + wasm32 target), rustdoc (-D warnings), no_std, trunk-pin,
    and no-commercial-ROMs.
  • trunk build --release produces a working browser demo (dist/).

== Version ==

All workspace crates bumped 0.6.0 -> 0.7.0. Docs reconciled: STATUS.md,
CHANGELOG.md, VERSION-PLAN.md, ROADMAP.md, README.md.

Next: v0.8.0 "Breadth" (Phase 7 — the accuracy battery; the commercial title
frame that R-18 defers lands there).

RustyN64 v0.6.0

Choose a tag to compare

@github-actions github-actions released this 24 Jul 10:27
841a251

RustyN64 v0.6.0 "Cartridge" — Phase 5 complete

A commercial cartridge boots. Phase 5 delivers the cartridge + I/O
boundary: the PI bus with BSD domain timing, the SI joybus, the CIC
handshake, and all four save backends (SRAM, FlashRAM, EEPROM 4k/16k,
Controller Pak), plus TWO boot paths.

  • HLE boot (default, CI-able): copies the cart's real IPL3 to DMEM,
    seeds the post-IPL3 CPU/COP0/PI-DOM1 state and the CIC seed word, and
    jumps in. Copyright-clean; the seeds are cited constants (ledger C-32).
  • Real-PIF boot (faithful, off by default, local-only): runs the
    console's REAL IPL1/IPL2 from the PIF ROM at the reset vector, with
    the CIC identified from the IPL3 CRC-32 and the PIF-SM5 boot handshake
    (ROM lockout + IPL2-checksum acquire/verify + NMI-freeze) modelled
    behaviourally from PIF-NUS.md. The SM5 firmware is not run and the
    6105 running challenge is not modelled — neither is needed to boot
    (ledger C-33). Never CI-gated: the PIF ROM is copyrighted.

Every save-type representative in the commercial corpus (6102/6103/6105
CICs) boots through the real IPL1 -> IPL2 -> IPL3 chain to game execution
in RDRAM, with IPL2's computed checksum matching the hardware-documented
CIC value — which independently proves the CPU reproduces the checksum
bit-exactly over the real IPL3.

Phase 5's committable gate (to-dos/VERSION-PLAN.md §v0.6.0) is met by
oracle, not self-assessment:

  • n64-systemtest cart/PIF/SI improvement: 93 -> 90 failing suite-wide,
    Phase-1 categories still Failed: 0
    cargo test -p rustyn64-test-harness --release --test systemtest
    -- --ignored --nocapture
  • Save round-trips per backend + a commercial ROM boots and executes
    (both boot paths), local capstone over the gitignored corpus
    cargo test -p rustyn64-test-harness --release --test commercial_boot
    -- --ignored --nocapture

Honest scope (the escalation gate): reaching a rendered TITLE FRAME is a
downstream VI/RI/F3DEX runtime gap, tracked as ledger R-18, deferred to a
later phase and outside the Phase 5 cart boundary (ADR 0003). The cut
criterion is restated as met — boots + executes + save round-trips — not
faked as a title-screen pass. A green test run still does not mean a
subsystem works; docs/STATUS.md is authoritative.

The earlier phases remain met: Phase 4 (audio), Phase 3 (RDP conformance

  • golden frame), Phase 2 (RSP Failed: 0 + rdpq microcode), Phase 1 (CPU
    Failed: 0 + ares golden trace).

Full gates green: fmt, clippy (-D warnings), workspace test, rustdoc
(-D warnings), the thumbv7em-none-eabihf no_std cross-build, markdownlint,
the microcode-integrity check, and the full macOS/Windows/Ubuntu +
test-roms CI matrix.

Install: download the per-target archive, verify against SHA256SUMS, and
extract. Each archive carries both licences (MIT OR Apache-2.0), NOTICE,
README, and CHANGELOG.

Full changes: CHANGELOG.md [0.6.0]. Dual-licensed MIT OR Apache-2.0.

RustyN64 v0.5.0

Choose a tag to compare

@github-actions github-actions released this 24 Jul 05:55
d6a94c4

v0.5.0 "Resonance" — AI audio (Phase 4)

Sound, from the buffer the RSP audio microcode produces. Phase 4 is complete: the
Audio Interface is implemented, the real libdragon audio-mixer microcode runs its
DSP on the LLE RSP and produces verified PCM, and the frontend resamples that
stream to the host device. There is no per-game audio HLE (ADR 0002) — the same
LLE RSP that runs graphics microcode mixes the audio, so audio "falls out free".

Exit criteria (both met by oracle, committed runners)

  • The real RSP audio microcode produces PCM: the vendored libdragon mixer
    (rsp_mixer.S, Unlicense) runs its resampling / volume-filter / mixing DSP on the
    LLE RSP, driven through the rspq overlay path with a hand-built channel table +
    sample bank, and DMAs back a golden-pinned, deterministic mixed 16-bit stereo
    PCM buffer. No RSP gaps surfaced — the VU/DMA path runs the real audio microcode
    as-is. Reproduce: cargo test -p rustyn64-test-harness --test mixer_microcode.
  • A real ROM plays audio through the AI: a committed license-clean bare-metal ROM
    (audio_play.z64) CPU-feeds a PCM buffer and programs the AI, which DMAs it out;
    the emitted stream matches byte-for-byte, deterministically, with the AI
    interrupt firing. Reproduce: cargo test -p rustyn64-test-harness --test audio_play_rom.

The real-game-through-the-frontend-ring variant is honestly carried to Phase 5/6:
it needs cartridge boot and the playable shell (the libdragon mixertest ROM cannot
boot in Phase 4 — it needs DFS/wav64/display/joypad).

What landed

  • rustyn64-audio implements the AI: the register block at 0x0450_0000, the two-deep
    DMA FIFO, derived-timing emission off the canonical master_ticks (ADR 0006),
    interrupt-on-start (not on drain), the AI_LENGTH mirror on write-only registers,
    the region-derived DAC rate (VIDEO_CLOCK_NTSC/PAL, documented provenance), the
    delayed-carry hardware bug (reproduced, with a guard test), and observable underrun.
  • The libdragon mixer microcode vendored + assembled (rsp_mixer.bin), booting on the
    RSP and driven to produce PCM through command_exec.
  • The frontend drains the AI stream and resamples it to the host device rate
    (resample_stereo, carried-phase linear, frontend-side per ADR 0004); the cpal
    device is opened and fed by the emu thread.
  • Ledgered residuals R-16 (AI_STATUS COUNT/WC/BC best-effort, no oracle) and R-17
    (no DMA setup latency; defined-but-unpinned underrun decay).

Earlier phases remain met

Phase 3 (v0.4.0): RDP conformance bit-matches Angrylion across 164 vectors; a real
ROM renders a golden frame. Phase 2 (v0.3.0): RSP-category n64-systemtest Failed: 0;
rdpq microcode emits an RDP command list. Phase 1 (v0.2.0): CPU Failed: 0; a 0-diff
golden trace against ares. v0.4.1 was a documentation-only patch over v0.4.0.

Verification

Full gates green: fmt, clippy (-D warnings), workspace test, rustdoc (-D warnings),
the thumbv7em-none-eabihf no_std cross-build, markdownlint, the two-blob sha256
microcode-integrity check, and the full macOS/Windows/Ubuntu + test-roms CI matrix.

Install: download the per-target archive, verify against SHA256SUMS, and extract.
Each archive carries both licences (MIT OR Apache-2.0), NOTICE, README, and CHANGELOG.

RustyN64 v0.4.1

Choose a tag to compare

@github-actions github-actions released this 24 Jul 01:54
31b7185

v0.4.1 — Phase-3-close documentation reconciliation

A documentation-only patch over v0.4.0 "Rasteriser". No code changed; the emulator
behaves byte-identically to v0.4.0. This release exists to reconcile the planning and
status documents with the reality that Phase 3 (the LLE RDP + VI) shipped in v0.4.0 —
the phase-close documentation pass had been skipped, so several docs still described a
pre-v0.4.0 world.

What changed

  • docs/STATUS.md: the RDP is no longer described as a stub (the chip-crate skeleton
    line, the DPC-registers row, and the "RDP LLE" roadmap row now record the working
    rasteriser); the release-infrastructure lines record v0.1.0-v0.4.1 as tagged and
    released instead of "no tag has been cut".
  • README.md: the Compatibility & Accuracy table adds the two passing Phase 3 gates
    (RDP conformance vs Angrylion across 164 vectors, and the real-ROM golden frame) and
    reclassifies real-game visual goldens to Phase 7; the Performance rationale, the
    "Current Release" next-rung (now v0.5.0), and the Roadmap phase list (Phases 0-3
    complete, Phase 4 next) are corrected; version badge and bibtex bumped.
  • to-dos/ROADMAP.md: the phase matrix marks Phase 2 (v0.3.0) and Phase 3 (v0.4.0)
    COMPLETE, the Status block advances to Phase 4, and the v1.0.0 milestone records the
    tags cut so far. v0.4.1 is noted as a documentation-only patch (no new scope).
  • to-dos/VERSION-PLAN.md, docs/rdp.md, docs/architecture.md: the RDP / rasteriser /
    RDP tick methods are marked implemented rather than stubbed.
  • Phase 0-3 plan files: completion banners added; Phase 2's nine met exit criteria and
    the Sprint 1/Sprint 4 checklists checked off (the SU/VU dual-issue timing box stays an
    honest, ledgered deferral); Phase 0's golden-trace and release-tag boxes marked
    resolved and the superseded ADR 0001 3:2-clock criterion annotated; Phase 3's exit
    criteria and Sprint 3 review checklist reconciled with their R-9/R-10/R-11 residuals.

Known limitations (unchanged from v0.4.0)

  • AI audio (Phase 4) and cartridge boot / saves (Phase 5) are not implemented; the
    rustyn64 binary opens a shell and presents a test pattern. A green cargo test does
    not mean a subsystem works — see docs/STATUS.md.
  • RDP residuals carried to Phase 7: attribute interpolation (R-9), exotic combiner
    inputs (R-10), and the coverage / AA / alpha-compare / dither paths (R-11).

Verification

No behavioural gates changed. All accuracy oracles remain as of v0.4.0: n64-systemtest
Failed: 0 on CPU/COP0/TLB/COP1 and RSP; the CPU golden-log 0-diff against ares; the RDP
conformance suite bit-matching Angrylion across 164 committed vectors; and the real-ROM
golden frame (T-33-006). CI green across the full release matrix (ubuntu/macOS/windows +
test-roms), rustdoc (-D warnings), the no_std cross-build, the commercial-ROM guard, and
markdownlint.

Install: download the per-target archive, verify against SHA256SUMS, and extract. Each
archive carries both licences (MIT OR Apache-2.0), NOTICE, README, and CHANGELOG.

v0.4.0 "Rasteriser" — the first picture

Choose a tag to compare

@doublegate doublegate released this 24 Jul 01:18
6176d23

The first RustyN64 release that produces a picture. The low-level Reality Display Processor renders through the per-pixel pipeline and the Video Interface scans it out. Phase 3 of the cycle-accurate N64 emulator is complete.

Cut criteria — both met (each an oracle result with a committed runner)

  • The conformance suite bit-matches Angrylion164 committed .rvec vectors replay byte-for-byte against the Angrylion-Plus software RDP: FILL rectangles, the asymmetric scissor clip, FILL / shaded / textured triangles, the colour combiner, the divide-free blender, dither, alpha-compare, coverage write-back, the combiner primitive mux, and copy-mode texture rectangles.
    cargo test -p rustyn64-test-harness --test rdp_conformance
  • A real ROM renders a stable golden frame — a license-clean homebrew ROM boots on the cycle-accurate VR4300, programs the VI, CPU-fills a framebuffer, and the VI scans it out to a verified 32×24 frame, bit-identical across two boots (ADR 0004).
    cargo test -p rustyn64-test-harness --test real_rom_frame

Highlights

  • The LLE RDP rasteriser (triangle edge-walk, sub-pixel coverage, the (A−B)·C+D combiner, the divide-free blender, TMEM loads + texel decoders, copy-mode texture rectangles, textured triangles) and VI scan-out — the first-picture loop RDP → framebuffer → VI → frontend is closed.
  • A seeded-fuzz conformance harness that found and fixed two real accuracy bugs — the FILL rectangle inclusive lower-right edge (R-3) and the asymmetric scissor clip (R-15) — and textured triangles resolved (R-13: the bi_lerp0 YUV-convert vector bug plus the s.5 texel-coordinate scale).

Not yet (later phases)

Perspective-correct and bilinear texturing, an RDP-driven real-ROM frame, AI audio (v0.5.0), and cartridge boot + saves (v0.6.0). docs/STATUS.md is authoritative — a green test run does not mean a subsystem works.

Build

Pure Rust, toolchain pinned to 1.96.0, edition 2024. cargo build --release. Linux frontend needs libxkbcommon wayland alsa-lib systemd-libs. Dual-licensed MIT OR Apache-2.0.

Full changelog: CHANGELOG.md.

RustyN64 v0.3.0

Choose a tag to compare

@github-actions github-actions released this 22 Jul 07:05
1ee000f

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 microcode

Both 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 at 0xFFC continues at 0x000 rather than faulting.
  • Misaligned data accesses are correct, not faults. A LW at 0x001 returns 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 the rspq_start boot 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 rdpq overlay 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_END is advanced through the DPC seam.

The seam itself: the RSP's COP0 registers c8c15 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/vs before 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, the sp_memory out-of-bounds cases).
  • c16c31 do not exist. The COP0 rd field 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 green cargo test does not mean a subsystem works — docs/STATUS.md is authoritative.
  • 93 n64-systemtest assertions fail, all cart/PIF (Phase 5) and the RDP rasteriser (Phase 3).
  • An MFC0 of DP state the RDP mutated on its own is not yet reflected in the RSP's c8c15
    shadow — a Phase 3 concern, when the RSP polls DP_STATUS on the buffer-switch path.
  • The test-roms / commercial-roms / per-crate std feature flags are internal placeholders
    that still gate zero code
    — no cfg(feature = …) exists for them yet, so --features test-roms
    runs the same tests as a bare cargo test. They reserve the wiring for later phases, not a public
    toggle.
  • Several timing constants remain unmeasuredM (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.

RustyN64 v0.2.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 07:33
19563f6

RustyN64 v0.2.0 "Interpreter"

The VR4300 works, and two independent oracles say so. Phase 1 is complete: the CPU executes
MIPS III — including the TLB, COP0, the FPU, the caches and the documented errata — and both of the
phase's exit criteria are met.

Unlike v0.1.0, which was tagged as a deliberately honest skeleton, this tag describes a subsystem
that genuinely runs. 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.

Accuracy statement

Criterion Result
n64-systemtest, CPU/COP0/TLB/COP1 categories Failed: 0 — 0 failing of 917 tests started
CPU golden-log 0-diff vs ares held — retired-instruction stream identical from the ELF entry
cargo test -p rustyn64-test-harness --release --test systemtest -- --ignored
cargo test -p rustyn64-test-harness --release --test golden_log -- --ignored

413 assertions still fail suite-wide. Every one is RSP/RCP, which is v0.3.0's criterion
(to-dos/VERSION-PLAN.md §v0.3.0), not this one. That distinction is deliberate and is enforced by
the runner, which matches on the categories to exclude so a new CPU-side category added upstream
lands inside the gate rather than being silently skipped.

CPU

Timebase and microarchitecture. One canonical 187.5 MHz master clock is the only incremented
counter; every other cycle position is a derived accessor, pinned by a residue-invariant test that
fails if any position becomes independent (ADR 0006, superseding ADR 0001). The CPU is a
cycle-accurate five-stage pipeline (IC/RF/EX/DC/WB) of four inter-stage latches advanced in
reverse stage order, which is what makes the latching implicit (ADR 0007). in_delay_slot rides
in the latch rather than in global state.

Instruction set. MIPS III including the 64-bit D* forms, LL/SC/LLD/SCD, the unaligned
LWL/LWR/LDL/LDR family, the trap family, and the branch-likely forms with delay-slot
nullification.

COP0, TLB and exceptions. The register file with correct 64-bit widths, Count/Compare timer
interrupts, the Status/Cause exception path, 32 dual TLB entries with variable page sizes and
ASID matching, and the refill/invalid/modified vectors. The micro-ITLB sits in front of the JTLB.

The primary caches. A 16 KiB instruction cache (32-byte lines) and an 8 KiB write-back data
cache (16-byte lines), both direct-mapped. Instruction fetch and every cached load and store run
through them; a cached store is a write-allocate that leaves the line dirty until an eviction or a
CACHE operation forces it out. All thirteen CACHE operations act.

COP1 runs on a soft-float core, not on Rust's f32/f64 operators. That is not gratuitous:
the native operators discard the exact pre-rounding result, so inexact/underflow cannot be
reported and FCSR.RM cannot be honoured. The module is verified bit-for-bit against those same
operators in round-to-nearest over ~100k cases — they are the independent oracle, which is why it
implements IEEE behaviour and leaves the VR4300's refusal to produce subnormals as a separate
layer.

Privilege and addressing. The segment map is a function of (address, mode, width), never the
address alone. KSEG0 does not exist in User mode, and it is the address-space check — not the TLB
— that stops a user program reaching it: an out-of-range address raises AdEL before the TLB is
consulted. XKPHYS, Status.RE reverse-endian, and the 64-bit-operation reservation in 32-bit
User/Supervisor mode are all implemented.

Hardware behaviours worth naming

Several of these look like bugs on every reading and are not. Each is recorded in
docs/accuracy-ledger.md with its evidence.

  • NaN classification is INVERTED from IEEE-754:2008 — significand MSB set means signalling,
    so f32::NAN raises Invalid. Legacy MIPS convention (C-12).
  • The VR4300 has no subnormal datapath. A subnormal operand or result raises the unmaskable
    unimplemented-operation cause, not a number — except that compares are exempt (C-13).
  • Under FR = 0, fs and ft resolve differently: the low bit of fs is ignored, the low bit
    of ft is not. The manual declines to define odd registers here, so the ROM's measured table is
    the oracle (C-21).
  • A PageMask pair stores only its higher bit, and a TLB tag is masked by PageMask rather
    than divided by the page size — a distinction invisible for all six legal page sizes.
  • Tininess is detected before rounding, so a directed rounding mode that lifts a tiny result back
    into the normal range still raises underflow.
  • An address in 32-bit mode must be the sign extension of its low word. 0x0000_0000_8000_1000
    is an address error, not shorthand for KSEG0.

Verification infrastructure

The n64-systemtest runner is committed (crates/rustyn64-test-harness/tests/systemtest.rs).
Previously each session rebuilt a throwaway, which made the published number unverifiable from the
repository. It witnesses execution before trusting a zero — an empty run produces zero failures just
as convincingly as a passing one.

The golden-log differ is wired to a committed reference trace (tests/golden/n64-systemtest.log)
captured from ares, carrying a provenance header naming the reference build, ROM hash and start PC.
The claim is stated narrowly and deliberately: given identical initial state, RustyN64 retires the
same instructions in the same order as the reference
. This is the tandem-verification shape used
by RISC-V co-simulation harnesses — align two models at a boundary and treat only deltas as the
claim. It says nothing about boot or timing (C-26).

EMUX (xdetect/xlog/xioctl) is implemented behind Bus::emux_enabled, off by default
because hardware has none. The test harness opts in and gets a console needing no PI/SI/ISViewer
emulation (~9× faster) plus xioctl(EXIT) as a definite end-of-run signal (C-27).

Known limitations

  • The RSP, RDP and AI are LLE-shaped stubs. No chip other than the CPU executes anything. A green
    cargo test does not mean a subsystem works — docs/STATUS.md is authoritative.
  • 413 n64-systemtest assertions fail, all RSP/RCP. Phase 2's criterion.
  • Cache ↔ DMA coherency is not modelled. Cart/RSP DMA writes land in RDRAM behind the cache.
  • The caches are indexed by physical address where hardware indexes virtually — a divergence in
    both directions, bounded to the tested KSEG0 scope (D-6).
  • LWL/LWR/SWL/SWR under Status.RE are implemented, but DMFC0/DMTC0 are deliberately
    excluded from the 64-bit reservation pending evidence.
  • Several timing constants remain unmeasuredM (memory access time), the exception-epilogue
    cost, CP0I, RDRAM bank-state costs. They are listed in the accuracy ledger as unmeasured, never
    fitted; no ROM-tuned constant has been admitted.
  • 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. 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.

RustyN64 v0.1.0

Choose a tag to compare

@github-actions github-actions released this 20 Jul 04:27

RustyN64 v0.1.0 "Foundation"

The architectural skeleton. The workspace compiles, CI is green across Linux, macOS, and
Windows, the documentation site publishes, and the accuracy oracle is acquired and
licence-classified.

No chip executes an instruction yet. This release is a foundation, not an emulator. It is
tagged so that foundation becomes a fixed, citable point rather than an ever-growing
[Unreleased] section — the same reason RustySNES cut its own retroactive v0.1.0.

If you are looking for something to play, this is not it. Come back at
v0.7.0 "Shell", which is the first rung that is playable.


What this release actually is

RustyN64 is a cycle-accurate Nintendo 64 emulator in pure Rust, third in the line after
RustyNES and
RustySNES. The accuracy bar is ares / CEN64 /
Gopher64 / ParaLLEl.

What is real in v0.1.0 is the architecture — the parts that are expensive to change later and
cheap to get right now:

Subsystem State
Fractional master-clock scheduler Implemented
Bus with single ownership of mutable state Implemented
One-directional crate graph Implemented
ROM format detection and normalisation Implemented
VR4300 CPU Stub
RSP (scalar + vector) Stub
RDP + VI Stub
AI audio Stub
PI/SI DMA, PIF/CIC boot, saves Stub

Stubs are TODO(T-XXX-NN) comments inside no-op bodies that compile and return — deliberately
not todo!() panics. That keeps the workspace green and the architecture exercisable, but it
also means a passing test suite proves nothing about emulation. docs/STATUS.md states this in
several places, and its accuracy table carries an explicit "oracle available?" column separate
from status, precisely so the two are never conflated.


The timebase

The load-bearing decision, and the one most worth explaining.

One master tick is one VR4300 cycle (MASTER_HZ = 93_750_000). The RCP
(RCP_HZ = 62_500_000) advances on a 3:2 fractional accumulator — two RCP ticks per three
master ticks — derived from the R4300i DivMode 1.5:1 ratio off the 14.31818 MHz NTSC
colourburst:

pub const RCP_NUM: u32 = 2;
pub const RCP_DEN: u32 = 3;

self.rcp_accum += RCP_NUM;
while self.rcp_accum >= RCP_DEN {
    self.rcp_accum -= RCP_DEN;
    self.step_rcp();
}

"Lockstep" here means not catch-up: an RCP event — a DP-done IRQ, an SP halt, an AI buffer
drain — is visible to the very next CPU step. That is what makes mid-frame coprocessor effects
work without per-quirk patches.

Integer lockstep was evaluated and rejected, because the surrounding clocks are not integer
multiples of either core clock: the AI sample rate is video_clock / (DACRATE + 1), an arbitrary
divisor, and the VI counters and PAL field timing do not divide evenly either. ADR 0001 records
the decision; ADR 0005 records the sub-cycle bus-timing refactor that remains deferred beyond
v1.0.

Pinned by fractional_divisor_holds_3_to_2 and reset_preserves_phase.

Ownership and the crate graph

rustyn64-core::Bus owns everything mutable: 8 MiB of RDRAM (4 MiB base plus Expansion Pak), the
RSP, RDP, AI, cart, controllers, and the MI interrupt lines. The CPU borrows &mut Bus; each
chip sees only the narrow trait it needs (CpuBus, RdramBus, VideoBus, RspBus,
AudioBus). Chips are stepped with a core::mem::take split-borrow — move the chip out, tick it
against &mut self, move it back — so there is no allocation and no Rc/RefCell in the hot
path. This is the TetaNES postmortem's answer to the "CPU holds the coprocessor, but the
coprocessor needs the CPU bus" borrow cycle.

The crate graph is strictly one-directional with exactly one permitted chip-to-chip edge:
rustyn64-rdprustyn64-cart, solely to borrow the RdramBus trait. Any other cross-chip
dependency breaks the fuzz-in-isolation invariant. Downstream consumers depend on
rustyn64-core, which re-exports the chip types, never on a chip crate directly.

Determinism

Same seed, ROM, and input sequence must yield a bit-identical framebuffer and audio (ADR 0004).
Power-on CPU/RCP phase alignment — genuinely indeterminate on real hardware — is modelled as a
seeded SplitMix64 parameter rather than live entropy, so it is reproducible and
save-state-able. Reset preserves alignment. The core reads no wall-clock time, no OS entropy, and
no thread-scheduling order.

Honestly: this contract is specified but not yet exercised. There is no determinism
regression test, because there is nothing to run twice — no CPU, no frames. frame_hash exists
but nothing calls it against a golden. ADR 0004's Consequences section says so, and the test
lands as T-11-007 in Phase 1, where it first becomes meaningful.


Infrastructure

CI

Eight jobs, green on Linux, macOS, and Windows: fmt, clippy -D warnings (under pedantic +
nursery), test (42 tests / 21 suites), rustdoc -D warnings, the no_std cross-build to
thumbv7em-none-eabihf, the test-roms battery, and two repository guards.

The matrix is split light/full: ordinary feature PRs get the fast gates on ubuntu only, while
push-to-main, the merge queue, release/* PRs, dispatch, and a weekly cron get the full
three-platform matrix plus the ROM battery.

One non-obvious fix worth recording: every Linux job installs libasound2-dev, libudev-dev,
libxkbcommon-dev, and libwayland-dev first. cpal pulls alsa-sys and gilrs pulls
libudev-sys, whose build scripts shell out to pkg-config, and those headers are not on
the GitHub runner images. Because cargo builds the whole workspace graph, this affected every
Linux job, not just the frontend ones — the runner log confirms all four were genuinely absent.

Documentation site

rustdoc publishes to https://doublegate.github.io/RustyN64/ on every push to main. Docs are
served under /api/, with / reserved for the wasm demo that lands in Phase 6 and currently
redirecting.

Release automation

This release is the first exercise of the release workflow. It builds rustyn64 for three
targets, packages each with both licences, NOTICE, README, and CHANGELOG under a versioned
directory, generates SHA256SUMS, and publishes. The tag is verified against the workspace
version before anything is published, so a mistyped tag fails rather than shipping mislabelled
artefacts.


The accuracy oracle, acquired ahead of the emulator

Staging the oracle before the emulator is deliberate: accuracy work is graded from the first
commit rather than retrofitted.

Hardware reference

n64brew_wiki/ is a gitignored offline mirror of the N64brew Wiki — 324 pages and 96 media files
across parallel HTML, Markdown, and wikitext trees, rebuilt by
scripts/mirror_n64brew_wiki.py. --verify reports 1,094 local references checked with 0
unresolved and 0 images still pointing off-site; --refresh re-fetches only changed revisions.
CC BY-SA 4.0.

Reference emulators

Eleven study clones in the gitignored ref-proj/, each with its licence verified by reading the
actual file and a recorded vendor-ok or study-only decision: ares, cen64, gopher64, simple64,
parallel-rdp, parallel-rsp, angrylion-rdp-plus, n64-systemtest, n64-tests, libdragon, and
PeterLemon/N64.

Two traps are called out explicitly in ref-proj/README.md, because both are easy to misread:

  • angrylion-rdp-plus ships no LICENSE file, which reads as unlicensed-and-free. It is
    not — it carries MAME License.txt, and its CREDITS.txt states "The code comes under MAME
    license." The old MAME licence forbids commercial use outright, making it stricter than the
    GPL clones and incompatible with this repository in both directions. It is also the reference
    software rasteriser the RDP will be graded against, so it is precisely the tree someone will
    want to open while chasing a bit-exactness failure. Compare outputs, never source.
  • n64-tests has no licence at all. Absence of a licence is not a public-domain grant; it
    means no rights are granted. Run the ROMs, do not vendor the sources.

Test-ROM corpora

Two tiers. The committed tier is permissive only, and every committed ROM ships its upstream
LICENSE — enforced by the guard script, not merely documented.

Corpus Licence Tier Staged
n64-systemtest MIT committed 1 ROM, 2.7 MB — built from source
krom (PeterLemon) Unlicense external (size) 196 ROMs, 182 MB
dillon-n64-tests none external (no grant) 26 ROMs, 38 MB
240p Test Suite GPL-2.0-or-later external (copyleft) 1 ROM, 12 MB — built from source
commercial copyrighted external (never) 66 ROMs, 1.5 GB

Both committed and 240p ROMs were built from source, since neither publishes a usable
prebuilt binary. The 240p build needed a containerised libdragon preview branch plus Tiny3D;
the recipe is in tests/roms/README.md, including the non-obvious requirement that dependency
install and build happen in one container because /opt/libdragon does not survive --rm.

The commercial corpus is organised by save type — the axis ADR 0003 says the cart is
parameterised by — with save types resolved by MD5 against the mupen64plus catalogue rather than
guessed. It covers every save backend, both custom-microcode families (Factor 5 and Boss Game
Studios, the canonical HLE-breakers), the Expansion Pak titles, the VRU and Transfer Pak, and
every N64 million-seller available locally.

Commercial ROMs cannot enter the repository

Three independent guards, each covering a bypass the others miss:

  1. .gitignore excludes *.z64/*.n64/*.v64/*.ndd everywhere, re-includes only the
    committed tier, then hard-excludes tests/roms/external/ last, so no negation can leak a
    dump back in.
  2. ...
Read more