Skip to main content

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