lib.rsannotatedlib.rssource342 lines · 12.8 KB · raw

An SNES, embedded.

snes-core is written to be driven by a frontend: every tick it is handed somewhere to draw, somewhere to play sound, somewhere to keep saves, and something to ask about the controller. [Console] owns all four, so the rest of this project sees a machine with a screen, a pad and RAM, and can read and write them between frames — which is the whole reason the emulator is a library here instead of a process beside us.

10use std::collections::HashMap;
11use std::convert::Infallible;
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;

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}
33impl Frame {

The picture as width * height * 4 bytes of RGBA, which is what every texture upload wants. Color is repr(C) and Pod, so this is a view of the same memory and not a conversion.

37    pub fn rgba(&self) -> &[u8] {
38        bytemuck::cast_slice(&self.pixels)
39    }
40}
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}

Stereo samples produced since the host last drained them.

61#[derive(Default)]
62pub struct Samples(pub Vec<(f64, f64)>);
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}

Cartridge saves, keyed by the extension the core asks for (sav).

Held in memory because where a save lives is the host's decision: a file natively, IndexedDB on the web. The core only ever sees this.

77#[derive(Clone, Default)]
78pub struct Saves(pub HashMap<String, Vec<u8>>);
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}

The whole machine at a frame boundary, as bytes: everything but the ROM, which the cartridge already is.

It opens with the core's save-state version and the cartridge's CRC-32, because the rest is the core's own structs laid end to end: decoded by a different core, or onto a different cartridge, it would be a machine that never existed. [Console::restore] refuses both by name.

139#[derive(Debug, Encode, Decode, PartialEq, Eq)]
140struct SnapshotHeader {
141    core: String,
142    cartridge: u32,
143}
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,

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,

None until a host says it has a sound device; headless never does.

184    resampling: Option<DynamicResamplingRate>,
185}
187impl Console {

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    }

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    }

Frames per second of the machine being emulated. NTSC is 60.0988, not 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    }

Say where the sound goes: a device running at hz, fed from a queue the host tries to keep target_queued stereo frames deep. The core resamples from the DSP's 32 kHz itself, so samples arrives at the 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    }

Report how many stereo frames are waiting in the host's queue, once per emulated frame. The console and the sound device run on two clocks that never agree exactly, so a fixed rate slowly drains or floods the queue; this bends the resampling rate by at most 0.5% toward the target depth, 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    }

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    }

Turn the machine off and on again. The cartridge's battery-backed save 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    }

The machine as it stands, between frames. This is the format of every state kept on disk (resume, home, the MCP tools' named states); 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    }

Become the machine in snapshot (from [Self::snapshot]). On any 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    }

[Self::snapshot] with every integer written at its full width, so the same field sits at the same offset in every snapshot of one cartridge. snapshot uses bincode's varint integers, whose length drifts as values cross 251 / 65536: 83 consecutive snapshots came in 70 different lengths (apps/replay-probe, 2026-09-22), which defeats any delta taken at fixed offsets. For packages/replay's keyframes 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    }

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    }
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    }

Work RAM, $7E0000-$7FFFFF, indexed from $7E0000.

333    pub fn wram(&self) -> &[u8; WRAM_LEN] {
334        self.emulator.wram()
335    }

Work RAM, writable, between frames. For a host that plays from RAM and 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}