Skip to content
Snippets Groups Projects
Commit addcd6ea authored by João Magalhães's avatar João Magalhães :rocket:
Browse files

Merge branch 'joamag/ram-loading' into 'master'

Support for .sav RAM files loading in SDL

See merge request !28
parents aaa99da6 b54df7f5
No related branches found
No related tags found
1 merge request!28Support for .sav RAM files loading in SDL
Pipeline #3046 passed
...@@ -13,6 +13,7 @@ use boytacean::{ ...@@ -13,6 +13,7 @@ use boytacean::{
ppu::{PaletteInfo, PpuMode}, ppu::{PaletteInfo, PpuMode},
rom::Cartridge, rom::Cartridge,
serial::{NullDevice, SerialDevice}, serial::{NullDevice, SerialDevice},
util::{replace_ext, write_file},
}; };
use chrono::Utc; use chrono::Utc;
use clap::Parser; use clap::Parser;
...@@ -38,6 +39,10 @@ const TITLE: &str = "Boytacean"; ...@@ -38,6 +39,10 @@ const TITLE: &str = "Boytacean";
/// amplification level of the volume /// amplification level of the volume
const VOLUME: f32 = 64.0; 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 { pub struct Benchmark {
count: usize, count: usize,
cpu_only: Option<bool>, cpu_only: Option<bool>,
...@@ -69,6 +74,7 @@ pub struct Emulator { ...@@ -69,6 +74,7 @@ pub struct Emulator {
audio: Option<Audio>, audio: Option<Audio>,
title: &'static str, title: &'static str,
rom_path: String, rom_path: String,
ram_path: String,
logic_frequency: u32, logic_frequency: u32,
visual_frequency: f32, visual_frequency: f32,
next_tick_time: f32, next_tick_time: f32,
...@@ -88,6 +94,7 @@ impl Emulator { ...@@ -88,6 +94,7 @@ impl Emulator {
audio: None, audio: None,
title: TITLE, title: TITLE,
rom_path: String::from("invalid"), rom_path: String::from("invalid"),
ram_path: String::from("invalid"),
logic_frequency: GameBoy::CPU_FREQ, logic_frequency: GameBoy::CPU_FREQ,
visual_frequency: GameBoy::VISUAL_FREQ, visual_frequency: GameBoy::VISUAL_FREQ,
next_tick_time: 0.0, next_tick_time: 0.0,
...@@ -202,8 +209,16 @@ impl Emulator { ...@@ -202,8 +209,16 @@ impl Emulator {
} }
pub fn load_rom(&mut self, path: Option<&str>) { pub fn load_rom(&mut self, path: Option<&str>) {
let path_res = path.unwrap_or(&self.rom_path); let rom_path: &str = path.unwrap_or(&self.rom_path);
let rom = self.system.load_rom_file(path_res); 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!( println!(
"========= Cartridge =========\n{}\n=============================", "========= Cartridge =========\n{}\n=============================",
rom rom
...@@ -213,7 +228,8 @@ impl Emulator { ...@@ -213,7 +228,8 @@ impl Emulator {
.set_title(format!("{} [{}]", self.title, rom.title()).as_str()) .set_title(format!("{} [{}]", self.title, rom.title()).as_str())
.unwrap(); .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) { pub fn reset(&mut self) {
...@@ -305,6 +321,10 @@ impl Emulator { ...@@ -305,6 +321,10 @@ impl Emulator {
.create_texture_streaming(PixelFormatEnum::RGB24, width as u32, height as u32) .create_texture_streaming(PixelFormatEnum::RGB24, width as u32, height as u32)
.unwrap(); .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 // starts the variable that will control the number of cycles that
// are going to move (because of overflow) from one tick to another // are going to move (because of overflow) from one tick to another
let mut pending_cycles = 0u32; let mut pending_cycles = 0u32;
...@@ -320,6 +340,14 @@ impl Emulator { ...@@ -320,6 +340,14 @@ impl Emulator {
// on the number of visual ticks since beginning // on the number of visual ticks since beginning
counter = counter.wrapping_add(1); 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 // obtains an event from the SDL sub-system to be
// processed under the current emulation context // processed under the current emulation context
while let Some(event) = self.sdl.as_mut().unwrap().event_pump.poll_event() { while let Some(event) = self.sdl.as_mut().unwrap().event_pump.poll_event() {
......
...@@ -642,7 +642,7 @@ impl GameBoy { ...@@ -642,7 +642,7 @@ impl GameBoy {
} }
pub fn set_ram_data(&mut self, ram_data: Vec<u8>) { 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 { pub fn registers(&mut self) -> Registers {
...@@ -974,15 +974,24 @@ impl GameBoy { ...@@ -974,15 +974,24 @@ impl GameBoy {
self.load_boot_file(BootRom::Cgb); self.load_boot_file(BootRom::Cgb);
} }
pub fn load_rom(&mut self, data: &[u8]) -> &mut Cartridge { pub fn load_rom(&mut self, data: &[u8], ram_data: Option<&[u8]>) -> &mut Cartridge {
let rom = Cartridge::from_data(data); 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().set_rom(rom);
self.mmu().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); 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>) { pub fn attach_serial(&mut self, device: Box<dyn SerialDevice>) {
...@@ -1006,7 +1015,7 @@ impl GameBoy { ...@@ -1006,7 +1015,7 @@ impl GameBoy {
} }
pub fn load_rom_ws(&mut self, data: &[u8]) -> Cartridge { 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| { rom.set_rumble_cb(|active| {
rumble_callback(active); rumble_callback(active);
}); });
......
...@@ -587,12 +587,16 @@ impl Cartridge { ...@@ -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> { pub fn ram_data_eager(&self) -> Vec<u8> {
self.ram_data.clone() self.ram_data.clone()
} }
pub fn set_ram_data(&mut self, ram_data: Vec<u8>) { pub fn set_ram_data(&mut self, data: &[u8]) {
self.ram_data = ram_data; self.ram_data = data.to_vec();
} }
pub fn description(&self, column_length: usize) -> String { pub fn description(&self, column_length: usize) -> String {
......
...@@ -30,7 +30,7 @@ pub fn run_test(rom_path: &str, max_cycles: Option<u64>, options: TestOptions) - ...@@ -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 max_cycles = max_cycles.unwrap_or(u64::MAX);
let mut game_boy = build_test(options); let mut game_boy = build_test(options);
game_boy.load_rom_file(rom_path); game_boy.load_rom_file(rom_path, None);
loop { loop {
cycles += game_boy.clock() as u64; cycles += game_boy.clock() as u64;
......
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>>; pub type SharedMut<T> = Rc<RefCell<T>>;
...@@ -11,3 +17,61 @@ pub fn read_file(path: &str) -> Vec<u8> { ...@@ -11,3 +17,61 @@ pub fn read_file(path: &str) -> Vec<u8> {
file.read_to_end(&mut data).unwrap(); file.read_to_end(&mut data).unwrap();
data 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);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment