lib.rsannotatedlib.rssource342 lines · 12.8 KB · raw
1//! An SNES, embedded.
2//!
3//! `snes-core` is written to be driven by a frontend: every tick it is handed
4//! somewhere to draw, somewhere to play sound, somewhere to keep saves, and
5//! something to ask about the controller. [`Console`] owns all four, so the
6//! rest of this project sees a machine with a screen, a pad and RAM, and can
7//! read and write them between frames — which is the whole reason the emulator
8//! is a library here instead of a process beside us.
9
10use std::collections::HashMap;
11use std::convert::Infallible;
12
13use bincode::{Decode, Encode};
14use crc::Crc;
15use jgenesis_common::audio::DynamicResamplingRate;
16use jgenesis_common::frontend::{
17    AudioOutput, Color, EmulatorTrait, FrameSize, InputPoller, RenderFrameOptions, Renderer,
18    SaveWriter, TickEffect,
19};
20use snes_core::api::{CoprocessorRoms, SnesEmulator, SnesEmulatorConfig, SnesLoadError};
21pub use snes_core::api::WRAM_LEN;
22use snes_core::input::SnesInputs;
23
24pub use snes_config::SnesButton;
25
26/// The most recent picture the PPU finished, RGBA, row-major.
27#[derive(Default)]
28pub struct Frame {
29    pub size: (u32, u32),
30    pub pixels: Vec<Color>,
31}
32
33impl Frame {
34    /// The picture as `width * height * 4` bytes of RGBA, which is what every
35    /// texture upload wants. `Color` is `repr(C)` and `Pod`, so this is a view
36    /// of the same memory and not a conversion.
37    pub fn rgba(&self) -> &[u8] {
38        bytemuck::cast_slice(&self.pixels)
39    }
40}
41
42impl Renderer for Frame {
43    type Err = Infallible;
44
45    fn render_frame(
46        &mut self,
47        frame_buffer: &[Color],
48        frame_size: FrameSize,
49        _target_fps: f64,
50        _options: RenderFrameOptions,
51    ) -> Result<(), Infallible> {
52        let len = (frame_size.width * frame_size.height) as usize;
53        self.size = (frame_size.width, frame_size.height);
54        self.pixels.clear();
55        self.pixels.extend_from_slice(&frame_buffer[..len]);
56        Ok(())
57    }
58}
59
60/// Stereo samples produced since the host last drained them.
61#[derive(Default)]
62pub struct Samples(pub Vec<(f64, f64)>);
63
64impl AudioOutput for Samples {
65    type Err = Infallible;
66
67    fn push_sample(&mut self, l: f64, r: f64) -> Result<(), Infallible> {
68        self.0.push((l, r));
69        Ok(())
70    }
71}
72
73/// Cartridge saves, keyed by the extension the core asks for (`sav`).
74///
75/// Held in memory because where a save lives is the host's decision: a file
76/// natively, IndexedDB on the web. The core only ever sees this.
77#[derive(Clone, Default)]
78pub struct Saves(pub HashMap<String, Vec<u8>>);
79
80#[derive(Debug)]
81pub enum SaveError {
82    Absent(String),
83    Encode(bincode::error::EncodeError),
84    Decode(bincode::error::DecodeError),
85}
86
87impl std::fmt::Display for SaveError {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            Self::Absent(ext) => write!(f, "no save with extension {ext}"),
91            Self::Encode(e) => write!(f, "encoding save: {e}"),
92            Self::Decode(e) => write!(f, "decoding save: {e}"),
93        }
94    }
95}
96
97impl SaveWriter for Saves {
98    type Err = SaveError;
99
100    fn load_bytes(&mut self, extension: &str) -> Result<Vec<u8>, SaveError> {
101        self.0.get(extension).cloned().ok_or_else(|| SaveError::Absent(extension.into()))
102    }
103
104    fn persist_bytes(&mut self, extension: &str, bytes: &[u8]) -> Result<(), SaveError> {
105        self.0.insert(extension.into(), bytes.to_vec());
106        Ok(())
107    }
108
109    fn load_serialized<D: Decode<()>>(&mut self, extension: &str) -> Result<D, SaveError> {
110        let bytes = self.load_bytes(extension)?;
111        bincode::decode_from_slice(&bytes, bincode::config::standard())
112            .map(|(value, _)| value)
113            .map_err(SaveError::Decode)
114    }
115
116    fn persist_serialized<E: Encode>(&mut self, extension: &str, data: E) -> Result<(), SaveError> {
117        let bytes = bincode::encode_to_vec(data, bincode::config::standard())
118            .map_err(SaveError::Encode)?;
119        self.0.insert(extension.into(), bytes);
120        Ok(())
121    }
122}
123
124struct Pad<'a>(&'a SnesInputs);
125
126impl InputPoller<SnesInputs> for Pad<'_> {
127    fn poll(&mut self) -> &SnesInputs {
128        self.0
129    }
130}
131
132/// The whole machine at a frame boundary, as bytes: everything but the ROM,
133/// which the cartridge already is.
134///
135/// It opens with the core's save-state version and the cartridge's CRC-32,
136/// because the rest is the core's own structs laid end to end: decoded by a
137/// different core, or onto a different cartridge, it would be a machine that
138/// never existed. [`Console::restore`] refuses both by name.
139#[derive(Debug, Encode, Decode, PartialEq, Eq)]
140struct SnapshotHeader {
141    core: String,
142    cartridge: u32,
143}
144
145#[derive(Debug)]
146pub enum SnapshotError {
147    Encode(bincode::error::EncodeError),
148    Decode(bincode::error::DecodeError),
149    OtherCore { snapshot: String, running: &'static str },
150    OtherCartridge { snapshot: u32, running: u32 },
151}
152
153impl std::fmt::Display for SnapshotError {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            Self::Encode(e) => write!(f, "encoding snapshot: {e}"),
157            Self::Decode(e) => write!(f, "decoding snapshot: {e}"),
158            Self::OtherCore { snapshot, running } => write!(
159                f,
160                "snapshot is from core save-state version {snapshot}, this is {running}"
161            ),
162            Self::OtherCartridge { snapshot, running } => write!(
163                f,
164                "snapshot is of cartridge {snapshot:08X}, this is {running:08X}"
165            ),
166        }
167    }
168}
169
170const CRC32: Crc<u32> = Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
171
172pub type TickError = <SnesEmulator as EmulatorTrait>::Err<Infallible, Infallible, SaveError>;
173
174pub struct Console {
175    emulator: SnesEmulator,
176    config: SnesEmulatorConfig,
177    /// CRC-32 of the cartridge image this was booted from.
178    cartridge: u32,
179    inputs: SnesInputs,
180    pub frame: Frame,
181    pub samples: Samples,
182    pub saves: Saves,
183    /// `None` until a host says it has a sound device; headless never does.
184    resampling: Option<DynamicResamplingRate>,
185}
186
187impl Console {
188    /// Boot a cartridge image. `saves` may already hold its `sav`.
189    pub fn boot(rom: Vec<u8>, mut saves: Saves) -> Result<Self, SnesLoadError> {
190        let config = SnesEmulatorConfig::default();
191        let cartridge = CRC32.checksum(&rom);
192        let emulator = SnesEmulator::create(rom, config, CoprocessorRoms::none(), &mut saves)?;
193        Ok(Self {
194            emulator,
195            config,
196            cartridge,
197            inputs: SnesInputs::default(),
198            frame: Frame::default(),
199            samples: Samples::default(),
200            saves,
201            resampling: None,
202        })
203    }
204
205    /// Run until the PPU finishes one frame.
206    pub fn run_frame(&mut self) -> Result<(), TickError> {
207        loop {
208            let effect = self.emulator.tick(
209                &mut self.frame,
210                &mut self.samples,
211                &mut Pad(&self.inputs),
212                &mut self.saves,
213            )?;
214            if effect == TickEffect::FrameRendered {
215                return Ok(());
216            }
217        }
218    }
219
220    /// Frames per second of the machine being emulated. NTSC is 60.0988, not
221    /// 60, so a host paces on this and never on its display's refresh rate.
222    pub fn target_fps(&self) -> f64 {
223        self.emulator.target_fps()
224    }
225
226    /// Say where the sound goes: a device running at `hz`, fed from a queue
227    /// the host tries to keep `target_queued` stereo frames deep. The core
228    /// resamples from the DSP's 32 kHz itself, so `samples` arrives at the
229    /// device's rate and the host never resamples.
230    pub fn audio_output(&mut self, hz: u32, target_queued: u32) {
231        self.emulator.update_audio_output_frequency(hz.into());
232        self.resampling = Some(DynamicResamplingRate::new(hz, target_queued));
233    }
234
235    /// Report how many stereo frames are waiting in the host's queue, once per
236    /// emulated frame. The console and the sound device run on two clocks that
237    /// never agree exactly, so a fixed rate slowly drains or floods the queue;
238    /// this bends the resampling rate by at most 0.5% toward the target depth,
239    /// which is inaudible and is what upstream's own frontends do.
240    pub fn audio_queued(&mut self, queued: u32) {
241        let Some(resampling) = &mut self.resampling else { return };
242        let before = resampling.current_output_frequency();
243        resampling.adjust(queued);
244        let after = resampling.current_output_frequency();
245        if after != before {
246            self.emulator.update_audio_output_frequency(after.into());
247        }
248    }
249
250    /// Hold or release a button on controller one.
251    pub fn set_button(&mut self, button: SnesButton, pressed: bool) {
252        self.inputs.p1.set_field(button, pressed);
253    }
254
255    /// Turn the machine off and on again. The cartridge's battery-backed save
256    /// is kept, as it would be.
257    pub fn power_cycle(&mut self) {
258        self.emulator.hard_reset(&mut self.saves);
259        // Upstream rebuilds the machine from the cartridge and its config, and
260        // the resampling rate is not part of either.
261        if let Some(resampling) = &self.resampling {
262            self.emulator.update_audio_output_frequency(resampling.current_output_frequency().into());
263        }
264    }
265
266    /// The machine as it stands, between frames. This is the format of every
267    /// state kept on disk (`resume`, `home`, the MCP tools' named states);
268    /// see [`Self::snapshot_fixed`] for the one that deltas well.
269    pub fn snapshot(&self) -> Result<Vec<u8>, SnapshotError> {
270        self.snapshot_with(bincode::config::standard())
271    }
272
273    /// Become the machine in `snapshot` (from [`Self::snapshot`]). On any
274    /// error this console is left exactly as it was.
275    pub fn restore(&mut self, snapshot: &[u8]) -> Result<(), SnapshotError> {
276        self.restore_with(snapshot, bincode::config::standard())
277    }
278
279    /// [`Self::snapshot`] with every integer written at its full width, so
280    /// the same field sits at the same offset in every snapshot of one
281    /// cartridge. `snapshot` uses bincode's varint integers, whose length
282    /// drifts as values cross 251 / 65536: 83 consecutive snapshots came in
283    /// 70 different lengths (apps/replay-probe, 2026-09-22), which defeats
284    /// any delta taken at fixed offsets. For `packages/replay`'s keyframes
285    /// only; the two formats do not read each other.
286    pub fn snapshot_fixed(&self) -> Result<Vec<u8>, SnapshotError> {
287        self.snapshot_with(bincode::config::standard().with_fixed_int_encoding())
288    }
289
290    /// Become the machine in `snapshot` (from [`Self::snapshot_fixed`]).
291    pub fn restore_fixed(&mut self, snapshot: &[u8]) -> Result<(), SnapshotError> {
292        self.restore_with(snapshot, bincode::config::standard().with_fixed_int_encoding())
293    }
294
295    fn snapshot_with<C: bincode::config::Config>(&self, config: C) -> Result<Vec<u8>, SnapshotError> {
296        let header = SnapshotHeader {
297            core: SnesEmulator::save_state_version().into(),
298            cartridge: self.cartridge,
299        };
300        let mut bytes = bincode::encode_to_vec(header, config).map_err(SnapshotError::Encode)?;
301        let machine = self.emulator.to_save_state();
302        bytes.extend(bincode::encode_to_vec(machine, config).map_err(SnapshotError::Encode)?);
303        Ok(bytes)
304    }
305
306    fn restore_with<C: bincode::config::Config>(&mut self, snapshot: &[u8], config: C) -> Result<(), SnapshotError> {
307        let (header, read): (SnapshotHeader, usize) =
308            bincode::decode_from_slice(snapshot, config).map_err(SnapshotError::Decode)?;
309        let running = SnesEmulator::save_state_version();
310        if header.core != running {
311            return Err(SnapshotError::OtherCore { snapshot: header.core, running });
312        }
313        if header.cartridge != self.cartridge {
314            return Err(SnapshotError::OtherCartridge {
315                snapshot: header.cartridge,
316                running: self.cartridge,
317            });
318        }
319        let (machine, _): (SnesEmulator, usize) =
320            bincode::decode_from_slice(&snapshot[read..], config)
321                .map_err(SnapshotError::Decode)?;
322        self.emulator.load_state(machine);
323        // The snapshot carries the config and the resampler of the machine it
324        // was taken from; this host's are the ones that apply.
325        self.emulator.reload_config(&self.config);
326        if let Some(resampling) = &self.resampling {
327            self.emulator.update_audio_output_frequency(resampling.current_output_frequency().into());
328        }
329        Ok(())
330    }
331
332    /// Work RAM, `$7E0000-$7FFFFF`, indexed from `$7E0000`.
333    pub fn wram(&self) -> &[u8; WRAM_LEN] {
334        self.emulator.wram()
335    }
336
337    /// Work RAM, writable, between frames. For a host that plays from RAM and
338    /// writes into it - the zbanks bot's cheats.
339    pub fn wram_mut(&mut self) -> &mut [u8; WRAM_LEN] {
340        self.emulator.wram_mut()
341    }
342}