From ce121864d2d0da599a68d3e07732a69154d4e915 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Mon, 20 Sep 2021 04:13:25 -0300 Subject: [PATCH] feat: implement cartridge saving Implemented for MBC1, MBC2, MBC3 and MBC5 --- Cargo.lock | 49 +++++++++++++++ Cargo.toml | 1 + src/bus.rs | 10 ++++ src/cartridge.rs | 153 ++++++++++++++++++++++++++++++++++++++++++++--- src/emu.rs | 51 +++++++++++++++- src/main.rs | 7 +++ 6 files changed, 263 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1eb78a0..7f0bfd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 189ed10..d4dd6f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/src/bus.rs b/src/bus.rs index bc26521..ab1f6fa 100644 --- a/src/bus.rs +++ b/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 { diff --git a/src/cartridge.rs b/src/cartridge.rs index e7bdc50..8d21674 100644 --- a/src/cartridge.rs +++ b/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) { + self.mbc.write_ext_ram(memory) + } + fn detect_mbc(memory: &[u8]) -> Box { 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, 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) { + 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, + + 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) { + if self.has_battery { + self.memory.copy_from_slice(&memory); } } } @@ -342,9 +411,10 @@ struct MBC5 { ram_bank: u8, rom_cap: usize, - memory: Vec, 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) { + 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) { + 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) { + // 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 { Box::new(NoMBC) } } + +trait Savable { + fn ext_ram(&self) -> Option<&[u8]>; + fn write_ext_ram(&mut self, memory: Vec); +} diff --git a/src/emu.rs b/src/emu.rs index a2d4c02..5b63205 100644 --- a/src/emu.rs +++ b/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) { - 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 { + 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 { diff --git a/src/main.rs b/src/main.rs index 48a9a8b..f9e02be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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; }