Build and tooling¶
References: Phase 1 toolchain decisions; docs/architecture.md §Workspace shape.
Toolchain¶
- Rust edition: 2024.
- MSRV (minimum supported Rust version): 1.96.0. Pinned via
rust-toolchain.toml. (Bumped from 1.86 in v1.3.0 "Bedrock" to unblock the edition-2024 + egui 0.34.3 / wgpu 29 / rfd 0.17.2 dependency tier.) - Channel: stable. Nightly only for fuzz tests (
cargo +nightly fuzz). - Targets supported:
x86_64-unknown-linux-gnu,aarch64-apple-darwin,x86_64-pc-windows-msvc. Tier 2:aarch64-unknown-linux-gnu. Cross-compile targets declared inrust-toolchain.toml(auto-installed):thumbv7em-none-eabihf(theno_stdchip-stack gate) andwasm32-unknown-unknown(browser). Android arm64/arm/x86_64 viacargo ndk(seedocs/android.md). Thex86_64-apple-darwinrelease target was retired (ADR 0009).
Workspace layout¶
Cargo.toml # workspace manifest
rust-toolchain.toml # pin 1.96.0 stable + thumbv7em + wasm32 targets
crates/
├── rustynes-core/ # public re-exports + Nes facade + scheduler + save state
├── rustynes-cpu/ # 2A03 CPU
├── rustynes-ppu/ # 2C02 PPU
├── rustynes-apu/ # 2A03 APU
├── rustynes-mappers/ # cartridge parsing + mapper trait + implementations
├── rustynes-netplay/ # GGPO-style rollback (UDP native + WebRTC browser)
├── rustynes-cheevos/ # RetroAchievements FFI over vendored rcheevos
├── rustynes-ra/ # RetroAchievements session state (Send; native-only)
├── rustynes-script/ # Lua engine (mlua native / piccolo wasm)
├── rustynes-gfx-shaders/ # shared WGSL presentation shaders (desktop + Android)
├── rustynes-hdpack/ # HD-pack loader + compositor + HD audio
├── rustynes-mobile/ # shared mobile UniFFI bridge (Android + future iOS)
├── rustynes-android/ # cfg-gated Android JNI / wgpu-SurfaceView / AAudio glue
├── rustynes-monetization/ # mobile-only UniFFI ad-policy crate (never touches the core)
├── rustynes-frontend/ # rustynes binary (winit + wgpu + cpal + egui)
└── rustynes-test-harness/ # test ROM runner, golden log compare
tests/ # workspace-level integration tests
benches/ # criterion benches
Common commands¶
| Action | Command |
|---|---|
| Build everything | cargo build --workspace |
| Build release frontend | cargo build --release -p rustynes-frontend |
| Run frontend | cargo run --release -p rustynes-frontend -- path/to/rom.nes |
| Unit tests | cargo test --workspace |
| Test ROM suite | cargo test --workspace --features test-roms |
| Lint | cargo clippy --workspace --all-targets -- -D warnings |
| Format check | cargo fmt --all --check |
| Format apply | cargo fmt --all |
| Generate docs | cargo doc --workspace --no-deps --open |
| Bench | cargo bench --workspace |
| Fuzz (one harness) | cargo +nightly fuzz run cartridge_parse |
Linting policy¶
Cargo.toml workspace-level lints:
[workspace.lints.rust]
unsafe_code = "warn" # only in CPU/PPU hot paths after benchmarking
missing_docs = "warn" # public API documented
[workspace.lints.clippy]
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
# Allow these (signal-to-noise):
module_name_repetitions = "allow"
similar_names = "allow"
must_use_candidate = "allow"
CI enforces clippy --all-targets -- -D warnings. Local development: developers can #[allow] per-call but should justify in a code comment.
Formatting¶
rustfmt with default settings + imports_granularity = "Crate". Configured in rustfmt.toml.
Profiles¶
[profile.dev]
opt-level = 1 # debug builds usable for emulator dev (1 fps else)
overflow-checks = true
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
panic = "abort"
overflow-checks = false # disabled for speed; CI runs a separate matrix with checks on
[profile.bench]
inherits = "release"
debug = true # for perf/flamegraph
Dependencies¶
See Cargo.toml files per crate. Key choices:
bitflagsfor CPU status flags + PPU control bits.thiserror2.x for typed error enums. Derivescore::error::Error(Rust 1.81+) — works inno_std. Workspace pin:default-features = falseso the chip stack stays no_std-clean;rustynes-frontendandrustynes-core'sstdfeature opt back in tothiserror/std.libm0.2 (no_std soft-float math) — pulled byrustynes-apuforexpfwhen therustynes-apu/stdfeature is off.serde+bincode(optional, behindserdefeature) for save states.lz4_flex0.13 (save-state / rewind delta compression).winit+wgpu29 +naga29 +cpal+egui0.34.3 +rfd0.17.2 +gilrsfor the frontend.mlua0.11 (vendored Lua 5.4, native scripting) /piccolo0.3.3 (pure-Rust wasm Lua backend, ADR 0012) — mutually exclusive.zip8,sha1/md-50.11 (movie / interchange hashing).- Android:
jni0.22,pollster0.4,android_logger0.15,uniffi0.31 (therustynes-mobile/rustynes-android/rustynes-monetizationcrates). criterion+proptest+instaas dev-deps.
No runtime async (tokio/async-std) in the emulator core — it is synchronous; the cpal callback runs on its own thread without async. (Netplay signaling uses a small blocking tungstenite worker in the frontend, outside the deterministic core.)
no_std + alloc migration (complete)¶
The chip stack — rustynes-cpu, rustynes-ppu, rustynes-apu, rustynes-mappers, rustynes-core —
is #![no_std] + extern crate alloc; (Track C5, post-v0.9.0). rustynes-frontend
stays std because it depends on wgpu / winit / cpal / egui.
CI gates this via a dedicated no_std job that cross-compiles
rustynes-core to a bare-metal embedded target:
rustup target add thumbv7em-none-eabihf
cargo build -p rustynes-core --target thumbv7em-none-eabihf --no-default-features
rustynes-core's default = ["std"] cargo feature propagates std to its
host-only deps (lz4_flex, sha2, thiserror) and to rustynes-apu/std
(which gates f32::exp vs libm::expf in the mixer's filter-coefficient
init). Desktop builds are unchanged.
To check your changes don't regress the migration, grep for use std::
in the chip crates — only the test / bench / example harnesses should hit:
Run the CI gate locally before committing chip-crate changes:
Files in .github/¶
actions/rust-setup/action.yml— shared composite action (toolchain + Linux wgpu/winit/cpal deps + cargo cache) used by all three workflows, so the setup steps + the apt package list live in exactly one place.workflows/ci.yml— lint (fmt + clippy + rustdoc on the pinned 1.96 toolchain, so the gate matches local) + the cross-platform test matrix + test-roms + no_std + wasm32 clippy + the frame-time bench gate. Runs the feature-combo clippy gates (scripting,hd-pack,retroachievements, and both wasm flavours) — never--all-features(thescripting/mlua andscript-wasm/piccolo backends are mutually exclusive). Skips entirely on documentation-only pushes (paths-ignore), and cancels superseded PR runs (concurrency).workflows/android.yml—cargo ndkcross-compilesrustynes-mobile+rustynes-android+rustynes-monetization, generates the UniFFI Kotlin bindings, and runs the Gradle build (AGP 9.2.1 / Gradle 9.4.1 / compileSdk 37 / targetSdk 36 / minSdk 26). Seedocs/android.md.workflows/release.yml— tag-triggered (v*), builds the per-platform release binaries and attaches them to the GitHub Release (it never writes the release body — see the anti-clobber note in the workflow).workflows/web.yml— deploys the wasm32 frontend to GitHub Pages; build + size-budget gate run on PRs (paths-filtered to build inputs), deploy only onmain.ISSUE_TEMPLATE/bug_report.mdandfeature_request.md— issue templates.PULL_REQUEST_TEMPLATE.md— PR checklist.
Local dev environment¶
- Linux (Wayland or X11): no system deps beyond a working Rust toolchain.
wgpufinds Vulkan vialibvulkan(any vendor). - macOS: Xcode command-line tools.
- Windows: MSVC build tools + Windows 10+ SDK. Vulkan SDK not required (wgpu uses D3D12 by default).
Open questions¶
- MSRV bumps. Plan: bump only when a dependency requires it; document in
CHANGELOG.md. -Cpanic=abortvs unwind. Abort gives smaller binaries and slightly faster code. We pay it; emulator core does not need to recover from panics.unsafeaudit. Only the band-limited audio buffer might needunsafe(ring-buffer atomics). Audit at every PR; require justification comment.