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.
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.
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.
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.
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.
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.
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.
None until a host says it has a sound device; headless never does.
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.
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.
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.
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.
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.
Become the machine in snapshot (from [Self::snapshot]). On any
error this console is left exactly as it was.
[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.
Become the machine in snapshot (from [Self::snapshot_fixed]).
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.