rustynes_core/lib.rs
1//! Cycle-accurate NES emulator core.
2//!
3//! This crate is the public entry point for embedders (frontend, test harness,
4//! future ports). It owns the scheduler, the bus, save-state serialization,
5//! the rewind ring, and the `Nes` facade. Per-chip implementations live in
6//! `rustynes-cpu`, `rustynes-ppu`, `rustynes-apu`, and `rustynes-mappers`, and are re-exported
7//! from this crate so downstream consumers depend on `rustynes-core` only.
8//!
9//! See `docs/architecture.md` and `docs/scheduler.md` for the design.
10
11#![no_std]
12// The chip stack carries no `unsafe`; `forbid` makes that a compile-time
13// guarantee rather than an observation (core audit section 2.1). The FFI
14// and platform `unsafe` lives in the frontend, cheevos and mobile crates.
15#![forbid(unsafe_code)]
16#![warn(missing_docs)]
17
18extern crate alloc;
19
20#[cfg(test)]
21extern crate std;
22
23pub use rustynes_apu;
24pub use rustynes_cpu;
25pub use rustynes_mappers;
26pub use rustynes_ppu;
27
28mod bus;
29// v2.0 R1c-1 diagnostic — per-instruction (PC, cpu_cycle) trace ring (gated).
30#[cfg(feature = "cpu-instr-cycle-trace")]
31pub use bus::instr_trace;
32pub mod bk2_interop;
33mod bus_snapshot;
34mod controller;
35#[cfg(feature = "cpu-boot-trace")]
36pub mod cpu_boot_trace;
37pub mod debug;
38pub mod genie;
39pub mod input_device;
40#[cfg(feature = "irq-timing-trace")]
41pub mod irq_trace;
42// v1.7.0 "Forge" G4 — legacy NES TAS movie importers (.fcm / .fmv / .vmv;
43// .mc2 is PC Engine and rejected). `no_std`-clean byte parsers, mirroring the
44// `.fm2`/`.bk2` interop design; reuse the canonical power-on alignment.
45pub mod legacy_movie;
46// v2.9.8 — the emulation options a movie or a netplay session must agree on.
47mod hardware_options;
48mod movie;
49pub mod movie_interop;
50mod nes;
51mod rewind;
52pub mod save_state;
53pub mod scheduler;
54pub mod vs_db;
55// v2.0.0 beta.5 (Workstream C) — the Vs. DualSystem dual-core wrapper: two
56// complete Nes instances + the cabinet's $4016-bit-1 comms protocol, the
57// shared 2 KiB WRAM swap, and the 5-CPU-cycle soft-lockstep. See
58// `docs/audit/vs-dualsystem-design-2026-06-11.md`.
59pub mod vs_dualsystem;
60// v1.7.0 "Forge" Workstream D2 — the Zwinder-class compressed, density-tiered
61// state manager (XOR-delta + LZ4 over the v1.6.0 uncompressed greenzone, with
62// reserved anchors), scaling the TAStudio greenzone to feature-length TASes.
63// Determinism-neutral: lossless round-trip, no timebase change. See
64// `docs/rewind.md` §Zwinder.
65pub mod zwinder;
66
67/// The APU sample rate `Nes::from_rom` and friends build with when no explicit
68/// rate is given (`44_100` Hz).
69///
70/// Re-exported so a downstream consumer that must *declare* the rate it produces
71/// — the libretro core does, in `retro_get_system_av_info` — can state the real
72/// value instead of transcribing a literal that could silently fall out of step
73/// with the samples actually emitted.
74pub use bus::DEFAULT_SAMPLE_RATE;
75pub use bus::SystemBus;
76#[cfg(feature = "debug-hooks")]
77pub use bus::{AccessRec, EventBpKind, EventBreakHit, EventKind, EventRec, InterruptRec};
78pub use bus_snapshot::{EXPANSION_DEVICE_MAX_LEN, SAVE_STATE_DEVICE_HEADROOM};
79pub use controller::{Buttons, Controller};
80pub use debug::{ApuDebugView, CpuDebugView, MapperDebugView, PpuDebugView};
81pub use genie::{GenieCode, GenieError};
82pub use hardware_options::{
83 BoardDescription, EMULATION_EPOCH, HardwareOptions, OptionsDecodeError, config_digest,
84};
85pub use input_device::{
86 BandaiHyperShotState, FamilyKeyboardState, InputDevice, KonamiHyperShotState, PowerPadState,
87 SnesMouseState, VausState, ZapperState,
88};
89pub use legacy_movie::{
90 LegacyMeta, LegacyMovieError, import_fcm, import_fmv, import_mc2, import_vmv,
91};
92pub use movie::{
93 ATTESTATION_CHECKPOINT_INTERVAL, ATTESTATION_MAGIC, ATTESTATION_VERSION, Attestation,
94 AttestationBuilder, BYTES_PER_FRAME, FrameInput, MIN_MOVIE_FORMAT_VERSION,
95 MOVIE_FORMAT_VERSION, MOVIE_MAGIC, Movie, MovieError, MoviePlayer, MovieRecorder, StartPoint,
96 VerifyOutcome, power_on_for_movie, recorded_before_v2_timebase,
97};
98#[cfg(feature = "debug-hooks")]
99pub use nes::TraceRec;
100pub use nes::{
101 ConsoleModel, FRAME_DURATION_DENDY, FRAME_DURATION_NTSC, FRAME_DURATION_PAL, MAX_CPU_OVERCLOCK,
102 MAX_EXTRA_SCANLINES, Nes, PowerOnConfig, PowerOnRam,
103};
104// v2.1.7 P5 — re-export the PPU-side hardware-revision knobs at the core surface
105// so downstream consumers (frontend, test-harness) depend on `rustynes-core`.
106pub use rewind::{
107 REWIND_DEFAULT_KEYFRAME_PERIOD, REWIND_DEFAULT_MAX_BYTES, RewindError, RewindRing,
108};
109pub use rustynes_ppu::{PaletteInit, PpuRevision};
110pub use save_state::{
111 BinReader, BinWriter, FORMAT_VERSION, HEADER_LEN, Header, MAGIC, MIN_FORMAT_VERSION,
112 ROM_HASH_TAG_LEN, Section, SectionIter, SnapshotError, THUMBNAIL_HEIGHT, THUMBNAIL_LEN,
113 THUMBNAIL_VERSION, THUMBNAIL_WIDTH, parse_header, tag, tag_string, write_header, write_section,
114};
115pub use scheduler::M2Phase;
116pub use vs_db::{VsDbEntry, lookup as vs_db_lookup};
117pub use vs_dualsystem::{Emu, VsDualSystem};
118pub use zwinder::{
119 ZWINDER_DEFAULT_BUDGET_BYTES, ZWINDER_DEFAULT_KEYFRAME_INTERVAL, ZwinderError,
120 ZwinderStateManager,
121};
122
123/// Returns the crate version string.
124#[must_use]
125pub const fn version() -> &'static str {
126 env!("CARGO_PKG_VERSION")
127}
128
129/// NES region (governs clock dividers, scanline counts, audio rate tables).
130///
131/// See `docs/glossary.md` for definitions.
132#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
133pub enum Region {
134 /// NTSC (Japan, North America, Australia). 60 Hz, 262 scanlines.
135 Ntsc,
136 /// PAL (Europe). 50 Hz, 312 scanlines.
137 Pal,
138 /// Dendy (Russian PAL famiclone). 50 Hz, 312 scanlines, NTSC-style timing.
139 Dendy,
140}
141
142/// Ricoh 2A03 CPU/APU die revision, selecting the hardware-revision difference
143/// in the DMA unit's **"unexpected DMA" extra halt-read** (v2.1.7 "Hardware
144/// Revisions & DMA Frontier").
145///
146/// # The frontier — read this before trusting the non-default arm
147///
148/// The 2A03 shipped in several mask revisions. nesdev
149/// ([DMA](https://www.nesdev.org/wiki/DMA)) documents that when a DMC DMA halt
150/// is requested on a CPU cycle where an OAM (`$4014`) DMA is *also* halting —
151/// the "double-halt" overlap — some silicon performs an **extra** re-read of
152/// the parked 6502 address bus before the transfer resumes (the "unexpected
153/// DMA" read), and this differs by die revision.
154///
155/// **No public reference emulator models this die-revision difference, and no
156/// public test ROM verifies it.** A survey of Mesen2, ares, `BizHawk`,
157/// `TriCNES`, fceux, nestopia, `GeraNES`, and higan (v2.1.7, see ADR 0033)
158/// found that *none*
159/// branch DMA cycle behavior on 2A03 die stepping — the only revision-like
160/// switch any of them models is the orthogonal **console-type** distinction
161/// (Mesen2 `isNesBehavior`: NES-001/AV-Famicom clock a controller only on the
162/// *first* DMA idle read, original Famicom on *every* one), which is a
163/// different axis and is already reflected in this core's default
164/// register-readout model. The die-revision extra-read is therefore a genuine
165/// open frontier: this enum provides the **config surface** for it and a
166/// conservative, deterministic model, but the [`Rp2A03H`](Self::Rp2A03H) arm's
167/// direction is an **unverified hypothesis**, not an oracle-proven behavior.
168///
169/// # Contract
170///
171/// * **Default = [`Rp2A03G`](Self::Rp2A03G)** is **byte-identical** to the core
172/// as it shipped before v2.1.7 (`AccuracyCoin` 141/141, nestest 0-diff, and
173/// every committed DMA oracle ROM — the five `dmc_dma_during_read4` ROMs and
174/// both `sprdma_and_dmc_dma` ROMs — still `Passed`).
175/// * **[`Rp2A03H`](Self::Rp2A03H)** is a purely additive, opt-in knob that
176/// *omits* the double-halt extra read in the model. It is deterministic and
177/// reachable only when explicitly selected; the shipped/default build never
178/// touches it. **On this engine the extra-read gate is a documented no-op on
179/// every committed oracle**, so today `Rp2A03H` produces a **byte-identical**
180/// result to `Rp2A03G` across the entire committed DMA corpus (proven by the
181/// `cpu_2a03_revision` tests): the halted-DMC overlap-read fires but its
182/// parked address is always the post-`$4014` instruction fetch, never a
183/// side-effect register (see below). The revision difference is therefore a
184/// mechanism-level *model*, not an observable divergence — ADR 0033.
185///
186/// The revision is a **config knob re-applied on load, not part of the
187/// save-state** (like the optional OAM-decay model): the only state it
188/// influences is fully re-derived from the deterministic timeline, so a
189/// save/restore round-trip stays byte-identical for a fixed revision. See
190/// `docs/adr/0033-cpu-2a03-revision-dma-frontier.md`.
191#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
192pub enum Cpu2A03Revision {
193 /// RP2A03G — the common early/mid die the accuracy oracles were captured
194 /// against. **Performs** the double-halt "unexpected DMA" extra read.
195 /// This is the **default** and the byte-identical baseline.
196 #[default]
197 Rp2A03G,
198 /// RP2A03H — a later die modeled as **omitting** the double-halt extra
199 /// read. Opt-in, additive, deterministic — but an **unverified** direction
200 /// (no reference / no ROM proves it; see the type-level docs and ADR 0033).
201 Rp2A03H,
202}
203
204impl Cpu2A03Revision {
205 /// Whether this revision performs the "unexpected DMA" extra re-read of the
206 /// parked address bus on the DMC-halt-coincides-with-OAM-halt overlap
207 /// cycle. `true` for [`Rp2A03G`](Self::Rp2A03G) (the default /
208 /// byte-identical baseline), `false` for [`Rp2A03H`](Self::Rp2A03H).
209 #[must_use]
210 pub const fn has_unexpected_dma_extra_read(self) -> bool {
211 matches!(self, Self::Rp2A03G)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn version_is_non_empty() {
221 assert_ne!(version(), "");
222 }
223}