1use crate::NativeEmulatorResult; 2use crate::mainloop::{NativeEmulatorError, bincode_config}; 3use jgenesis_common::frontend::EmulatorTrait; 4use std::ffi::OsStr; 5use std::fs::File; 6use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; 7use std::path::{Path, PathBuf}; 8use std::sync::mpsc::{Receiver, SendError, Sender}; 9use std::sync::{Arc, Mutex, mpsc}; 10use std::time::SystemTime; 11use std::{array, fs, thread}; 12 13pub const SAVE_STATE_SLOTS: usize = 10; 14pub const EXTENSION: &str = "jst"; 15 16const FILE_PREFIX: &[u8] = b"jgenstate"; 17 18// 000.111.222.333 19const MAX_VERSION_LEN: usize = 15; 20 21// Prefix + version 22const MAX_HEADER_LEN: usize = FILE_PREFIX.len() + MAX_VERSION_LEN; 23 24pub type SaveStatePaths = [PathBuf; SAVE_STATE_SLOTS]; 25 26pub fn init_paths(path: &Path) -> NativeEmulatorResult<[PathBuf; SAVE_STATE_SLOTS]> { 27 let path_no_ext = path.with_extension(""); 28 let file_name = path_no_ext 29 .file_name() 30 .and_then(OsStr::to_str) 31 .ok_or_else(|| NativeEmulatorError::ParseFileName(path.display().to_string()))?; 32 33 let file_names: [_; SAVE_STATE_SLOTS] = 34 array::from_fn(|i| format!("{file_name}_{i}.{EXTENSION}")); 35 36 Ok(file_names.map(|name| path.with_file_name(name))) 37} 38 39#[derive(Debug, Clone, Default)] 40pub struct SaveStateMetadata { 41 pub times_nanos: [Option<u128>; SAVE_STATE_SLOTS], 42} 43 44impl SaveStateMetadata { 45 pub(crate) fn load(paths: &SaveStatePaths, version: &str) -> Self { 46 let times_nanos = array::from_fn(|i| { 47 let header_version = read_version_from_header(&paths[i]); 48 if header_version.as_deref() != Some(version) { 49 return None; 50 } 51 52 let metadata = fs::metadata(&paths[i]).ok()?; 53 let modified = metadata.modified().ok()?; 54 Some(modified.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos()) 55 }); 56 57 Self { times_nanos } 58 } 59} 60 61fn read_version_from_header(path: &Path) -> Option<String> { 62 let mut file = File::open(path).ok()?; 63 64 let mut buffer = [0_u8; MAX_HEADER_LEN]; 65 file.read_exact(&mut buffer).ok()?; 66 67 if &buffer[..FILE_PREFIX.len()] != FILE_PREFIX { 68 return None; 69 } 70 71 read_version_from_buffer(&buffer) 72} 73 74fn read_version_from_buffer(buffer: &[u8]) -> Option<String> { 75 let version_len = buffer[FILE_PREFIX.len()] as usize; 76 if !(1..=MAX_VERSION_LEN).contains(&version_len) { 77 return None; 78 } 79 80 let version_bytes = &buffer[FILE_PREFIX.len() + 1..FILE_PREFIX.len() + 1 + version_len]; 81 String::from_utf8(version_bytes.to_vec()).ok() 82} 83 84pub struct SaveStateRequest<State> { 85 pub paths: SaveStatePaths, 86 pub slot: usize, 87 pub state: Box<State>, 88 pub metadata: Arc<Mutex<SaveStateMetadata>>, 89} 90 91pub struct SaveStateResponse { 92 pub slot: usize, 93} 94 95pub type SaveStateResult = Result<SaveStateResponse, (NativeEmulatorError, SaveStateResponse)>; 96 97pub struct StateSaverThreadHandle<Emulator: EmulatorTrait> { 98 request_sender: Sender<SaveStateRequest<Emulator::SaveState>>, 99 response_receiver: Receiver<SaveStateResult>, 100} 101 102impl<Emulator: EmulatorTrait> StateSaverThreadHandle<Emulator> { 103 pub fn send_save_request( 104 &self, 105 emulator: &Emulator, 106 paths: &SaveStatePaths, 107 slot: usize, 108 metadata: Arc<Mutex<SaveStateMetadata>>, 109 ) -> Result<(), SendError<()>> { 110 let state = Box::new(emulator.to_save_state()); 111 112 self.request_sender 113 .send(SaveStateRequest { paths: paths.clone(), slot, state, metadata }) 114 .map_err(|_| SendError(())) 115 } 116 117 pub fn try_recv_save_response(&self) -> Option<SaveStateResult> { 118 self.response_receiver.try_recv().ok() 119 } 120} 121 122pub fn spawn_state_saver_thread<Emulator: EmulatorTrait>() -> StateSaverThreadHandle<Emulator> { 123 let (request_sender, request_receiver) = mpsc::channel(); 124 let (response_sender, response_receiver) = mpsc::channel(); 125 126 thread::spawn(move || run_state_saver_thread::<Emulator>(request_receiver, response_sender)); 127 128 StateSaverThreadHandle { request_sender, response_receiver } 129} 130 131fn run_state_saver_thread<Emulator: EmulatorTrait>( 132 request_receiver: Receiver<SaveStateRequest<Emulator::SaveState>>, 133 response_sender: Sender<SaveStateResult>, 134) { 135 loop { 136 let Ok(request) = request_receiver.recv() else { 137 // Runner thread has dropped sender; stop running 138 return; 139 }; 140 141 let save_result = 142 save::<Emulator>(&request.state, &request.paths, request.slot, &request.metadata); 143 144 let response = SaveStateResponse { slot: request.slot }; 145 let result = match save_result { 146 Ok(()) => Ok(response), 147 Err(err) => Err((err, response)), 148 }; 149 150 if response_sender.send(result).is_err() { 151 // Runner thread has dropped receiver; stop running 152 return; 153 } 154 } 155} 156 157pub fn save<Emulator: EmulatorTrait>( 158 state: &Emulator::SaveState, 159 paths: &SaveStatePaths, 160 slot: usize, 161 metadata: &Mutex<SaveStateMetadata>, 162) -> NativeEmulatorResult<()> { 163 let current_version = Emulator::save_state_version(); 164 assert!( 165 current_version.len() <= MAX_VERSION_LEN, 166 "save state version is '{current_version}' (len {}), len must be at most {MAX_VERSION_LEN}", 167 current_version.len() 168 ); 169 170 let path = &paths[slot]; 171 let file = File::create(path).map_err(|source| NativeEmulatorError::StateFileOpen { 172 path: path.display().to_string(), 173 source, 174 })?; 175 176 let mut writer = BufWriter::new(file); 177 writer.write_all(FILE_PREFIX).map_err(NativeEmulatorError::SaveStateIo)?; 178 179 let version_len = current_version.len() as u8; 180 writer.write_all(&[version_len]).map_err(NativeEmulatorError::SaveStateIo)?; 181 writer.write_all(current_version.as_bytes()).map_err(NativeEmulatorError::SaveStateIo)?; 182 183 let mut encoder = zstd::Encoder::new(writer, 0).map_err(NativeEmulatorError::SaveStateIo)?; 184 bincode::encode_into_std_write(state, &mut encoder, bincode_config!())?; 185 encoder.finish().map_err(NativeEmulatorError::SaveStateIo)?; 186 187 let now_nanos = 188 SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default().as_nanos(); 189 { 190 let mut metadata = metadata.lock().unwrap(); 191 metadata.times_nanos[slot] = Some(now_nanos); 192 } 193 194 Ok(()) 195} 196 197pub fn load<Emulator: EmulatorTrait>( 198 emulator: &mut Emulator, 199 config: &Emulator::Config, 200 paths: &SaveStatePaths, 201 slot: usize, 202) -> NativeEmulatorResult<()> { 203 let path = &paths[slot]; 204 let file = File::open(path).map_err(|source| NativeEmulatorError::StateFileOpen { 205 path: path.display().to_string(), 206 source, 207 })?; 208 209 let mut reader = BufReader::new(file); 210 let mut header_buffer = [0_u8; MAX_HEADER_LEN]; 211 reader.read_exact(&mut header_buffer).map_err(NativeEmulatorError::LoadStateIo)?; 212 213 if &header_buffer[..FILE_PREFIX.len()] != FILE_PREFIX { 214 return Err(NativeEmulatorError::LoadStatePrefixMismatch); 215 } 216 217 let current_version = Emulator::save_state_version(); 218 let version_in_header = read_version_from_buffer(&header_buffer) 219 .ok_or(NativeEmulatorError::LoadStatePrefixMismatch)?; 220 221 if version_in_header.as_str() != current_version { 222 return Err(NativeEmulatorError::LoadStateVersionMismatch { 223 expected: current_version.into(), 224 actual: version_in_header, 225 }); 226 } 227 228 let total_header_len = (FILE_PREFIX.len() + 1 + current_version.len()) as u64; 229 reader.seek(SeekFrom::Start(total_header_len)).map_err(NativeEmulatorError::SaveStateIo)?; 230 let mut decoder = zstd::Decoder::new(reader).map_err(NativeEmulatorError::LoadStateIo)?; 231 let loaded_state: Emulator::SaveState = 232 bincode::decode_from_std_read(&mut decoder, bincode_config!())?; 233 234 emulator.load_state(loaded_state); 235 emulator.reload_config(config); 236 237 Ok(()) 238}