diff --git a/ghast/src/debug.rs b/ghast/src/debug.rs index 9068ba3..3879bb7 100644 --- a/ghast/src/debug.rs +++ b/ghast/src/debug.rs @@ -41,13 +41,13 @@ impl Debugger { fn tile_map_0( gb: &Gameboy, ) -> impl '_ + Iterator>> { - tile_map(&gb.mem.vram.vram.0[0x1800..0x1BFF]) + tile_map(&gb.mem.vram.vram[0][0x1800..0x1BFF]) } fn tile_map_1( gb: &Gameboy, ) -> impl '_ + Iterator>> { - tile_map(&gb.mem.vram.vram.0[0x1C00..0x1FFF]) + tile_map(&gb.mem.vram.vram[0][0x1C00..0x1FFF]) } fn tile_map(map: &[u8]) -> impl '_ + Iterator>> { @@ -95,10 +95,10 @@ fn oam_obj_repr( fn print_tile_map(gb: &Gameboy) { let map = if check_bit_const::<3>(gb.mem.io().lcd_control) { println!("The second tile map:"); - &gb.mem.vram.vram.0[0x1C00..0x2000] + &gb.mem.vram.vram[0][0x1C00..0x2000] } else { println!("The first tile map:"); - &gb.mem.vram.vram.0[0x1800..0x1C00] + &gb.mem.vram.vram[0][0x1800..0x1C00] }; for i in 0..32 { print!("["); @@ -113,14 +113,14 @@ fn vram_0_to_tiles( gb: &Gameboy, palette: u8, ) -> impl '_ + Iterator>> { - vram_bank_to_tiles(gb, palette, &gb.mem.vram.vram.0) + vram_bank_to_tiles(gb, palette, &gb.mem.vram.vram[0]) } fn vram_1_to_tiles( gb: &Gameboy, palette: u8, ) -> impl '_ + Iterator>> { - vram_bank_to_tiles(gb, palette, &gb.mem.vram.vram.1) + vram_bank_to_tiles(gb, palette, &gb.mem.vram.vram[1]) } fn vram_bank_to_tiles<'a, M: 'static>( diff --git a/spirit/Cargo.toml b/spirit/Cargo.toml index 6da23d0..4c49655 100644 --- a/spirit/Cargo.toml +++ b/spirit/Cargo.toml @@ -6,15 +6,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -# serde = { version = "1.0", features = ["derive"] } # serde_json = { version = "1.0" } # hex = { version = "0.4", features = ["serde"] } derive_more = "0.99" array-concat = "0.5.2" # once_cell = "1.19" tracing = "0.1.40" -heapless = "0.8.0" +heapless = { version = "0.8.0", features = ["serde"] } serde = { version = "1.0.210", features = ["derive"] } +serde_with = "3.9.0" [dev-dependencies] postcard = { version = "1.0.10", features = ["alloc"] } diff --git a/spirit/src/lib.rs b/spirit/src/lib.rs index a44b238..6a30ada 100644 --- a/spirit/src/lib.rs +++ b/spirit/src/lib.rs @@ -28,6 +28,7 @@ pub mod lookup; pub mod mem; pub mod ppu; pub mod rom; +pub(crate) mod utils; /// Represents a Gameboy color with a cartridge inserted. #[derive(Debug, Hash)] diff --git a/spirit/src/mem/io.rs b/spirit/src/mem/io.rs index 89fbb1e..6b06a45 100644 --- a/spirit/src/mem/io.rs +++ b/spirit/src/mem/io.rs @@ -1,12 +1,13 @@ use std::ops::{Index, IndexMut}; +use serde::{Deserialize, Serialize}; use tracing::trace; use crate::{cpu::check_bit_const, lookup::InterruptOp, ppu::Pixel, ButtonInput}; use super::{vram::PpuMode, MemoryMap}; -#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct IoRegisters { /// ADDR FF00 pub(super) joypad: Joypad, @@ -292,7 +293,7 @@ impl Index for IoRegisters { } /// In GBC mode, there are extra palettes for the colors -#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct ColorPalettes { pub(crate) index: u8, /// This array is indexed into by the index field. @@ -352,7 +353,7 @@ impl IndexMut for ColorPalettes { } /// All of the date for one of the 8 palettes that can be held in memory. -#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct Palette { pub(crate) colors: [PaletteColor; 4], } @@ -385,7 +386,7 @@ impl IndexMut for Palette { /// The colors inside a palette are a bit odd. Each color takes up two bytes and represents each /// color with 5 bits (in little-endian). The top bit is not used. -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct PaletteColor(pub [u8; 2]); impl PaletteColor { @@ -432,7 +433,7 @@ impl IndexMut for PaletteColor { } } -#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Default, Serialize, Deserialize)] pub enum TimerControl { #[default] Disabled, @@ -462,7 +463,7 @@ pub enum TimerControl { /// The solution: Both registers will be synchronized when a tick is processed. Currently, this /// happens immediately before the instruction is actually processed, but this would also work if /// it happened immediately after too. As long as the tick is applied in the same order everywhere. -#[derive(Debug, Default, PartialEq, Eq, Hash, Clone)] +#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] pub(super) struct Joypad { main: u8, dup: u8, diff --git a/spirit/src/mem/mbc/mbc1.rs b/spirit/src/mem/mbc/mbc1.rs index 927801b..8cab58b 100644 --- a/spirit/src/mem/mbc/mbc1.rs +++ b/spirit/src/mem/mbc/mbc1.rs @@ -1,6 +1,8 @@ use std::ops::Range; -#[derive(Debug, Hash, Clone, PartialEq, Eq)] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MBC1 { kind: MBC1Kind, rom: Vec, @@ -101,13 +103,13 @@ impl MBC1 { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum MBC1Kind { Standard, Rewired, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum BankingMode { Simple = 0, Advanced = 1, diff --git a/spirit/src/mem/mbc/mbc2.rs b/spirit/src/mem/mbc/mbc2.rs index 37e22b6..b8455a0 100644 --- a/spirit/src/mem/mbc/mbc2.rs +++ b/spirit/src/mem/mbc/mbc2.rs @@ -1,8 +1,12 @@ use std::ops::{Index, IndexMut}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + use crate::cpu::u16_check_bit_const; -#[derive(Debug, Hash, Clone, PartialEq, Eq)] +#[serde_as] +#[derive(Debug, Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MBC2 { rom: Vec, // TODO: This can probably be replaced with NonZeroUsize @@ -10,6 +14,7 @@ pub struct MBC2 { // TODO: The built-in RAM only uses the lower 4-bits. There is no good way to model this via // indexing, so we are going to rely on the ROM writers to obey this. It might be the case // that this needs to be changed. + #[serde_as(as = "serde_with::Bytes")] ram: Box<[u8; 512]>, ram_enabled: u8, dead_byte: u8, diff --git a/spirit/src/mem/mbc/mod.rs b/spirit/src/mem/mbc/mod.rs index 9200e5b..6403fa0 100644 --- a/spirit/src/mem/mbc/mod.rs +++ b/spirit/src/mem/mbc/mod.rs @@ -15,6 +15,7 @@ pub use mbc1::*; pub use mbc2::*; pub use mbc3::*; pub use mbc5::*; +use serde::{Deserialize, Serialize}; use tracing::error; static NINTENDO_LOGO: &[u8] = &[ @@ -23,7 +24,7 @@ static NINTENDO_LOGO: &[u8] = &[ 0xBB, 0xBB, 0x67, 0x63, 0x6E, 0x0E, 0xEC, 0xCC, 0xDD, 0xDC, 0x99, 0x9F, 0xBB, 0xB9, 0x33, 0x3E, ]; -#[derive(Hash, Clone, PartialEq, Eq)] +#[derive(Hash, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum MemoryBankController { /// There is no external MBC. The game ROM is mapped into the 32 KiB that starts at 0x0000 and /// extends to 0x7FFF. An additional 8 KiB of RAM could be connected. This 8 KiB starts at diff --git a/spirit/src/mem/mod.rs b/spirit/src/mem/mod.rs index 18c9f0a..78d7475 100644 --- a/spirit/src/mem/mod.rs +++ b/spirit/src/mem/mod.rs @@ -17,6 +17,8 @@ pub use mbc::MemoryBankController; use mbc::*; use io::IoRegisters; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; use tracing::trace; use vram::PpuMode; @@ -26,16 +28,20 @@ pub static START_UP_HEADER: &[u8; 0x900] = include_bytes!("../cgb.bin"); pub type StartUpHeaders = ([u8; 0x100], [u8; 0x700]); -#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[serde_as] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct MemoryMap { // The MBC mbc: MemoryBankController, // The video RAM and Object attribute map pub vram: VRam, // The working RAM - wram: ([u8; 0x1000], [u8; 0x1000]), + #[serde(serialize_with = "crate::utils::serialize_slices_as_one")] + #[serde(deserialize_with = "crate::utils::deserialize_slices_as_one")] + wram: [[u8; 0x1000]; 2], io: IoRegisters, // High RAM + #[serde_as(as = "serde_with::Bytes")] hr: [u8; 0x7F], /// The interrupt enable register. Bits 0-4 flag where or not certain interrupt handlers can be /// called. @@ -56,7 +62,7 @@ impl MemoryMap { Self { mbc: MemoryBankController::new(cart), vram: VRam::new(), - wram: ([0; 0x1000], [0; 0x1000]), + wram: [[0; 0x1000]; 2], dead_byte: 0, io: IoRegisters::default(), hr: [0; 0x7F], @@ -185,7 +191,7 @@ impl MemoryMap { Self { mbc, vram: VRam::new(), - wram: ([0; 0x1000], [0; 0x1000]), + wram: [[0; 0x1000]; 2], dead_byte: 0, io: IoRegisters::default(), hr: [0; 0x7F], @@ -215,11 +221,11 @@ impl Index for MemoryMap { 0x0000..=0x7FFF => &self.mbc[index], n @ 0x8000..=0x9FFF => &self.vram[CpuVramIndex(self.io.vram_select == 1, n)], n @ 0xA000..=0xBFFF => &self.mbc[n], - n @ 0xC000..=0xCFFF => &self.wram.0[n as usize - 0xC000], - n @ 0xD000..=0xDFFF => &self.wram.1[n as usize - 0xD000], + n @ 0xC000..=0xCFFF => &self.wram[0][n as usize - 0xC000], + n @ 0xD000..=0xDFFF => &self.wram[1][n as usize - 0xD000], // Echo RAM - n @ 0xE000..=0xEFFF => &self.wram.0[n as usize - 0xE000], - n @ 0xF000..=0xFDFF => &self.wram.1[n as usize - 0xF000], + n @ 0xE000..=0xEFFF => &self.wram[0][n as usize - 0xE000], + n @ 0xF000..=0xFDFF => &self.wram[1][n as usize - 0xF000], n @ 0xFE00..=0xFE9F => &self.vram[CpuOamIndex(n)], // NOTE: This region *should not* actually be accessed, but, instead of panicking, a // dead byte will be returned instead. @@ -239,11 +245,11 @@ impl IndexMut for MemoryMap { n @ 0x0000..=0x7FFF => &mut self.mbc[n], n @ 0x8000..=0x9FFF => &mut self.vram[CpuVramIndex(self.io.vram_select == 1, n)], n @ 0xA000..=0xBFFF => &mut self.mbc[n], - n @ 0xC000..=0xCFFF => &mut self.wram.0[n as usize - 0xC000], - n @ 0xD000..=0xDFFF => &mut self.wram.1[n as usize - 0xD000], + n @ 0xC000..=0xCFFF => &mut self.wram[0][n as usize - 0xC000], + n @ 0xD000..=0xDFFF => &mut self.wram[1][n as usize - 0xD000], // Echo RAM - n @ 0xE000..=0xEFFF => &mut self.wram.0[n as usize - 0xE000], - n @ 0xF000..=0xFDFF => &mut self.wram.1[n as usize - 0xF000], + n @ 0xE000..=0xEFFF => &mut self.wram[0][n as usize - 0xE000], + n @ 0xF000..=0xFDFF => &mut self.wram[1][n as usize - 0xF000], n @ 0xFE00..=0xFE9F => &mut self.vram[CpuOamIndex(n)], // NOTE: This region *should not* actually be accessed, but, instead of panicking, a // dead byte will be returned instead. diff --git a/spirit/src/mem/vram.rs b/spirit/src/mem/vram.rs index fd07c0a..b435b6b 100644 --- a/spirit/src/mem/vram.rs +++ b/spirit/src/mem/vram.rs @@ -7,6 +7,8 @@ use std::ops::{Index, IndexMut}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; use tracing::{info, trace}; use super::{ @@ -29,7 +31,18 @@ pub(super) struct CpuOamIndex(pub u16); #[repr(u8)] #[derive( - Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, derive_more::IsVariant, + Debug, + Default, + Clone, + Copy, + Hash, + PartialEq, + Eq, + PartialOrd, + Ord, + derive_more::IsVariant, + Serialize, + Deserialize, )] pub(crate) enum PpuMode { /// Also refered to as "Mode 2" in the pandocs. @@ -43,12 +56,16 @@ pub(crate) enum PpuMode { VBlank = 3, } -#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[serde_as] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct VRam { // TODO: This is a GBC emulator, show it needs to support switching between banks 0 and 1. /// The main video RAM. Accessible through the address range 0x8000 through 0x9FFF. - pub vram: ([u8; 0x2000], [u8; 0x2000]), + #[serde(serialize_with = "crate::utils::serialize_slices_as_one")] + #[serde(deserialize_with = "crate::utils::deserialize_slices_as_one")] + pub vram: [[u8; 0x2000]; 2], /// The Object Attribute Map. Accessible through the address range 0xFE00 through 0xFE9F + #[serde_as(as = "serde_with::Bytes")] pub oam: [u8; 0xA0], /// The status that the PPU is currently in. This mode is set when the PPU is ticked and /// determines how the VRAM and OAM are indexed into. @@ -61,7 +78,7 @@ pub struct VRam { impl VRam { pub(super) fn new() -> Self { Self { - vram: ([0; 0x2000], [0; 0x2000]), + vram: [[0; 0x2000]; 2], oam: [0; 0xA0], status: PpuMode::default(), dead_byte: 0xFF, @@ -86,9 +103,9 @@ impl Index for VRam { &DEAD_READ_ONLY_BYTE } else { if bank && index < 0x9C00 { - &self.vram.1[index as usize - 0x8000] + &self.vram[1][index as usize - 0x8000] } else { - &self.vram.0[index as usize - 0x8000] + &self.vram[0][index as usize - 0x8000] } } } @@ -102,9 +119,9 @@ impl IndexMut for VRam { &mut self.dead_byte } else { if bank && index < 0x9C00 { - &mut self.vram.1[index as usize - 0x8000] + &mut self.vram[1][index as usize - 0x8000] } else { - &mut self.vram.0[index as usize - 0x8000] + &mut self.vram[0][index as usize - 0x8000] } } } @@ -159,7 +176,7 @@ impl Index<(ObjTileDataIndex, bool)> for VRam { &self, (ObjTileDataIndex(index, bank), size): (ObjTileDataIndex, bool), ) -> &Self::Output { - let bank = if bank { &self.vram.1 } else { &self.vram.0 }; + let bank = if bank { &self.vram[1] } else { &self.vram[0] }; let start = 16 * index as usize; let end = start + 16 + if size { 16 } else { 0 }; &bank[start..end] @@ -176,7 +193,7 @@ impl Index for VRam { let x = x as usize / 8; let y = y as usize / 8; let index = 0x1800 + (second_map as usize * 0x400) + (y * 32) + x; - &self.vram.0[index] + &self.vram[0][index] } } @@ -187,7 +204,7 @@ impl Index for VRam { let x = x as usize / 8; let y = y as usize / 8; let index = 0x1800 + (y * 32) + x; - &self.vram.1[index] + &self.vram[1][index] } } @@ -212,7 +229,7 @@ impl Index for VRam { 0x1000 + (16 * index as usize) } }; - let bank = if bank { &self.vram.1 } else { &self.vram.0 }; + let bank = if bank { &self.vram[1] } else { &self.vram[0] }; (&bank[index..index + 16]).try_into().unwrap() } } diff --git a/spirit/src/utils.rs b/spirit/src/utils.rs new file mode 100644 index 0000000..fe59cd9 --- /dev/null +++ b/spirit/src/utils.rs @@ -0,0 +1,29 @@ +use serde::{ser::SerializeSeq, Deserialize, de:::Error, Deserializer, Serializer}; + +use heapless::Vec as InlineVec; + +pub(crate) fn serialize_slices_as_one, Se: Serializer>( + slices: &[Sl], + ser: Se, +) -> Result { + let mut seq = ser.serialize_seq(Some(slices.len()))?; + slices + .iter() + .map(AsRef::as_ref) + .try_for_each(|b| seq.serialize_element(b)) + .and_then(|()| seq.end()) +} + +pub(crate) fn deserialize_slices_as_one< + 'de, + const N: usize, + const M: usize, + De: Deserializer<'de>, +>( + de: De, +) -> Result<[[u8; N]; M], De::Error> { + let data = InlineVec::, M>::deserialize(de)?; + data.into_array() + .map_err(|e| De::Error::custom(format!("")))? + .map(|data| data.into_array().unwrap()) +} diff --git a/spirit/tests/data/start_up_memory_maps.postcard b/spirit/tests/data/start_up_memory_maps.postcard new file mode 100644 index 0000000..38bb580 Binary files /dev/null and b/spirit/tests/data/start_up_memory_maps.postcard differ diff --git a/spirit/tests/start_up.rs b/spirit/tests/start_up.rs index 2ff4eae..a546f57 100644 --- a/spirit/tests/start_up.rs +++ b/spirit/tests/start_up.rs @@ -1,23 +1,20 @@ use std::fmt::Display; -use spirit::{ppu::Pixel, Gameboy}; +use spirit::{mem::MemoryMap, ppu::Pixel, Gameboy}; static START_UP_SCREENS: &[u8] = include_bytes!("data/start_up_screens.postcard"); -/* #[test] fn generate_frames() { let mut gb = Gameboy::new(include_bytes!("roms/acid/cgb-acid2.gbc")).start_up(); - let mut frames = Vec::new(); + let mut datums = Vec::new(); while !gb.is_complete() { gb.frame_step().complete(); - frames.push(gb.gb().ppu.screen.clone()); + datums.push(gb.gb().mem.clone()); } - let data = postcard::to_allocvec(&frames).unwrap(); - std::fs::write("tests/data/start_up_screens.postcard", data).unwrap(); + let data = postcard::to_allocvec(&datums).unwrap(); + std::fs::write("tests/data/start_up_memory_maps.postcard", data).unwrap(); } -*/ - #[test] fn test_startup_frames() { let states: Vec>> = postcard::from_bytes(START_UP_SCREENS).unwrap(); @@ -39,6 +36,17 @@ fn test_startup_frames() { assert!(gb.is_complete()); } +#[test] +fn test_startup_memory_maps() { + let states: Vec = postcard::from_bytes(START_UP_SCREENS).unwrap(); + let mut gb = Gameboy::new(include_bytes!("roms/acid/cgb-acid2.gbc")).start_up(); + for (frame_num, state) in states.into_iter().enumerate() { + gb.frame_step().complete(); + assert_eq!(state, gb.gb().mem, "Mismatched memory map on frame #{frame_num}"); + } + assert!(gb.is_complete()); +} + struct DisplaySlice<'a, T>(&'a [T]); impl<'a, T> Display for DisplaySlice<'a, T> @@ -49,7 +57,7 @@ where let mut iter = self.0.iter(); write!(f, "[")?; if let Some(val) = iter.next() { - write!(f, "{val}"); + write!(f, "{val}")?; } iter.try_for_each(|val| write!(f, ", {val}"))?; write!(f, "]")