feat: implement cartridge saving
Implemented for MBC1, MBC2, MBC3 and MBC5
This commit is contained in:
		
							
								
								
									
										49
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										49
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -592,6 +592,27 @@ dependencies = [ | ||||
|  "syn", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "directories-next" | ||||
| version = "2.0.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" | ||||
| dependencies = [ | ||||
|  "cfg-if 1.0.0", | ||||
|  "dirs-sys-next", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "dirs-sys-next" | ||||
| version = "0.1.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" | ||||
| dependencies = [ | ||||
|  "libc", | ||||
|  "redox_users", | ||||
|  "winapi", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "discard" | ||||
| version = "1.0.4" | ||||
| @@ -671,6 +692,7 @@ dependencies = [ | ||||
|  "anyhow", | ||||
|  "bitfield", | ||||
|  "clap", | ||||
|  "directories-next", | ||||
|  "gilrs", | ||||
|  "pixels", | ||||
|  "rodio", | ||||
| @@ -679,6 +701,17 @@ dependencies = [ | ||||
|  "winit_input_helper", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "getrandom" | ||||
| version = "0.2.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" | ||||
| dependencies = [ | ||||
|  "cfg-if 1.0.0", | ||||
|  "libc", | ||||
|  "wasi", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "gilrs" | ||||
| version = "0.8.1" | ||||
| @@ -1493,6 +1526,16 @@ dependencies = [ | ||||
|  "bitflags", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "redox_users" | ||||
| version = "0.4.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" | ||||
| dependencies = [ | ||||
|  "getrandom", | ||||
|  "redox_syscall", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "regex" | ||||
| version = "1.5.4" | ||||
| @@ -1916,6 +1959,12 @@ dependencies = [ | ||||
|  "winapi-util", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "wasi" | ||||
| version = "0.10.2+wasi-snapshot-preview1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" | ||||
|  | ||||
| [[package]] | ||||
| name = "wasm-bindgen" | ||||
| version = "0.2.77" | ||||
|   | ||||
| @@ -17,6 +17,7 @@ winit = "^0.25" | ||||
| winit_input_helper = "^0.10" | ||||
| rodio = "^0.14" | ||||
| rtrb = "^0.1.4" | ||||
| directories-next = "2.0.0" | ||||
|  | ||||
| [profile.release] | ||||
| debug = true | ||||
|   | ||||
							
								
								
									
										10
									
								
								src/bus.rs
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/bus.rs
									
									
									
									
									
								
							| @@ -90,6 +90,16 @@ impl Bus { | ||||
|     pub(crate) fn joyp_mut(&mut self) -> &mut Joypad { | ||||
|         &mut self.joypad | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn cart(&self) -> Option<&Cartridge> { | ||||
|         self.cart.as_ref() | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub(crate) fn cart_mut(&mut self) -> Option<&mut Cartridge> { | ||||
|         self.cart.as_mut() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Bus { | ||||
|   | ||||
							
								
								
									
										153
									
								
								src/cartridge.rs
									
									
									
									
									
								
							
							
						
						
									
										153
									
								
								src/cartridge.rs
									
									
									
									
									
								
							| @@ -24,6 +24,14 @@ impl Cartridge { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn ext_ram(&self) -> Option<&[u8]> { | ||||
|         self.mbc.ext_ram() | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn write_ext_ram(&mut self, memory: Vec<u8>) { | ||||
|         self.mbc.write_ext_ram(memory) | ||||
|     } | ||||
|  | ||||
|     fn detect_mbc(memory: &[u8]) -> Box<dyn MBCIo> { | ||||
|         let ram_size = Self::detect_ram_info(memory); | ||||
|         let rom_size = Self::detect_rom_info(memory); | ||||
| @@ -38,13 +46,13 @@ impl Cartridge { | ||||
|         match mbc_kind { | ||||
|             MBCKind::None => Box::new(NoMBC), | ||||
|             MBCKind::MBC1 => Box::new(MBC1::new(ram_size, rom_size)), | ||||
|             MBCKind::MBC1WithBattery => Box::new(MBC1::new(ram_size, rom_size)), // TODO: Implement Saving | ||||
|             MBCKind::MBC1WithBattery => Box::new(MBC1::with_battery(ram_size, rom_size)), // TODO: Implement Saving | ||||
|             MBCKind::MBC2 => Box::new(MBC2::new(rom_cap)), | ||||
|             MBCKind::MBC2WithBattery => Box::new(MBC2::new(rom_cap)), // TODO: Implement Saving | ||||
|             MBCKind::MBC2WithBattery => Box::new(MBC2::with_battery(rom_cap)), // TODO: Implement Saving | ||||
|             MBCKind::MBC3 => Box::new(MBC3::new(ram_cap)), | ||||
|             MBCKind::MBC3WithBattery => Box::new(MBC3::new(ram_cap)), // TODO: Implement Saving | ||||
|             MBCKind::MBC3WithBattery => Box::new(MBC3::with_battery(ram_cap)), // TODO: Implement Saving | ||||
|             MBCKind::MBC5 => Box::new(MBC5::new(ram_cap, rom_cap)), | ||||
|             MBCKind::MBC5WithBattery => Box::new(MBC5::new(ram_cap, rom_cap)), // TDO: Implement Saving | ||||
|             MBCKind::MBC5WithBattery => Box::new(MBC5::with_battery(ram_cap, rom_cap)), // TDO: Implement Saving | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -117,6 +125,8 @@ struct MBC1 { | ||||
|     memory: Vec<u8>, | ||||
|     rom_size: RomSize, | ||||
|     mem_enabled: bool, | ||||
|  | ||||
|     has_battery: bool, | ||||
| } | ||||
|  | ||||
| impl MBC1 { | ||||
| @@ -129,6 +139,20 @@ impl MBC1 { | ||||
|             ram_bank: Default::default(), | ||||
|             mode: Default::default(), | ||||
|             mem_enabled: Default::default(), | ||||
|             has_battery: Default::default(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn with_battery(ram_size: RamSize, rom_size: RomSize) -> Self { | ||||
|         Self { | ||||
|             rom_bank: 0x01, | ||||
|             memory: vec![0; ram_size.capacity() as usize], | ||||
|             ram_size, | ||||
|             rom_size, | ||||
|             ram_bank: Default::default(), | ||||
|             mode: Default::default(), | ||||
|             mem_enabled: Default::default(), | ||||
|             has_battery: true, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -197,6 +221,21 @@ impl MBC1 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Savable for MBC1 { | ||||
|     fn ext_ram(&self) -> Option<&[u8]> { | ||||
|         match self.has_battery { | ||||
|             true => Some(&self.memory), | ||||
|             false => None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn write_ext_ram(&mut self, memory: Vec<u8>) { | ||||
|         if self.has_battery { | ||||
|             self.memory.copy_from_slice(&memory); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl MBCIo for MBC1 { | ||||
|     fn handle_read(&self, addr: u16) -> MBCResult { | ||||
|         use MBCResult::*; | ||||
| @@ -256,6 +295,8 @@ struct MBC3 { | ||||
|  | ||||
|     // RTC Data Latch Previous Write | ||||
|     prev_latch_write: Option<u8>, | ||||
|  | ||||
|     has_battery: bool, | ||||
| } | ||||
|  | ||||
| impl MBC3 { | ||||
| @@ -267,6 +308,34 @@ impl MBC3 { | ||||
|             devs_enabled: Default::default(), | ||||
|             mapped: Default::default(), | ||||
|             prev_latch_write: Default::default(), | ||||
|             has_battery: Default::default(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn with_battery(ram_cap: usize) -> Self { | ||||
|         Self { | ||||
|             memory: vec![0; ram_cap], | ||||
|             rom_bank: Default::default(), | ||||
|             ram_bank: Default::default(), | ||||
|             devs_enabled: Default::default(), | ||||
|             mapped: Default::default(), | ||||
|             prev_latch_write: Default::default(), | ||||
|             has_battery: true, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Savable for MBC3 { | ||||
|     fn ext_ram(&self) -> Option<&[u8]> { | ||||
|         match self.has_battery { | ||||
|             true => Some(&self.memory), | ||||
|             false => None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn write_ext_ram(&mut self, memory: Vec<u8>) { | ||||
|         if self.has_battery { | ||||
|             self.memory.copy_from_slice(&memory); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -342,9 +411,10 @@ struct MBC5 { | ||||
|     ram_bank: u8, | ||||
|  | ||||
|     rom_cap: usize, | ||||
|  | ||||
|     memory: Vec<u8>, | ||||
|     mem_enabled: bool, | ||||
|  | ||||
|     has_battery: bool, | ||||
| } | ||||
|  | ||||
| impl MBC5 { | ||||
| @@ -355,6 +425,18 @@ impl MBC5 { | ||||
|             rom_cap, | ||||
|             ram_bank: Default::default(), | ||||
|             mem_enabled: Default::default(), | ||||
|             has_battery: Default::default(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn with_battery(ram_cap: usize, rom_cap: usize) -> Self { | ||||
|         Self { | ||||
|             rom_bank: 0x01, | ||||
|             memory: vec![0; ram_cap], | ||||
|             rom_cap, | ||||
|             ram_bank: Default::default(), | ||||
|             mem_enabled: Default::default(), | ||||
|             has_battery: true, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -363,6 +445,21 @@ impl MBC5 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Savable for MBC5 { | ||||
|     fn ext_ram(&self) -> Option<&[u8]> { | ||||
|         match self.has_battery { | ||||
|             true => Some(&self.memory), | ||||
|             false => None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn write_ext_ram(&mut self, memory: Vec<u8>) { | ||||
|         if self.has_battery { | ||||
|             self.memory.copy_from_slice(&memory); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl MBCIo for MBC5 { | ||||
|     fn handle_read(&self, addr: u16) -> MBCResult { | ||||
|         use MBCResult::*; | ||||
| @@ -401,6 +498,7 @@ struct MBC2 { | ||||
|     mem_enabled: bool, | ||||
|  | ||||
|     rom_cap: usize, | ||||
|     has_battery: bool, | ||||
| } | ||||
|  | ||||
| impl MBC2 { | ||||
| @@ -410,8 +508,19 @@ impl MBC2 { | ||||
|         Self { | ||||
|             rom_bank: 0x01, | ||||
|             memory: Box::new([0; Self::RAM_SIZE]), | ||||
|             mem_enabled: Default::default(), | ||||
|             rom_cap, | ||||
|             mem_enabled: Default::default(), | ||||
|             has_battery: Default::default(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn with_battery(rom_cap: usize) -> Self { | ||||
|         Self { | ||||
|             rom_bank: 0x01, | ||||
|             memory: Box::new([0; Self::RAM_SIZE]), | ||||
|             rom_cap, | ||||
|             mem_enabled: Default::default(), | ||||
|             has_battery: true, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -420,6 +529,21 @@ impl MBC2 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Savable for MBC2 { | ||||
|     fn ext_ram(&self) -> Option<&[u8]> { | ||||
|         match self.has_battery { | ||||
|             true => Some(self.memory.as_ref()), | ||||
|             false => None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn write_ext_ram(&mut self, memory: Vec<u8>) { | ||||
|         if self.has_battery { | ||||
|             self.memory.copy_from_slice(&memory); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl MBCIo for MBC2 { | ||||
|     fn handle_read(&self, addr: u16) -> MBCResult { | ||||
|         use MBCResult::*; | ||||
| @@ -457,6 +581,16 @@ impl MBCIo for MBC2 { | ||||
| #[derive(Debug)] | ||||
| struct NoMBC; | ||||
|  | ||||
| impl Savable for NoMBC { | ||||
|     fn ext_ram(&self) -> Option<&[u8]> { | ||||
|         None | ||||
|     } | ||||
|  | ||||
|     fn write_ext_ram(&mut self, _memory: Vec<u8>) { | ||||
|         // Nothing Happens Here | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl MBCIo for NoMBC { | ||||
|     fn handle_read(&self, addr: u16) -> MBCResult { | ||||
|         MBCResult::Address(addr as usize) | ||||
| @@ -467,7 +601,7 @@ impl MBCIo for NoMBC { | ||||
|     } | ||||
| } | ||||
|  | ||||
| trait MBCIo { | ||||
| trait MBCIo: Savable { | ||||
|     fn handle_read(&self, addr: u16) -> MBCResult; | ||||
|     fn handle_write(&mut self, addr: u16, byte: u8); | ||||
| } | ||||
| @@ -607,3 +741,8 @@ impl Default for Box<dyn MBCIo> { | ||||
|         Box::new(NoMBC) | ||||
|     } | ||||
| } | ||||
|  | ||||
| trait Savable { | ||||
|     fn ext_ram(&self) -> Option<&[u8]>; | ||||
|     fn write_ext_ram(&mut self, memory: Vec<u8>); | ||||
| } | ||||
|   | ||||
							
								
								
									
										51
									
								
								src/emu.rs
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								src/emu.rs
									
									
									
									
									
								
							| @@ -2,7 +2,11 @@ use crate::apu::gen::SampleProducer; | ||||
| use crate::cpu::Cpu; | ||||
| use crate::joypad::{self, Joypad}; | ||||
| use crate::{Cycle, GB_HEIGHT, GB_WIDTH}; | ||||
| use clap::crate_name; | ||||
| use gilrs::Gilrs; | ||||
| use std::fs::File; | ||||
| use std::io::{Read, Write}; | ||||
| use std::path::PathBuf; | ||||
| use std::time::Duration; | ||||
| use winit_input_helper::WinitInputHelper; | ||||
|  | ||||
| @@ -50,7 +54,7 @@ impl Emulator { | ||||
|     } | ||||
|  | ||||
|     fn load_cart(&mut self, rom: Vec<u8>) { | ||||
|         self.cpu.bus_mut().load_cart(rom) | ||||
|         self.cpu.bus_mut().load_cart(rom); | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
| @@ -65,6 +69,51 @@ impl Emulator { | ||||
|     pub fn title(&self) -> &str { | ||||
|         self.cpu.bus().cart_title().unwrap_or(DEFAULT_TITLE) | ||||
|     } | ||||
|  | ||||
|     pub fn try_write_sav(&self) -> std::io::Result<()> { | ||||
|         if let Some(ext_ram) = self.cpu.bus().cart().map(|c| c.ext_ram()).flatten() { | ||||
|             if let Some(title) = self.cpu.bus().cart_title() { | ||||
|                 let mut save_path = Self::data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||
|                 save_path.push(title); | ||||
|                 save_path.set_extension("sav"); | ||||
|  | ||||
|                 let mut file = File::create(save_path)?; | ||||
|                 file.write_all(ext_ram)?; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn try_load_sav(&mut self) -> std::io::Result<()> { | ||||
|         if let Some(cart) = self.cpu.bus_mut().cart_mut() { | ||||
|             if let Some(title) = cart.title() { | ||||
|                 let mut save_path = Self::data_path().unwrap_or_else(|| PathBuf::from(".")); | ||||
|                 save_path.push(title); | ||||
|                 save_path.set_extension("sav"); | ||||
|  | ||||
|                 if let Ok(mut file) = File::open(save_path) { | ||||
|                     let mut memory = Vec::new(); | ||||
|                     file.read_to_end(&mut memory)?; | ||||
|  | ||||
|                     cart.write_ext_ram(memory); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn data_path() -> Option<PathBuf> { | ||||
|         match directories_next::ProjectDirs::from("dev", "musuka", crate_name!()) { | ||||
|             Some(dirs) => { | ||||
|                 let data_local = dirs.data_local_dir(); | ||||
|                 std::fs::create_dir_all(data_local).ok()?; | ||||
|                 Some(data_local.to_path_buf()) | ||||
|             } | ||||
|             None => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub mod build { | ||||
|   | ||||
| @@ -49,6 +49,9 @@ fn main() -> Result<()> { | ||||
|     } | ||||
|  | ||||
|     let mut emu = emu_build.finish(); | ||||
|  | ||||
|     // Load Save file if it exists | ||||
|     emu.try_load_sav().expect("Load save if exists"); | ||||
|     let rom_title = emu.title(); | ||||
|  | ||||
|     let mut gamepad = Gilrs::new().expect("Initialize Controller Support"); | ||||
| @@ -96,6 +99,8 @@ fn main() -> Result<()> { | ||||
|                 .map_err(|e| anyhow!("pixels.render() failed: {}", e)) | ||||
|                 .is_err() | ||||
|             { | ||||
|                 emu.try_write_sav().expect("Write game save if need be"); | ||||
|  | ||||
|                 *control_flow = ControlFlow::Exit; | ||||
|                 return; | ||||
|             } | ||||
| @@ -103,6 +108,8 @@ fn main() -> Result<()> { | ||||
|  | ||||
|         if input.update(&event) { | ||||
|             if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { | ||||
|                 emu.try_write_sav().expect("Write game save if need be"); | ||||
|  | ||||
|                 *control_flow = ControlFlow::Exit; | ||||
|                 return; | ||||
|             } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user