From 52ad064ab52513b39e943a4aec0474e209888978 Mon Sep 17 00:00:00 2001 From: Franco Colmenarez Date: Fri, 19 Nov 2021 17:11:27 -0500 Subject: [PATCH] Saving RAM when the emulator is closed --- README.md | 4 +- src/bus.rs | 16 +++--- src/emulator.rs | 12 +++++ src/lib.rs | 1 + src/render.rs | 4 +- src/rom.rs | 133 +++++++++++++++++++++++++++++++++++++++++++++--- src/sound.rs | 47 +++++++++++++++++ 7 files changed, 200 insertions(+), 17 deletions(-) create mode 100644 src/sound.rs diff --git a/README.md b/README.md index b138077..658799c 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,11 @@ Any help or suggestion is welcome! - [x] MBC1 - [x] MBC2 - [ ] MBC3 (partially implemented) - - [X] MBC5 + - [x] MBC5 - [ ] MBC6 - [ ] MBC7 - [ ] HuC1 -- [ ] Save files +- [x] Save files - [ ] Web Assembly support (because this is a Rust project and it has to support Web Assembly) - [ ] Gameboy boot ROM (Not important for now) - [ ] Gameboy Color compatibility diff --git a/src/bus.rs b/src/bus.rs index 389e941..3b118ed 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -27,8 +27,8 @@ pub const INTERRUPT_ENABLE_ADDRESS: u16 = 0xFFFF; pub const INTERRUPT_FLAG_ADDRESS: u16 = 0xFF0F; pub struct Bus { - game_rom: Box, data: [u8; 0x10000], + pub rom: Box, pub ppu: PPU, pub joypad: Joypad, pub timer: Timer, @@ -39,19 +39,19 @@ impl Bus { let args: Vec = std::env::args().collect(); #[cfg(not(test))] if args.len() < 2 { - println!("Please, specify a ROM file"); + eprintln!("Please, specify a ROM file"); std::process::exit(1); } - let game_rom = match load_rom(&args.get(1).unwrap_or(&"".to_string())) { + let rom = match load_rom(&args.get(1).unwrap_or(&"".to_string())) { Ok(rom) => rom, Err(err) => { - println!("Could not read ROM: {}", err); + eprintln!("Could not read ROM: {}", err); std::process::exit(1); }, }; let mut bus = Self { data: [0x00; 0x10000], - game_rom, + rom, ppu: PPU::new(), joypad: Joypad::new(), timer: Timer::new(), @@ -85,7 +85,7 @@ impl Bus { pub fn read(&self, address: u16) -> u8 { if BANK_ZERO.contains(&address) || BANK_SWITCHABLE.contains(&address) || EXTERNAL_RAM.contains(&address) { - return self.game_rom.read(address); + return self.rom.read(address); } else if address == INTERRUPT_ENABLE_ADDRESS || address == INTERRUPT_FLAG_ADDRESS { return 0b11100000 | self.data[address as usize]; } else if VIDEO_RAM.contains(&address) { @@ -112,7 +112,7 @@ impl Bus { } if BANK_ZERO.contains(&address) || BANK_SWITCHABLE.contains(&address) || EXTERNAL_RAM.contains(&address) { - self.game_rom.write(address, data); + self.rom.write(address, data); } else if WORK_RAM_1.contains(&address) || WORK_RAM_2.contains(&address) { self.data[address as usize] = data; // Copy to the ECHO RAM @@ -120,7 +120,7 @@ impl Bus { self.data[(ECHO_RAM.min().unwrap() + (address - WORK_RAM_1.min().unwrap())) as usize] = data; } } else if EXTERNAL_RAM.contains(&address) { - self.game_rom.write(address, data); + self.rom.write(address, data); } else if ECHO_RAM.contains(&address) { self.data[address as usize] = data; self.data[(WORK_RAM_1.min().unwrap() + (address - ECHO_RAM.min().unwrap())) as usize] = data; // Copy to the working RAM diff --git a/src/emulator.rs b/src/emulator.rs index 65831f0..7de5907 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -5,6 +5,8 @@ use winit::event::{VirtualKeyCode}; use crate::cpu::{CPU, Cycles, Interrupt}; use crate::bus::Bus; use crate::joypad::{Button}; +#[cfg(not(test))] +use crate::rom::{save_file}; pub struct Emulator { bus: Bus, @@ -19,6 +21,16 @@ impl Emulator { } } + pub fn close(&self) { + println!("closing emulator"); + + #[cfg(not(test))] + match save_file(self.bus.rom.ram(), self.bus.rom.info()) { + Err(err) => eprintln!("Could not save file: {}", err), + _ => {}, + }; + } + pub fn handle_input(&mut self, input: &WinitInputHelper) { let mut change = false; if input.key_pressed(VirtualKeyCode::K) { diff --git a/src/lib.rs b/src/lib.rs index 7a044bb..2a07d9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod utils; pub mod cpu; pub mod ppu; pub mod timer; +pub mod sound; pub mod rom; pub mod bus; pub mod joypad; diff --git a/src/render.rs b/src/render.rs index f93dd64..129cd86 100644 --- a/src/render.rs +++ b/src/render.rs @@ -52,12 +52,13 @@ pub fn start_eventloop() { let mut pixels = create_pixels(WIDTH, HEIGHT, &window); event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; + // *control_flow = ControlFlow::Wait; // Handle input events if input.update(&event) { // Close events if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { + emulator.close(); *control_flow = ControlFlow::Exit; return; } @@ -76,6 +77,7 @@ pub fn start_eventloop() { .. } => { println!("The close button was pressed; stopping"); + emulator.close(); *control_flow = ControlFlow::Exit }, Event::MainEventsCleared => { diff --git a/src/rom.rs b/src/rom.rs index 5e70abf..a557bd0 100644 --- a/src/rom.rs +++ b/src/rom.rs @@ -2,6 +2,8 @@ use std::fs::File; #[cfg(not(test))] use std::io::Read; +#[cfg(not(test))] +use std::io::Write; use crate::bus::{ BANK_ZERO, @@ -36,6 +38,7 @@ fn header_checksum(data: &Vec) -> bool { pub fn load_rom(_filename: &str) -> std::io::Result> { Ok(Box::new(NoMBC::new(Vec::new(), ROMInfo { mbc: MBC::NoMBC, + filename: "".to_string(), publisher: "".to_string(), title: "".to_string(), cgb_only: false, @@ -58,16 +61,59 @@ pub fn load_rom(filename: &str) -> std::io::Result> { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Header checksum failed. Is this a Gameboy ROM?")); } - let info = ROMInfo::from_bytes(&data); + let mut info = ROMInfo::from_bytes(&data); + info.set_filename(filename.to_string()); + let info_copy = info.clone(); - Ok(match info.mbc { + let mut rom: Box = match info.mbc { MBC::NoMBC => Box::new(NoMBC::new(data, info)), MBC::MBC1 => Box::new(MBC1::new(data, info)), MBC::MBC2 => Box::new(MBC2::new(data, info)), MBC::MBC3 => Box::new(MBC3::new(data, info)), MBC::MBC5 => Box::new(MBC5::new(data, info)), _ => unimplemented!(), - }) + }; + + #[cfg(not(test))] + match load_save(rom.ram_mut(), &info_copy) { + Err(err) => eprintln!("Could not load save file: {}", err), + _ => {}, + }; + + Ok(rom) +} + +#[cfg(not(test))] +pub fn save_file(ram: &Vec, info: &ROMInfo) -> std::io::Result<()> { + if !info.has_ram || !info.has_battery { + return Ok(()); + } + let mut file = File::create(format!("{}.sav", info.filename))?; + file.write_all(ram)?; + Ok(()) +} + +#[cfg(not(test))] +pub fn load_save(ram: &mut Vec, info: &ROMInfo) -> std::io::Result<()> { + if !info.has_ram || !info.has_battery { + return Ok(()); + } + + let mut file = File::open(format!("{}.sav", info.filename))?; + let mut data = vec![]; + file.read_to_end(&mut data)?; + + let mut index = 0; + let size = match ram.len() < data.len() { + true => ram.len(), + false => data.len(), + }; + while index < size { + ram[index] = data[index]; + index += 1; + } + + Ok(()) } #[derive(Debug, Copy, Clone)] @@ -86,21 +132,22 @@ enum MBC { BandaiTIMA5, } -#[derive(Debug)] +#[derive(Copy, Clone, Debug)] enum Region { Japanese, NonJapanese, } -#[derive(Debug, PartialEq)] +#[derive(Copy, Clone, PartialEq, Debug)] enum BankingMode { Simple, Advanced, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct ROMInfo { mbc: MBC, + filename: String, publisher: String, title: String, cgb_only: bool, @@ -114,6 +161,10 @@ pub struct ROMInfo { } impl ROMInfo { + pub fn set_filename(&mut self, filename: String) { + self.filename = filename; + } + pub fn from_bytes(bytes: &[u8]) -> Self { let rom_type = bytes[CARTRIDGE_TYPE_ADDRESS as usize]; Self { @@ -147,6 +198,7 @@ impl ROMInfo { 0xFF => MBC::HuC1, _ => unreachable!(), }, + filename: "".to_string(), region: match bytes[DESTINATION_CODE_ADDRESS as usize] { 0x00 => Region::Japanese, _ => Region::NonJapanese, @@ -207,11 +259,15 @@ impl ROMInfo { pub trait ROM { fn read(&self, address: u16) -> u8; fn write(&mut self, address: u16, data: u8); + fn ram_mut(&mut self) -> &mut Vec; + fn ram(&self) -> &Vec; + fn info(&self) -> &ROMInfo; } pub struct NoMBC { data: Vec, info: ROMInfo, + ram: Vec, } impl NoMBC { @@ -219,6 +275,7 @@ impl NoMBC { let rom = Self { data, info, + ram: Vec::new(), }; println!("MBC {:?}", rom.info.mbc); println!("Region {:?}", rom.info.region); @@ -236,6 +293,18 @@ impl ROM for NoMBC { } fn write(&mut self, _address: u16, _data: u8) {} + + fn ram_mut(&mut self) -> &mut Vec { + &mut self.ram + } + + fn ram(&self) -> &Vec { + &self.ram + } + + fn info(&self) -> &ROMInfo { + &self.info + } } pub struct MBC1 { @@ -254,6 +323,7 @@ impl MBC1 { println!("MBC {:?}", info.mbc); println!("Region {:?}", info.region); println!("Has RAM {}", info.has_ram); + println!("Has battery {}", info.has_battery); println!("ROM banks {}", info.rom_banks); println!("RAM banks {}", info.ram_banks); let ram = vec![0; info.ram_size() as usize]; @@ -367,6 +437,18 @@ impl ROM for MBC1 { } } } + + fn ram_mut(&mut self) -> &mut Vec { + &mut self.ram + } + + fn ram(&self) -> &Vec { + &self.ram + } + + fn info(&self) -> &ROMInfo { + &self.info + } } pub struct MBC2 { @@ -382,6 +464,7 @@ impl MBC2 { println!("MBC {:?}", info.mbc); println!("Region {:?}", info.region); println!("Has RAM {}", info.has_ram); + println!("Has battery {}", info.has_battery); println!("ROM banks {}", info.rom_banks); println!("RAM banks {}", info.ram_banks); let ram = vec![0; 0x200]; @@ -443,6 +526,18 @@ impl ROM for MBC2 { } } } + + fn ram_mut(&mut self) -> &mut Vec { + &mut self.ram + } + + fn ram(&self) -> &Vec { + &self.ram + } + + fn info(&self) -> &ROMInfo { + &self.info + } } pub struct MBC3 { @@ -459,6 +554,7 @@ impl MBC3 { println!("MBC {:?}", info.mbc); println!("Region {:?}", info.region); println!("Has RAM {}", info.has_ram); + println!("Has battery {}", info.has_battery); println!("ROM banks {}", info.rom_banks); println!("RAM banks {}", info.ram_banks); let ram = vec![0; info.ram_size() as usize]; @@ -519,6 +615,18 @@ impl ROM for MBC3 { } } + + fn ram_mut(&mut self) -> &mut Vec { + &mut self.ram + } + + fn ram(&self) -> &Vec { + &self.ram + } + + fn info(&self) -> &ROMInfo { + &self.info + } } pub struct MBC5 { @@ -535,6 +643,7 @@ impl MBC5 { println!("MBC {:?}", info.mbc); println!("Region {:?}", info.region); println!("Has RAM {}", info.has_ram); + println!("Has battery {}", info.has_battery); println!("ROM banks {}", info.rom_banks); println!("RAM banks {}", info.ram_banks); let ram = vec![0; info.ram_size() as usize]; @@ -591,4 +700,16 @@ impl ROM for MBC5 { } } } + + fn ram_mut(&mut self) -> &mut Vec { + &mut self.ram + } + + fn ram(&self) -> &Vec { + &self.ram + } + + fn info(&self) -> &ROMInfo { + &self.info + } } diff --git a/src/sound.rs b/src/sound.rs new file mode 100644 index 0000000..2d9c72e --- /dev/null +++ b/src/sound.rs @@ -0,0 +1,47 @@ +use std::ops::RangeInclusive; + +pub const NR10_ADDRESS: u16 = 0xFF10; +pub const NR11_ADDRESS: u16 = 0xFF11; +pub const NR12_ADDRESS: u16 = 0xFF12; +pub const NR13_ADDRESS: u16 = 0xFF13; +pub const NR14_ADDRESS: u16 = 0xFF14; + +pub const NR21_ADDRESS: u16 = 0xFF16; +pub const NR22_ADDRESS: u16 = 0xFF17; +pub const NR23_ADDRESS: u16 = 0xFF18; +pub const NR24_ADDRESS: u16 = 0xFF19; + +pub const NR30_ADDRESS: u16 = 0xFF1A; +pub const NR31_ADDRESS: u16 = 0xFF1B; +pub const NR32_ADDRESS: u16 = 0xFF1C; +pub const NR33_ADDRESS: u16 = 0xFF1D; +pub const NR34_ADDRESS: u16 = 0xFF1E; + +pub const NR41_ADDRESS: u16 = 0xFF20; +pub const NR42_ADDRESS: u16 = 0xFF21; +pub const NR43_ADDRESS: u16 = 0xFF22; +pub const NR44_ADDRESS: u16 = 0xFF23; + +pub const NR50_ADDRESS: u16 = 0xFF24; +pub const NR51_ADDRESS: u16 = 0xFF25; +pub const NR52_ADDRESS: u16 = 0xFF26; + +pub const WAVE_PATTERN_RAM: RangeInclusive = 0xFF30..=0xFF3F; + +pub struct Sound { + io_registers: [u8; 48], +} + +impl Sound { + pub fn is_io_register(address: u16) -> bool { + address >= 0xFF10 && address <= 0xFF3F + } + + pub fn get_register(&self, address: u16) -> u8 { + self.io_registers[(address - 0xFF10) as usize] + } + + pub fn set_register(&mut self, address: u16, data: u8) { + self.io_registers[(address - 0xFF10) as usize] = data; + } +}