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}