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}