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
61pub use bus::LockstepBus;
62#[cfg(feature = "debug-hooks")]
63pub use bus::{AccessRec, EventBpKind, EventBreakHit, EventKind, EventRec, InterruptRec};
64pub use controller::{Buttons, Controller};
65pub use debug::{ApuDebugView, CpuDebugView, MapperDebugView, PpuDebugView};
66pub use genie::{GenieCode, GenieError};
67pub use input_device::{
68    BandaiHyperShotState, FamilyKeyboardState, InputDevice, KonamiHyperShotState, PowerPadState,
69    SnesMouseState, VausState, ZapperState,
70};
71pub use legacy_movie::{
72    LegacyMeta, LegacyMovieError, import_fcm, import_fmv, import_mc2, import_vmv,
73};
74pub use movie::{
75    BYTES_PER_FRAME, FrameInput, MOVIE_FORMAT_VERSION, MOVIE_MAGIC, Movie, MovieError, MoviePlayer,
76    MovieRecorder, StartPoint, recorded_before_v2_timebase,
77};
78#[cfg(feature = "debug-hooks")]
79pub use nes::TraceRec;
80pub use nes::{
81    FRAME_DURATION_DENDY, FRAME_DURATION_NTSC, FRAME_DURATION_PAL, Nes, PowerOnConfig, PowerOnRam,
82};
83// v2.1.7 P5 — re-export the PPU-side hardware-revision knobs at the core surface
84// so downstream consumers (frontend, test-harness) depend on `rustynes-core`.
85pub use rewind::{
86    REWIND_DEFAULT_KEYFRAME_PERIOD, REWIND_DEFAULT_MAX_BYTES, RewindError, RewindRing,
87};
88pub use rustynes_ppu::{PaletteInit, PpuRevision};
89pub use save_state::{
90    BinReader, BinWriter, FORMAT_VERSION, HEADER_LEN, Header, MAGIC, ROM_HASH_TAG_LEN, Section,
91    SectionIter, SnapshotError, THUMBNAIL_HEIGHT, THUMBNAIL_LEN, THUMBNAIL_VERSION,
92    THUMBNAIL_WIDTH, parse_header, tag, tag_string, write_header, write_section,
93};
94pub use scheduler::M2Phase;
95pub use vs_db::{VsDbEntry, lookup as vs_db_lookup};
96pub use vs_dualsystem::{Emu, VsDualSystem};
97pub use zwinder::{
98    ZWINDER_DEFAULT_BUDGET_BYTES, ZWINDER_DEFAULT_KEYFRAME_INTERVAL, ZwinderError,
99    ZwinderStateManager,
100};
101
102/// Returns the crate version string.
103#[must_use]
104pub const fn version() -> &'static str {
105    env!("CARGO_PKG_VERSION")
106}
107
108/// NES region (governs clock dividers, scanline counts, audio rate tables).
109///
110/// See `docs/glossary.md` for definitions.
111#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
112pub enum Region {
113    /// NTSC (Japan, North America, Australia). 60 Hz, 262 scanlines.
114    Ntsc,
115    /// PAL (Europe). 50 Hz, 312 scanlines.
116    Pal,
117    /// Dendy (Russian PAL famiclone). 50 Hz, 312 scanlines, NTSC-style timing.
118    Dendy,
119}
120
121/// Ricoh 2A03 CPU/APU die revision, selecting the hardware-revision difference
122/// in the DMA unit's **"unexpected DMA" extra halt-read** (v2.1.7 "Hardware
123/// Revisions & DMA Frontier").
124///
125/// # The frontier — read this before trusting the non-default arm
126///
127/// The 2A03 shipped in several mask revisions. nesdev
128/// ([DMA](https://www.nesdev.org/wiki/DMA)) documents that when a DMC DMA halt
129/// is requested on a CPU cycle where an OAM (`$4014`) DMA is *also* halting —
130/// the "double-halt" overlap — some silicon performs an **extra** re-read of
131/// the parked 6502 address bus before the transfer resumes (the "unexpected
132/// DMA" read), and this differs by die revision.
133///
134/// **No public reference emulator models this die-revision difference, and no
135/// public test ROM verifies it.** A survey of Mesen2, ares, `BizHawk`,
136/// `TriCNES`, fceux, nestopia, `GeraNES`, and higan (v2.1.7, see ADR 0033)
137/// found that *none*
138/// branch DMA cycle behavior on 2A03 die stepping — the only revision-like
139/// switch any of them models is the orthogonal **console-type** distinction
140/// (Mesen2 `isNesBehavior`: NES-001/AV-Famicom clock a controller only on the
141/// *first* DMA idle read, original Famicom on *every* one), which is a
142/// different axis and is already reflected in this core's default
143/// register-readout model. The die-revision extra-read is therefore a genuine
144/// open frontier: this enum provides the **config surface** for it and a
145/// conservative, deterministic model, but the [`Rp2A03H`](Self::Rp2A03H) arm's
146/// direction is an **unverified hypothesis**, not an oracle-proven behavior.
147///
148/// # Contract
149///
150/// * **Default = [`Rp2A03G`](Self::Rp2A03G)** is **byte-identical** to the core
151///   as it shipped before v2.1.7 (`AccuracyCoin` 141/141, nestest 0-diff, and
152///   every committed DMA oracle ROM — the five `dmc_dma_during_read4` ROMs and
153///   both `sprdma_and_dmc_dma` ROMs — still `Passed`).
154/// * **[`Rp2A03H`](Self::Rp2A03H)** is a purely additive, opt-in knob that
155///   *omits* the double-halt extra read in the model. It is deterministic and
156///   reachable only when explicitly selected; the shipped/default build never
157///   touches it. **On this engine the extra-read gate is a documented no-op on
158///   every committed oracle**, so today `Rp2A03H` produces a **byte-identical**
159///   result to `Rp2A03G` across the entire committed DMA corpus (proven by the
160///   `cpu_2a03_revision` tests): the halted-DMC overlap-read fires but its
161///   parked address is always the post-`$4014` instruction fetch, never a
162///   side-effect register (see below). The revision difference is therefore a
163///   mechanism-level *model*, not an observable divergence — ADR 0033.
164///
165/// The revision is a **config knob re-applied on load, not part of the
166/// save-state** (like the optional OAM-decay model): the only state it
167/// influences is fully re-derived from the deterministic timeline, so a
168/// save/restore round-trip stays byte-identical for a fixed revision. See
169/// `docs/adr/0033-cpu-2a03-revision-dma-frontier.md`.
170#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
171pub enum Cpu2A03Revision {
172    /// RP2A03G — the common early/mid die the accuracy oracles were captured
173    /// against. **Performs** the double-halt "unexpected DMA" extra read.
174    /// This is the **default** and the byte-identical baseline.
175    #[default]
176    Rp2A03G,
177    /// RP2A03H — a later die modeled as **omitting** the double-halt extra
178    /// read. Opt-in, additive, deterministic — but an **unverified** direction
179    /// (no reference / no ROM proves it; see the type-level docs and ADR 0033).
180    Rp2A03H,
181}
182
183impl Cpu2A03Revision {
184    /// Whether this revision performs the "unexpected DMA" extra re-read of the
185    /// parked address bus on the DMC-halt-coincides-with-OAM-halt overlap
186    /// cycle. `true` for [`Rp2A03G`](Self::Rp2A03G) (the default /
187    /// byte-identical baseline), `false` for [`Rp2A03H`](Self::Rp2A03H).
188    #[must_use]
189    pub const fn has_unexpected_dma_extra_read(self) -> bool {
190        matches!(self, Self::Rp2A03G)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn version_is_non_empty() {
200        assert!(!version().is_empty());
201    }
202}