tetanes_core/
control_deck.rs

1//! Control Deck implementation. The primary entry-point for emulating the NES.
2
3use crate::{
4    apu::{self, Apu, Channel},
5    bus::Bus,
6    cart::{self, Cart},
7    common::{Clock, NesRegion, Regional, Reset, ResetKind, Sram},
8    cpu::Cpu,
9    debug::Debugger,
10    fs,
11    genie::{self, GenieCode},
12    input::{FourPlayer, Joypad, Player},
13    mapper::{Bf909Revision, Mapper, MapperRevision, Mmc3Revision},
14    mem::RamState,
15    ppu::Ppu,
16    video::{Video, VideoFilter},
17};
18use bitflags::bitflags;
19use serde::{Deserialize, Serialize};
20use std::{
21    io::Read,
22    path::{Path, PathBuf},
23};
24use thiserror::Error;
25use tracing::{error, info};
26
27/// Result returned from [`ControlDeck`] methods.
28pub type Result<T> = std::result::Result<T, Error>;
29
30/// Errors that [`ControlDeck`] can return.
31#[derive(Error, Debug)]
32#[must_use]
33pub enum Error {
34    /// [`Cart`] error when loading a ROM.
35    #[error(transparent)]
36    Cart(#[from] cart::Error),
37    /// Battery-backed RAM error.
38    #[error("sram error: {0:?}")]
39    Sram(fs::Error),
40    /// Save state error.
41    #[error("save state error: {0:?}")]
42    SaveState(fs::Error),
43    /// When trying to load a save state that doesn't exist.
44    #[error("no save state found")]
45    NoSaveStateFound,
46    /// Operational error indicating a ROM must be loaded first.
47    #[error("no rom is loaded")]
48    RomNotLoaded,
49    /// CPU state is corrupted and emulation can't continue. Could be due to a bad ROM image or a
50    /// corrupt save state.
51    #[error("cpu state is corrupted")]
52    CpuCorrupted,
53    /// Invalid Game Genie code error.
54    #[error(transparent)]
55    InvalidGenieCode(#[from] genie::Error),
56    /// Invalid file path.
57    #[error("invalid file path {0:?}")]
58    InvalidFilePath(PathBuf),
59    #[error("unimplemented mapper `{0}`")]
60    UnimplementedMapper(u16),
61    /// Filesystem error.
62    #[error(transparent)]
63    Fs(#[from] fs::Error),
64    /// IO error.
65    #[error("{context}: {source:?}")]
66    Io {
67        context: String,
68        source: std::io::Error,
69    },
70}
71
72impl Error {
73    pub fn io(source: std::io::Error, context: impl Into<String>) -> Self {
74        Self::Io {
75            context: context.into(),
76            source,
77        }
78    }
79}
80
81bitflags! {
82    /// Headless mode flags to disable audio and video processing, reducing CPU usage.
83    #[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize, )]
84    #[must_use]
85    pub struct HeadlessMode: u8 {
86        /// Disable audio mixing.
87        const NO_AUDIO = 0x01;
88        /// Disable pixel rendering.
89        const NO_VIDEO = 0x02;
90    }
91}
92
93/// Set of desired mapper revisions to use when loading a ROM matching the available mapper types.
94#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
95#[must_use]
96pub struct MapperRevisionsConfig {
97    /// MMC3 mapper revision.
98    pub mmc3: Mmc3Revision,
99    /// BF909 mapper revision.
100    pub bf909: Bf909Revision,
101}
102
103impl MapperRevisionsConfig {
104    /// Set the desired mapper revision to use when loading a ROM matching the available mapper types.
105    pub const fn set(&mut self, rev: MapperRevision) {
106        match rev {
107            MapperRevision::Mmc3(rev) => self.mmc3 = rev,
108            MapperRevision::Bf909(rev) => self.bf909 = rev,
109        }
110    }
111}
112
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114#[serde(default)]
115#[must_use]
116/// Control deck configuration settings.
117pub struct Config {
118    /// Whether to emulate the NES with cycle accuracy or not. Increased CPU use, but more accurate
119    /// emulation.
120    pub cycle_accurate: bool,
121    /// Video filter.
122    pub filter: VideoFilter,
123    /// NES region.
124    pub region: NesRegion,
125    /// RAM initialization state.
126    pub ram_state: RamState,
127    /// Four player adapter.
128    pub four_player: FourPlayer,
129    /// Enable zapper gun.
130    pub zapper: bool,
131    /// Game Genie codes.
132    pub genie_codes: Vec<GenieCode>,
133    /// Whether to support concurrent D-Pad input which wasn't possible on the original NES.
134    pub concurrent_dpad: bool,
135    /// Apu channels enabled.
136    pub channels_enabled: [bool; Apu::MAX_CHANNEL_COUNT],
137    /// Headless mode.
138    pub headless_mode: HeadlessMode,
139    /// Data directory for storing battery-backed RAM.
140    pub data_dir: PathBuf,
141    /// Which mapper revisions to emulate for any ROM loaded that uses this mapper.
142    pub mapper_revisions: MapperRevisionsConfig,
143    /// Whether to emulate PPU warmup where writes to certain registers are ignored. Can result in
144    /// some games not working correctly.
145    ///
146    /// See: <https://www.nesdev.org/wiki/PPU_power_up_state>
147    pub emulate_ppu_warmup: bool,
148}
149
150impl Config {
151    /// Base directory for storing TetaNES data.
152    pub const BASE_DIR: &'static str = "tetanes";
153    /// Directory for storing battery-backed Cart RAM.
154    pub const SRAM_DIR: &'static str = "sram";
155    /// File extension for battery-backed Cart RAM.
156    pub const SRAM_EXTENSION: &'static str = "sram";
157
158    /// Returns the default directory where TetaNES data is stored.
159    #[inline]
160    #[must_use]
161    pub fn default_data_dir() -> PathBuf {
162        dirs::data_local_dir().map_or_else(|| PathBuf::from("data"), |dir| dir.join(Self::BASE_DIR))
163    }
164
165    /// Returns the directory used to store battery-backed Cart RAM.
166    #[inline]
167    #[must_use]
168    pub fn sram_dir(&self) -> PathBuf {
169        self.data_dir.join(Self::SRAM_DIR)
170    }
171}
172
173impl Default for Config {
174    fn default() -> Self {
175        Self {
176            cycle_accurate: true,
177            filter: VideoFilter::default(),
178            region: NesRegion::Auto,
179            ram_state: RamState::Random,
180            four_player: FourPlayer::default(),
181            zapper: false,
182            genie_codes: vec![],
183            concurrent_dpad: false,
184            channels_enabled: [true; Apu::MAX_CHANNEL_COUNT],
185            headless_mode: HeadlessMode::empty(),
186            data_dir: Self::default_data_dir(),
187            mapper_revisions: MapperRevisionsConfig::default(),
188            emulate_ppu_warmup: false,
189        }
190    }
191}
192
193/// Represents a loaded ROM [`Cart`].
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct LoadedRom {
196    /// Name of ROM.
197    pub name: String,
198    /// Whether the loaded Cart is battery-backed.
199    pub battery_backed: bool,
200    /// Auto-detected of the loaded Cart.
201    pub region: NesRegion,
202}
203
204/// Represents an NES Control Deck. Encapsulates the entire emulation state.
205#[derive(Debug, Clone)]
206#[must_use]
207pub struct ControlDeck {
208    /// Whether a ROM is loaded and the emulation is currently running or not.
209    running: bool,
210    /// Video output and filtering.
211    video: Video,
212    /// Last frame number rendered, allowing `frame_buffer` to be cached if called multiple times.
213    last_frame_number: u32,
214    /// The currently loaded ROM [`Cart`], if any.
215    loaded_rom: Option<LoadedRom>,
216    /// Directory for storing battery-backed Cart RAM if a ROM is loaded.
217    sram_dir: PathBuf,
218    /// Mapper revisions to emulate for any ROM loaded that matches the given mappers.
219    mapper_revisions: MapperRevisionsConfig,
220    /// Whether to auto-detect the region based on the loaded Cart.
221    auto_detect_region: bool,
222    /// Remaining CPU cycles to execute used to clock a given number of seconds.
223    cycles_remaining: f32,
224    /// Emulated frame speed ranging from 0.25 to 2.0.
225    frame_speed: f32,
226    /// Accumulated frame speed to account for slower 1x speeds.
227    frame_accumulator: f32,
228    /// NES CPU.
229    cpu: Cpu,
230}
231
232impl Default for ControlDeck {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238impl ControlDeck {
239    /// Create a NES `ControlDeck` with the default configuration.
240    pub fn new() -> Self {
241        Self::with_config(Config::default())
242    }
243
244    /// Create a NES `ControlDeck` with a configuration.
245    pub fn with_config(cfg: Config) -> Self {
246        let mut cpu = Cpu::new(Bus::new(cfg.region, cfg.ram_state));
247        cpu.bus.ppu.skip_rendering = cfg.headless_mode.contains(HeadlessMode::NO_VIDEO);
248        cpu.bus.ppu.emulate_warmup = cfg.emulate_ppu_warmup;
249        cpu.bus.apu.skip_mixing = cfg.headless_mode.contains(HeadlessMode::NO_AUDIO);
250        if cfg.region.is_auto() {
251            cpu.set_region(NesRegion::Ntsc);
252        } else {
253            cpu.set_region(cfg.region);
254        }
255        cpu.bus.input.set_concurrent_dpad(cfg.concurrent_dpad);
256        cpu.bus.input.set_four_player(cfg.four_player);
257        cpu.bus.input.connect_zapper(cfg.zapper);
258        for (i, enabled) in cfg.channels_enabled.iter().enumerate() {
259            match Channel::try_from(i) {
260                Ok(channel) => cpu.bus.apu.set_channel_enabled(channel, *enabled),
261                Err(apu::ParseChannelError) => tracing::error!("invalid APU channel: {i}"),
262            }
263        }
264        for genie_code in cfg.genie_codes.iter().cloned() {
265            cpu.bus.add_genie_code(genie_code);
266        }
267        let video = Video::with_filter(cfg.filter);
268        Self {
269            running: false,
270            video,
271            last_frame_number: 0,
272            loaded_rom: None,
273            sram_dir: cfg.sram_dir(),
274            mapper_revisions: cfg.mapper_revisions,
275            auto_detect_region: cfg.region.is_auto(),
276            cycles_remaining: 0.0,
277            frame_speed: 1.0,
278            frame_accumulator: 0.0,
279            cpu,
280        }
281    }
282
283    /// Returns the path to the SRAM save file for a given ROM name which is used to store
284    /// battery-backed Cart RAM. Returns `None` when the current platform doesn't have a
285    /// `data` directory and no custom `data_dir` was configured.
286    pub fn sram_dir(&self, name: &str) -> PathBuf {
287        self.sram_dir.join(name)
288    }
289
290    /// Loads a ROM cartridge into memory
291    ///
292    /// # Errors
293    ///
294    /// If there is any issue loading the ROM, then an error is returned.
295    pub fn load_rom<S: ToString, F: Read>(&mut self, name: S, rom: &mut F) -> Result<LoadedRom> {
296        let name = name.to_string();
297        self.unload_rom()?;
298        let cart = Cart::from_rom(&name, rom, self.cpu.bus.ram_state)?;
299        if cart.mapper.is_none() {
300            return Err(Error::UnimplementedMapper(cart.mapper_num()));
301        }
302        let loaded_rom = LoadedRom {
303            name: name.clone(),
304            battery_backed: cart.battery_backed(),
305            region: cart.region(),
306        };
307        if self.auto_detect_region {
308            self.cpu.set_region(loaded_rom.region);
309        }
310        self.cpu.bus.load_cart(cart);
311        self.update_mapper_revisions();
312        self.reset(ResetKind::Hard);
313        self.running = true;
314        let sram_dir = self.sram_dir(&name);
315        if let Err(err) = self.load_sram(sram_dir) {
316            error!("failed to load SRAM: {err:?}");
317        }
318        self.loaded_rom = Some(loaded_rom.clone());
319        Ok(loaded_rom)
320    }
321
322    /// Loads a ROM cartridge into memory from a path.
323    ///
324    /// # Errors
325    ///
326    /// If there is any issue loading the ROM, then an error is returned.
327    pub fn load_rom_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<LoadedRom> {
328        use std::{fs::File, io::BufReader};
329
330        let path = path.as_ref();
331        let filename = fs::filename(path);
332        info!("loading ROM: {filename}");
333        File::open(path)
334            .map_err(|err| Error::io(err, format!("failed to open rom {path:?}")))
335            .and_then(|rom| self.load_rom(filename, &mut BufReader::new(rom)))
336    }
337
338    /// Unloads the currently loaded ROM and saves SRAM to disk if the Cart is battery-backed.
339    ///
340    /// # Errors
341    ///
342    /// If the loaded [`Cart`] is battery-backed and saving fails, then an error is returned.
343    pub fn unload_rom(&mut self) -> Result<()> {
344        if let Some(rom) = &self.loaded_rom {
345            let sram_dir = self.sram_dir(&rom.name);
346            if let Err(err) = self.save_sram(sram_dir) {
347                error!("failed to save SRAM: {err:?}");
348            }
349        }
350        self.loaded_rom = None;
351        self.cpu.bus.unload_cart();
352        self.running = false;
353        Ok(())
354    }
355
356    /// Load a previously saved CPU state.
357    #[inline]
358    pub fn load_cpu(&mut self, cpu: Cpu) {
359        self.cpu.load(cpu);
360    }
361
362    /// Set the [`MapperRevision`] to emulate for the any ROM loaded that uses this mapper.
363    #[inline]
364    pub const fn set_mapper_revision(&mut self, rev: MapperRevision) {
365        self.mapper_revisions.set(rev);
366        self.update_mapper_revisions();
367    }
368
369    /// Set the set of [`MapperRevisionsConfig`] to emulate for the any ROM loaded that uses this
370    /// mapper.
371    #[inline]
372    pub const fn set_mapper_revisions(&mut self, revs: MapperRevisionsConfig) {
373        self.mapper_revisions = revs;
374        self.update_mapper_revisions();
375    }
376
377    /// Internal method to update the loaded ROM mapper revision when `mapper_revisions` is
378    /// updated.
379    const fn update_mapper_revisions(&mut self) {
380        match &mut self.cpu.bus.ppu.bus.mapper {
381            Mapper::Txrom(mapper) => {
382                mapper.set_revision(self.mapper_revisions.mmc3);
383            }
384            Mapper::Bf909x(mapper) => {
385                mapper.set_revision(self.mapper_revisions.bf909);
386            }
387            // Remaining mappers all have more concrete detection via ROM headers
388            Mapper::None(_)
389            | Mapper::Nrom(_)
390            | Mapper::Sxrom(_)
391            | Mapper::Uxrom(_)
392            | Mapper::Cnrom(_)
393            | Mapper::Exrom(_)
394            | Mapper::Axrom(_)
395            | Mapper::Pxrom(_)
396            | Mapper::Fxrom(_)
397            | Mapper::ColorDreams(_)
398            | Mapper::BandaiFCG(_)
399            | Mapper::JalecoSs88006(_)
400            | Mapper::Namco163(_)
401            | Mapper::Vrc6(_)
402            | Mapper::Bnrom(_)
403            | Mapper::Nina001(_)
404            | Mapper::Gxrom(_)
405            | Mapper::SunsoftFme7(_)
406            | Mapper::Dxrom76(_)
407            | Mapper::Nina003006(_)
408            | Mapper::Dxrom88(_)
409            | Mapper::Dxrom95(_)
410            | Mapper::Dxrom154(_)
411            | Mapper::Dxrom206(_) => (),
412        }
413    }
414
415    /// Set whether concurrent D-Pad input is enabled which wasn't possible on the original NES.
416    #[inline]
417    pub fn set_concurrent_dpad(&mut self, enabled: bool) {
418        self.cpu.bus.input.set_concurrent_dpad(enabled);
419    }
420
421    /// Set whether emulation should be cycle accurate or not. Disabling this can increase
422    /// performance.
423    #[inline]
424    pub const fn set_cycle_accurate(&mut self, enabled: bool) {
425        self.cpu.cycle_accurate = enabled;
426    }
427
428    /// Set emulation RAM initialization state.
429    #[inline]
430    pub const fn set_ram_state(&mut self, ram_state: RamState) {
431        self.cpu.bus.ram_state = ram_state;
432    }
433
434    /// Set the headless mode which can increase performance when the frame and audio outputs are
435    /// not needed.
436    #[inline]
437    pub const fn set_headless_mode(&mut self, mode: HeadlessMode) {
438        self.cpu.bus.ppu.skip_rendering = mode.contains(HeadlessMode::NO_VIDEO);
439        self.cpu.bus.apu.skip_mixing = mode.contains(HeadlessMode::NO_AUDIO);
440    }
441
442    /// Set whether to emulate PPU warmup where writes to certain registers are ignored. Can result
443    /// in some games not working correctly.
444    ///
445    /// See: <https://www.nesdev.org/wiki/PPU_power_up_state>
446    #[inline]
447    pub const fn set_emulate_ppu_warmup(&mut self, enabled: bool) {
448        self.cpu.bus.ppu.emulate_warmup = enabled;
449    }
450
451    /// Adds a debugger callback to be executed any time the debugger conditions
452    /// match.
453    pub fn add_debugger(&mut self, debugger: Debugger) {
454        match debugger {
455            Debugger::Ppu(debugger) => self.cpu.bus.ppu.debugger = Some(debugger),
456        }
457    }
458
459    /// Removes a debugger callback.
460    pub fn remove_debugger(&mut self, debugger: Debugger) {
461        match debugger {
462            Debugger::Ppu(_) => self.cpu.bus.ppu.debugger = None,
463        }
464    }
465
466    /// Returns the name of the currently loaded ROM [`Cart`]. Returns `None` if no ROM is loaded.
467    #[inline]
468    #[must_use]
469    pub const fn loaded_rom(&self) -> Option<&LoadedRom> {
470        self.loaded_rom.as_ref()
471    }
472
473    /// Returns the auto-detected [`NesRegion`] for the loaded ROM. Returns `None` if no ROM is
474    /// loaded.
475    #[inline]
476    #[must_use]
477    pub fn cart_region(&self) -> Option<NesRegion> {
478        self.loaded_rom.as_ref().map(|rom| rom.region)
479    }
480
481    /// Returns whether the loaded ROM is battery-backed. Returns `None` if no ROM is loaded.
482    #[inline]
483    #[must_use]
484    pub fn cart_battery_backed(&self) -> Option<bool> {
485        self.loaded_rom.as_ref().map(|rom| rom.battery_backed)
486    }
487
488    /// Returns the NES Work RAM.
489    #[inline]
490    #[must_use]
491    pub fn wram(&self) -> &[u8] {
492        self.cpu.bus.wram()
493    }
494
495    /// Returns the battery-backed Save RAM.
496    #[inline]
497    #[must_use]
498    pub fn sram(&self) -> &[u8] {
499        self.cpu.bus.sram()
500    }
501
502    /// Save battery-backed Save RAM to a file (if cartridge supports it)
503    ///
504    /// # Errors
505    ///
506    /// If the file path is invalid or fails to save, then an error is returned.
507    pub fn save_sram(&self, path: impl AsRef<Path>) -> Result<()> {
508        if let Some(true) = self.cart_battery_backed() {
509            let path = path.as_ref();
510            if path.is_dir() {
511                return Err(Error::InvalidFilePath(path.to_path_buf()));
512            }
513
514            info!("saving SRAM...");
515            self.cpu
516                .bus
517                .save(path.with_extension(Config::SRAM_EXTENSION))
518                .map_err(Error::Sram)?;
519        }
520        Ok(())
521    }
522
523    /// Load battery-backed Save RAM from a file (if cartridge supports it)
524    ///
525    /// # Errors
526    ///
527    /// If the file path is invalid or fails to load, then an error is returned.
528    pub fn load_sram(&mut self, path: impl AsRef<Path>) -> Result<()> {
529        if let Some(true) = self.cart_battery_backed() {
530            let path = path.as_ref();
531            if path.is_dir() {
532                return Err(Error::InvalidFilePath(path.to_path_buf()));
533            }
534            if path.is_file() {
535                info!("loading SRAM...");
536                self.cpu
537                    .bus
538                    .load(path.with_extension(Config::SRAM_EXTENSION))
539                    .map_err(Error::Sram)?;
540            }
541        }
542        Ok(())
543    }
544
545    /// Save the current state of the console into a save file.
546    ///
547    /// # Errors
548    ///
549    /// If there is an issue saving the state, then an error is returned.
550    pub fn save_state(&mut self, path: impl AsRef<Path>) -> Result<()> {
551        if self.loaded_rom().is_none() {
552            return Err(Error::RomNotLoaded);
553        };
554        let path = path.as_ref();
555        fs::save(path, &self.cpu).map_err(Error::SaveState)
556    }
557
558    /// Load the console with data saved from a save state, if it exists.
559    ///
560    /// # Errors
561    ///
562    /// If there is an issue loading the save state, then an error is returned.
563    pub fn load_state(&mut self, path: impl AsRef<Path>) -> Result<()> {
564        if self.loaded_rom().is_none() {
565            return Err(Error::RomNotLoaded);
566        };
567        let path = path.as_ref();
568        if fs::exists(path) {
569            fs::load::<Cpu>(path)
570                .map_err(Error::SaveState)
571                .map(|mut cpu| {
572                    cpu.bus.input.clear(); // Discard inputs from save states
573                    self.load_cpu(cpu)
574                })
575        } else {
576            Err(Error::NoSaveStateFound)
577        }
578    }
579
580    /// Load the raw underlying frame buffer from the PPU for further processing.
581    pub fn frame_buffer_raw(&mut self) -> &[u16] {
582        self.cpu.bus.ppu.frame_buffer()
583    }
584
585    /// Load a frame worth of pixels.
586    #[inline]
587    pub fn frame_buffer(&mut self) -> &[u8] {
588        // Avoid applying filter if the frame number hasn't changed
589        let frame_number = self.cpu.bus.ppu.frame_number();
590        if self.last_frame_number == frame_number {
591            return &self.video.frame;
592        }
593
594        self.last_frame_number = frame_number;
595        self.video
596            .apply_filter(self.cpu.bus.ppu.frame_buffer(), frame_number)
597    }
598
599    /// Load a frame worth of pixels into the given buffer.
600    #[inline]
601    pub fn frame_buffer_into(&self, buffer: &mut [u8]) {
602        self.video.apply_filter_into(
603            self.cpu.bus.ppu.frame_buffer(),
604            self.cpu.bus.ppu.frame_number(),
605            buffer,
606        );
607    }
608
609    /// Get the current frame number.
610    #[inline]
611    #[must_use]
612    pub const fn frame_number(&self) -> u32 {
613        self.cpu.bus.ppu.frame_number()
614    }
615
616    /// Get audio samples.
617    #[inline]
618    #[must_use]
619    pub fn audio_samples(&self) -> &[f32] {
620        self.cpu.bus.audio_samples()
621    }
622
623    /// Clear audio samples.
624    #[inline]
625    pub fn clear_audio_samples(&mut self) {
626        self.cpu.bus.clear_audio_samples();
627    }
628
629    /// CPU clock rate based on currently configured NES region.
630    #[inline]
631    #[must_use]
632    pub const fn clock_rate(&self) -> f32 {
633        self.cpu.clock_rate()
634    }
635
636    /// Steps the control deck one CPU clock.
637    ///
638    /// # Errors
639    ///
640    /// If CPU encounters an invalid opcode, then an error is returned.
641    pub fn clock_instr(&mut self) -> Result<u64> {
642        if !self.running {
643            return Err(Error::RomNotLoaded);
644        }
645        let cycles = self.clock();
646        if self.cpu_corrupted() {
647            self.running = false;
648            return Err(Error::CpuCorrupted);
649        }
650        Ok(cycles)
651    }
652
653    /// Steps the control deck the number of seconds.
654    ///
655    /// # Errors
656    ///
657    /// If CPU encounters an invalid opcode, then an error is returned.
658    pub fn clock_seconds(&mut self, seconds: f32) -> Result<u64> {
659        self.cycles_remaining += self.clock_rate() * seconds;
660        let mut total_cycles = 0;
661        while self.cycles_remaining > 0.0 {
662            let cycles = self.clock_instr()?;
663            total_cycles += cycles;
664            self.cycles_remaining -= cycles as f32;
665        }
666        Ok(total_cycles)
667    }
668
669    /// Steps the control deck an entire frame.
670    ///
671    /// # Errors
672    ///
673    /// If CPU encounters an invalid opcode, then an error is returned.
674    pub fn clock_frame(&mut self) -> Result<u64> {
675        #[cfg(feature = "profiling")]
676        puffin::profile_function!();
677
678        // Frames that aren't multiples of the default render 1 more/less frames
679        // every other frame
680        // e.g. a speed of 1.5 will clock # of frames: 1, 2, 1, 2, 1, 2, 1, 2, ...
681        // A speed of 0.5 will clock 0, 1, 0, 1, 0, 1, 0, 1, 0, ...
682        self.frame_accumulator += self.frame_speed;
683        let mut frames_to_clock = 0;
684        while self.frame_accumulator >= 1.0 {
685            self.frame_accumulator -= 1.0;
686            frames_to_clock += 1;
687        }
688
689        let mut total_cycles = 0;
690        for _ in 0..frames_to_clock {
691            let frame = self.frame_number();
692            while frame == self.frame_number() {
693                total_cycles += self.clock_instr()?;
694            }
695        }
696        self.cpu.bus.apu.clock_flush();
697
698        Ok(total_cycles)
699    }
700
701    /// Steps the control deck an entire frame, calling `handle_output` with the `cycles`, `frame_buffer` and
702    /// `audio_samples` for that frame.
703    ///
704    /// # Errors
705    ///
706    /// If CPU encounters an invalid opcode, then an error is returned.
707    pub fn clock_frame_output<T>(
708        &mut self,
709        handle_output: impl FnOnce(u64, &[u8], &[f32]) -> T,
710    ) -> Result<T> {
711        let cycles = self.clock_frame()?;
712        let frame = self.video.apply_filter(
713            self.cpu.bus.ppu.frame_buffer(),
714            self.cpu.bus.ppu.frame_number(),
715        );
716        let audio = self.cpu.bus.audio_samples();
717        let res = handle_output(cycles, frame, audio);
718        self.cpu.bus.clear_audio_samples();
719        Ok(res)
720    }
721
722    /// Steps the control deck an entire frame, copying the `frame_buffer` and
723    /// `audio_samples` for that frame into the provided buffers.
724    ///
725    /// # Errors
726    ///
727    /// If CPU encounteres an invalid opcode, an error is returned.
728    pub fn clock_frame_into(
729        &mut self,
730        frame_buffer: &mut [u8],
731        audio_samples: &mut [f32],
732    ) -> Result<u64> {
733        let cycles = self.clock_frame()?;
734        let frame = self.video.apply_filter(
735            self.cpu.bus.ppu.frame_buffer(),
736            self.cpu.bus.ppu.frame_number(),
737        );
738        frame_buffer.copy_from_slice(&frame[..frame_buffer.len()]);
739        let audio = self.cpu.bus.audio_samples();
740        audio_samples.copy_from_slice(&audio[..audio_samples.len()]);
741        self.clear_audio_samples();
742        Ok(cycles)
743    }
744
745    /// Steps the control deck an entire frame with run-ahead frames to reduce input lag.
746    ///
747    /// # Errors
748    ///
749    /// If CPU encounters an invalid opcode, then an error is returned.
750    pub fn clock_frame_ahead<T>(
751        &mut self,
752        run_ahead: usize,
753        handle_output: impl FnOnce(u64, &[u8], &[f32]) -> T,
754    ) -> Result<T> {
755        #[cfg(feature = "profiling")]
756        puffin::profile_function!();
757
758        if run_ahead == 0 {
759            return self.clock_frame_output(handle_output);
760        }
761
762        // Clock current frame and save state so we can rewind
763        self.clock_frame()?;
764        let frame = std::mem::take(&mut self.cpu.bus.ppu.frame.buffer);
765        // Save state so we can rewind
766        let state = bincode::serialize(&self.cpu)
767            .map_err(|err| fs::Error::SerializationFailed(err.to_string()))?;
768
769        // Clock additional frames and discard video/audio
770        self.cpu.bus.ppu.skip_rendering = true;
771        for _ in 1..run_ahead {
772            self.clock_frame()?;
773        }
774        self.cpu.bus.ppu.skip_rendering = false;
775
776        // Output the future frame video/audio
777        self.clear_audio_samples();
778        let result = self.clock_frame_output(handle_output)?;
779
780        // Restore back to current frame
781        let mut state = bincode::deserialize::<Cpu>(&state)
782            .map_err(|err| fs::Error::DeserializationFailed(err.to_string()))?;
783        state.bus.ppu.frame.buffer = frame;
784        self.load_cpu(state);
785
786        Ok(result)
787    }
788
789    /// Steps the control deck an entire frame with run-ahead frames to reduce input lag.
790    ///
791    /// # Errors
792    ///
793    /// If CPU encounters an invalid opcode, then an error is returned.
794    pub fn clock_frame_ahead_into(
795        &mut self,
796        run_ahead: usize,
797        frame_buffer: &mut [u8],
798        audio_samples: &mut [f32],
799    ) -> Result<u64> {
800        #[cfg(feature = "profiling")]
801        puffin::profile_function!();
802
803        if run_ahead == 0 {
804            return self.clock_frame_into(frame_buffer, audio_samples);
805        }
806
807        // Clock current frame and save state so we can rewind
808        self.clock_frame()?;
809        let frame = std::mem::take(&mut self.cpu.bus.ppu.frame.buffer);
810        // Save state so we can rewind
811        let state = bincode::serialize(&self.cpu)
812            .map_err(|err| fs::Error::SerializationFailed(err.to_string()))?;
813
814        // Clock additional frames and discard video/audio
815        for _ in 1..run_ahead {
816            self.clock_frame()?;
817        }
818
819        // Output the future frame/audio
820        self.clear_audio_samples();
821        let cycles = self.clock_frame_into(frame_buffer, audio_samples)?;
822
823        // Restore back to current frame
824        let mut state = bincode::deserialize::<Cpu>(&state)
825            .map_err(|err| fs::Error::DeserializationFailed(err.to_string()))?;
826        state.bus.ppu.frame.buffer = frame;
827        self.load_cpu(state);
828
829        Ok(cycles)
830    }
831
832    /// Steps the control deck a single scanline.
833    ///
834    /// # Errors
835    ///
836    /// If CPU encounters an invalid opcode, then an error is returned.
837    pub fn clock_scanline(&mut self) -> Result<u64> {
838        #[cfg(feature = "profiling")]
839        puffin::profile_function!();
840
841        let mut total_cycles = 0;
842        let current_scanline = self.cpu.bus.ppu.scanline;
843        while current_scanline == self.cpu.bus.ppu.scanline {
844            total_cycles += self.clock_instr()?;
845        }
846        Ok(total_cycles)
847    }
848
849    /// Returns whether the CPU is corrupted or not which means it encounted an invalid/unhandled
850    /// opcode and can't proceed executing the current ROM.
851    #[inline]
852    #[must_use]
853    pub const fn cpu_corrupted(&self) -> bool {
854        self.cpu.corrupted
855    }
856
857    /// Returns the current [`Cpu`] state.
858    #[inline]
859    pub const fn cpu(&self) -> &Cpu {
860        &self.cpu
861    }
862
863    /// Returns a mutable reference to the current [`Cpu`] state.
864    #[inline]
865    pub const fn cpu_mut(&mut self) -> &mut Cpu {
866        &mut self.cpu
867    }
868
869    /// Returns the current [`Ppu`] state.
870    #[inline]
871    pub const fn ppu(&self) -> &Ppu {
872        &self.cpu.bus.ppu
873    }
874
875    /// Returns a mutable reference to the current [`Ppu`] state.
876    #[inline]
877    pub const fn ppu_mut(&mut self) -> &mut Ppu {
878        &mut self.cpu.bus.ppu
879    }
880
881    /// Retu[ns the current [`Bus`] state.
882    #[inline]
883    pub const fn bus(&self) -> &Bus {
884        &self.cpu.bus
885    }
886
887    /// Returns a mutable reference to the current [`Bus`] state.
888    #[inline]
889    pub const fn bus_mut(&mut self) -> &mut Bus {
890        &mut self.cpu.bus
891    }
892
893    /// Returns the current [`Apu`] state.
894    #[inline]
895    pub const fn apu(&self) -> &Apu {
896        &self.cpu.bus.apu
897    }
898
899    /// Returns a mutable reference to the current [`Apu`] state.
900    #[inline]
901    pub const fn apu_mut(&mut self) -> &Apu {
902        &mut self.cpu.bus.apu
903    }
904
905    /// Returns the current [`Mapper`] state.
906    #[inline]
907    pub const fn mapper(&self) -> &Mapper {
908        &self.cpu.bus.ppu.bus.mapper
909    }
910
911    /// Returns a mutable reference to the current [`Mapper`] state.
912    #[inline]
913    pub const fn mapper_mut(&mut self) -> &mut Mapper {
914        &mut self.cpu.bus.ppu.bus.mapper
915    }
916
917    /// Returns the current four player mode.
918    #[inline]
919    pub const fn four_player(&self) -> FourPlayer {
920        self.cpu.bus.input.four_player
921    }
922
923    /// Enable/Disable Four Score for 4-player controllers.
924    #[inline]
925    pub fn set_four_player(&mut self, four_player: FourPlayer) {
926        self.cpu.bus.input.set_four_player(four_player);
927    }
928
929    /// Returns the current [`Joypad`] state for a given controller slot.
930    #[inline]
931    pub const fn joypad(&mut self, slot: Player) -> &Joypad {
932        self.cpu.bus.input.joypad(slot)
933    }
934
935    /// Returns a mutable reference to the current [`Joypad`] state for a given controller slot.
936    #[inline]
937    pub const fn joypad_mut(&mut self, slot: Player) -> &mut Joypad {
938        self.cpu.bus.input.joypad_mut(slot)
939    }
940
941    /// Returns whether the [`Zapper`](crate::input::Zapper) gun is connected.
942    #[inline]
943    pub const fn zapper_connected(&self) -> bool {
944        self.cpu.bus.input.zapper.connected
945    }
946
947    /// Enable [`Zapper`](crate::input::Zapper) gun.
948    #[inline]
949    pub const fn connect_zapper(&mut self, enabled: bool) {
950        self.cpu.bus.input.connect_zapper(enabled);
951    }
952
953    /// Returns the current [`Zapper`](crate::input::Zapper) aim position.
954    #[inline]
955    #[must_use]
956    pub const fn zapper_pos(&self) -> (u32, u32) {
957        let zapper = self.cpu.bus.input.zapper;
958        (zapper.x(), zapper.y())
959    }
960
961    /// Trigger [`Zapper`](crate::input::Zapper) gun.
962    #[inline]
963    pub fn trigger_zapper(&mut self) {
964        self.cpu.bus.input.zapper.trigger();
965    }
966
967    /// Aim [`Zapper`](crate::input::Zapper) gun.
968    #[inline]
969    pub fn aim_zapper(&mut self, x: u32, y: u32) {
970        self.cpu.bus.input.zapper.aim(x, y);
971    }
972
973    /// Set the video filter for frame buffer output when calling [`ControlDeck::frame_buffer`].
974    #[inline]
975    pub const fn set_filter(&mut self, filter: VideoFilter) {
976        self.video.filter = filter;
977    }
978
979    /// Set the [`Apu`] sample rate.
980    #[inline]
981    pub fn set_sample_rate(&mut self, sample_rate: f32) {
982        self.cpu.bus.apu.set_sample_rate(sample_rate);
983    }
984
985    /// Set the emulation speed.
986    #[inline]
987    pub fn set_frame_speed(&mut self, speed: f32) {
988        self.frame_speed = speed;
989        self.cpu.bus.apu.set_frame_speed(speed);
990    }
991
992    /// Add a NES Game Genie code.
993    ///
994    /// # Errors
995    ///
996    /// If the genie code is invalid, an error is returned.
997    #[inline]
998    pub fn add_genie_code(&mut self, genie_code: String) -> Result<()> {
999        self.cpu.bus.add_genie_code(GenieCode::new(genie_code)?);
1000        Ok(())
1001    }
1002
1003    /// Remove a NES Game Genie code.
1004    #[inline]
1005    pub fn remove_genie_code(&mut self, genie_code: &str) {
1006        self.cpu.bus.remove_genie_code(genie_code);
1007    }
1008
1009    /// Remove all NES Game Genie codes.
1010    #[inline]
1011    pub fn clear_genie_codes(&mut self) {
1012        self.cpu.bus.clear_genie_codes();
1013    }
1014
1015    /// Returns whether a given [`Apu`] [`Channel`] is enabled.
1016    #[inline]
1017    #[must_use]
1018    pub const fn channel_enabled(&self, channel: Channel) -> bool {
1019        self.cpu.bus.apu.channel_enabled(channel)
1020    }
1021
1022    /// Enable or disable a given [`Apu`] [`Channel`].
1023    #[inline]
1024    pub const fn set_apu_channel_enabled(&mut self, channel: Channel, enabled: bool) {
1025        self.cpu.bus.apu.set_channel_enabled(channel, enabled);
1026    }
1027
1028    /// Toggle a given [`Apu`] [`Channel`].
1029    #[inline]
1030    pub const fn toggle_apu_channel(&mut self, channel: Channel) {
1031        self.cpu.bus.apu.toggle_channel(channel);
1032    }
1033
1034    /// Returns whether the control deck is currently running.
1035    #[inline]
1036    #[must_use]
1037    pub const fn is_running(&self) -> bool {
1038        self.running
1039    }
1040}
1041
1042impl Clock for ControlDeck {
1043    /// Steps the control deck a single clock cycle.
1044    fn clock(&mut self) -> u64 {
1045        self.cpu.clock()
1046    }
1047}
1048
1049impl Regional for ControlDeck {
1050    /// Get the NES format for the emulation.
1051    fn region(&self) -> NesRegion {
1052        self.cpu.region
1053    }
1054
1055    /// Set the NES format for the emulation.
1056    fn set_region(&mut self, region: NesRegion) {
1057        self.auto_detect_region = region.is_auto();
1058        if self.auto_detect_region {
1059            self.cpu.set_region(self.cart_region().unwrap_or_default());
1060        } else {
1061            self.cpu.set_region(region);
1062        }
1063    }
1064}
1065
1066impl Reset for ControlDeck {
1067    /// Resets the console.
1068    fn reset(&mut self, kind: ResetKind) {
1069        self.cpu.reset(kind);
1070        if self.loaded_rom.is_some() {
1071            self.running = true;
1072        }
1073    }
1074}