diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index 6e51071513fc53e243cea2930d2bb01bde721cc7..62b394779ec564aba4822170c7e7e555d43643fe 100644 --- a/frontends/sdl/src/main.rs +++ b/frontends/sdl/src/main.rs @@ -13,6 +13,7 @@ use boytacean::{ ppu::{PaletteInfo, PpuMode}, rom::Cartridge, serial::{NullDevice, SerialDevice}, + util::{replace_ext, write_file}, }; use chrono::Utc; use clap::Parser; @@ -38,6 +39,10 @@ const TITLE: &str = "Boytacean"; /// amplification level of the volume const VOLUME: f32 = 64.0; +/// The rate (in seconds) at which the current battery +/// backed RAM is going to be stored into the file system. +const STORE_RATE: u8 = 5; + pub struct Benchmark { count: usize, cpu_only: Option<bool>, @@ -69,6 +74,7 @@ pub struct Emulator { audio: Option<Audio>, title: &'static str, rom_path: String, + ram_path: String, logic_frequency: u32, visual_frequency: f32, next_tick_time: f32, @@ -88,6 +94,7 @@ impl Emulator { audio: None, title: TITLE, rom_path: String::from("invalid"), + ram_path: String::from("invalid"), logic_frequency: GameBoy::CPU_FREQ, visual_frequency: GameBoy::VISUAL_FREQ, next_tick_time: 0.0, @@ -202,8 +209,16 @@ impl Emulator { } pub fn load_rom(&mut self, path: Option<&str>) { - let path_res = path.unwrap_or(&self.rom_path); - let rom = self.system.load_rom_file(path_res); + let rom_path: &str = path.unwrap_or(&self.rom_path); + let ram_path = replace_ext(rom_path, "sav").unwrap_or("invalid".to_string()); + let rom = self.system.load_rom_file( + rom_path, + if Path::new(&ram_path).exists() { + Some(&ram_path) + } else { + None + }, + ); println!( "========= Cartridge =========\n{}\n=============================", rom @@ -213,7 +228,8 @@ impl Emulator { .set_title(format!("{} [{}]", self.title, rom.title()).as_str()) .unwrap(); } - self.rom_path = String::from(path_res); + self.rom_path = String::from(rom_path); + self.ram_path = ram_path; } pub fn reset(&mut self) { @@ -305,6 +321,10 @@ impl Emulator { .create_texture_streaming(PixelFormatEnum::RGB24, width as u32, height as u32) .unwrap(); + // calculates the rate as visual cycles that will take from + // the current visual frequency to re-save the battery backed RAM + let store_count = (self.visual_frequency * STORE_RATE as f32).round() as u32; + // starts the variable that will control the number of cycles that // are going to move (because of overflow) from one tick to another let mut pending_cycles = 0u32; @@ -320,6 +340,14 @@ impl Emulator { // on the number of visual ticks since beginning counter = counter.wrapping_add(1); + // in case the current counter is a multiple of the store rate + // then we've reached the time to re-save the battery backed RAM + // into a *.sav file in the file system + if counter % store_count == 0 && self.system.rom().has_battery() { + let ram_data = self.system.ram_data_eager(); + write_file(&self.ram_path, ram_data); + } + // obtains an event from the SDL sub-system to be // processed under the current emulation context while let Some(event) = self.sdl.as_mut().unwrap().event_pump.poll_event() { diff --git a/src/gb.rs b/src/gb.rs index 87b2379fa0412c2186a5e3b463fc3c05904f5778..84191d0f22d2fcb27ed1b48a688687f5ea97e22d 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -642,7 +642,7 @@ impl GameBoy { } pub fn set_ram_data(&mut self, ram_data: Vec<u8>) { - self.mmu().rom().set_ram_data(ram_data) + self.mmu().rom().set_ram_data(&ram_data) } pub fn registers(&mut self) -> Registers { @@ -974,15 +974,24 @@ impl GameBoy { self.load_boot_file(BootRom::Cgb); } - pub fn load_rom(&mut self, data: &[u8]) -> &mut Cartridge { - let rom = Cartridge::from_data(data); + pub fn load_rom(&mut self, data: &[u8], ram_data: Option<&[u8]>) -> &mut Cartridge { + let mut rom = Cartridge::from_data(data); + if let Some(ram_data) = ram_data { + rom.set_ram_data(ram_data) + } self.mmu().set_rom(rom); self.mmu().rom() } - pub fn load_rom_file(&mut self, path: &str) -> &mut Cartridge { + pub fn load_rom_file(&mut self, path: &str, ram_path: Option<&str>) -> &mut Cartridge { let data = read_file(path); - self.load_rom(&data) + match ram_path { + Some(ram_path) => { + let ram_data = read_file(ram_path); + self.load_rom(&data, Some(&ram_data)) + } + None => self.load_rom(&data, None), + } } pub fn attach_serial(&mut self, device: Box<dyn SerialDevice>) { @@ -1006,7 +1015,7 @@ impl GameBoy { } pub fn load_rom_ws(&mut self, data: &[u8]) -> Cartridge { - let rom = self.load_rom(data); + let rom = self.load_rom(data, None); rom.set_rumble_cb(|active| { rumble_callback(active); }); diff --git a/src/rom.rs b/src/rom.rs index 90d0545d79e229ef11f8b7771710a8d63082443b..f793667fa3d84c9f5c63627effbd6ae50f3a08e6 100644 --- a/src/rom.rs +++ b/src/rom.rs @@ -587,12 +587,16 @@ impl Cartridge { ) } + pub fn rom_data_eager(&self) -> Vec<u8> { + self.rom_data.clone() + } + pub fn ram_data_eager(&self) -> Vec<u8> { self.ram_data.clone() } - pub fn set_ram_data(&mut self, ram_data: Vec<u8>) { - self.ram_data = ram_data; + pub fn set_ram_data(&mut self, data: &[u8]) { + self.ram_data = data.to_vec(); } pub fn description(&self, column_length: usize) -> String { diff --git a/src/test.rs b/src/test.rs index 43f4ab89ff59b349ed930df205036c2ff9dfe67e..ff38d784b70cb9379c89a7adf2e09d2b0b844ab0 100644 --- a/src/test.rs +++ b/src/test.rs @@ -30,7 +30,7 @@ pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) - let max_cycles = max_cycles.unwrap_or(u64::MAX); let mut game_boy = build_test(options); - game_boy.load_rom_file(rom_path); + game_boy.load_rom_file(rom_path, None); loop { cycles += game_boy.clock() as u64; diff --git a/src/util.rs b/src/util.rs index 96776eecc5b692c096fae2dabd603f119a988465..c73eff41fd7821316ab0e5e1e46bcccb98fe1559 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,10 @@ -use std::{cell::RefCell, fs::File, io::Read, rc::Rc}; +use std::{ + cell::RefCell, + fs::File, + io::{Read, Write}, + path::Path, + rc::Rc, +}; pub type SharedMut<T> = Rc<RefCell<T>>; @@ -11,3 +17,61 @@ pub fn read_file(path: &str) -> Vec<u8> { file.read_to_end(&mut data).unwrap(); data } + +pub fn write_file(path: &str, data: Vec<u8>) { + let mut file = match File::create(path) { + Ok(file) => file, + Err(_) => panic!("Failed to open file: {}", path), + }; + file.write_all(&data).unwrap() +} + +/// Replaces the extension in the given path with the provided extension. +/// This function allows for simple associated file discovery. +pub fn replace_ext(path: &str, new_extension: &str) -> Option<String> { + let file_path = Path::new(path); + let parent_dir = file_path.parent()?; + let file_stem = file_path.file_stem()?; + let file_extension = file_path.extension()?; + if file_stem == file_extension { + return None; + } + let new_file_name = format!("{}.{}", file_stem.to_str()?, new_extension); + let new_file_path = parent_dir.join(new_file_name); + Some(String::from(new_file_path.to_str()?)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_change_extension() { + let new_path = replace_ext("/path/to/file.txt", "dat").unwrap(); + assert_eq!( + new_path, + Path::new("/path/to").join("file.dat").to_str().unwrap() + ); + + let new_path = replace_ext("/path/to/file.with.multiple.dots.txt", "dat").unwrap(); + assert_eq!( + new_path, + Path::new("/path/to") + .join("file.with.multiple.dots.dat") + .to_str() + .unwrap() + ); + + let new_path = replace_ext("/path/to/file.without.extension", "dat").unwrap(); + assert_eq!( + new_path, + Path::new("/path/to") + .join("file.without.dat") + .to_str() + .unwrap() + ); + + let new_path = replace_ext("/path/to/directory/", "dat"); + assert_eq!(new_path, None); + } +}