Skip to content

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. scripts/check_no_roms.sh (pre-commit) checks the staged list, closing the gap that
    git add -f bypasses .gitignore silently, plus a size ceiling for a ROM renamed to hide its
    extension.
  3. the no-commercial-roms CI job re-runs that scan server-side over the whole tracked tree
    — unskippable, unlike a local hook.

Verified against a real bypass attempt rather than assumed: git add -f on an 8 MB commercial
ROM does stage past .gitignore, and the hook then rejects it.


Planning

to-dos/ carries the full spine: a ROADMAP of nine phases, per-phase overviews with real exit
criteria, and ten sprint files minting 49 tickets (T-PS-NNN). VERSION-PLAN.md sequences
those phases into a named release ladder, and LOCKSTEP-CHECKLIST.md is the pass run when
scoping each release to track the two sibling projects.

Next rung: v0.2.0 "Interpreter" — the VR4300 to n64-systemtest Failed: 0. That is the
first release where a test ROM actually runs, because run_until_complete currently always
returns Timeout.


Known limitations

Recorded rather than hidden:

  • No emulation. Every chip tick is a no-op. The rustyn64 binary opens a shell and
    presents a test pattern.
  • The test-roms and commercial-roms features gate zero code. No cfg(feature = ...)
    exists for either, so --features test-roms runs exactly the same tests as a bare
    cargo test --workspace. Same for the per-crate std features: every chip crate is
    unconditionally #![no_std], so --no-default-features is currently a no-op.
  • CI lints one feature configuration. clippy runs once with no feature permutations, so
    feature-gated code is unlinted.
  • wasm compiles but does not run. The crate builds for wasm32-unknown-unknown, but there is
    no wasm-bindgen dependency, no #[wasm_bindgen(start)] entry point, and no index.html — so
    trunk build cannot produce a demo. Phase 6.
  • pages.yml publishes docs only. The wasm demo slot at / is a redirect until Phase 6.
  • The benches are placeholders — 7-line files with no criterion dependency.
  • The determinism contract is unexercised, as described above.
  • No accuracy gate reports a number. The ROMs exist for most of them; nothing loads or scores
    a ROM yet.

Verification

cargo fmt --all --check · cargo clippy --workspace --all-targets -- -D warnings ·
cargo test --workspace (42 passed) · cargo test --workspace --features test-roms ·
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps ·
cargo build -p rustyn64-core --target thumbv7em-none-eabihf --no-default-features ·
scripts/check_no_roms.sh · actionlint · markdownlint

All green on Linux, macOS, and Windows.

Accuracy suites: none active. No gate can report a number until Phase 1 lands a CPU.

Installation

Download the archive for your platform below, verify it against SHA256SUMS, and extract. Each
archive contains the rustyn64 binary plus LICENSE-MIT, LICENSE-APACHE, NOTICE,
README.md, and CHANGELOG.md.

sha256sum -c SHA256SUMS --ignore-missing
tar xf rustyn64-v0.1.0-x86_64-unknown-linux-gnu.tar.gz
./rustyn64-v0.1.0-x86_64-unknown-linux-gnu/rustyn64 --version

On Linux the binary needs the wgpu/winit/cpal system libraries:

# Arch / CachyOS
sudo pacman -S --needed libxkbcommon wayland alsa-lib systemd-libs
# Debian / Ubuntu
sudo apt-get install -y libxkbcommon-dev libwayland-dev libxkbcommon-x11-dev libasound2-dev libudev-dev

Licence

Dual-licensed MIT OR Apache-2.0. Test ROMs under tests/roms/ are individually licensed and
tiered; no commercial ROMs are included, and they never will be.