diff --git a/src/state.rs b/src/state.rs index 8a1d3e7422c1b92eb4f3fff32f9f707819cff3ed..e94be690a001db9327034e1de8b2c1a3b38cc065 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,11 +11,13 @@ use std::{ use crate::{ gb::{GameBoy, GameBoySpeed}, info::Info, + ppu::{DISPLAY_HEIGHT, DISPLAY_WIDTH, FRAME_BUFFER_SIZE}, rom::{CgbMode, MbcType}, + util::save_bmp, }; /// Magic string for the BOS (Boytacean Save State) format. -pub const BOS_MAGIC: &'static str = "BOS\0"; +pub const BOS_MAGIC: &str = "BOS\0"; /// Magic string ("BOS\0") in little endian unsigned 32 bit format. pub const BOS_MAGIC_UINT: u32 = 0x00534f42; @@ -31,10 +33,23 @@ pub enum SaveStateFormat { Bess, } +#[derive(Clone, Copy)] pub enum BosBlockKind { Name = 0x01, ImageBuffer = 0x02, SystemInfo = 0x03, + Unknown = 0xff, +} + +impl BosBlockKind { + fn from_u8(value: u8) -> Self { + match value { + 0x01 => Self::Name, + 0x02 => Self::ImageBuffer, + 0x03 => Self::SystemInfo, + _ => Self::Unknown, + } + } } pub trait Serialize { @@ -62,7 +77,8 @@ pub trait State { pub struct BosState { magic: u32, version: u8, - blocks: Vec<BosBlock>, + block_count: u8, + image_buffer: Option<BosImageBuffer>, bess: BessState, } @@ -85,12 +101,37 @@ impl BosState { self.bess.verify()?; Ok(()) } + + pub fn save_bmp(&self, file_path: &str) -> Result<(), String> { + if let Some(image_buffer) = &self.image_buffer { + image_buffer.save_bmp(file_path)?; + Ok(()) + } else { + Err(String::from("No image buffer found")) + } + } + + fn build_block_count(&self) -> u8 { + let mut count = 0_u8; + if self.image_buffer.is_some() { + count += 1; + } + count + } } impl Serialize for BosState { fn write(&mut self, buffer: &mut Cursor<Vec<u8>>) { + self.block_count = self.build_block_count(); + buffer.write_all(&self.magic.to_le_bytes()).unwrap(); buffer.write_all(&self.version.to_le_bytes()).unwrap(); + buffer.write_all(&self.block_count.to_le_bytes()).unwrap(); + + if let Some(image_buffer) = &mut self.image_buffer { + image_buffer.write(buffer); + } + self.bess.write(buffer); } @@ -101,6 +142,28 @@ impl Serialize for BosState { let mut buffer = [0x00; 1]; data.read_exact(&mut buffer).unwrap(); self.version = u8::from_le_bytes(buffer); + let mut buffer = [0x00; 1]; + data.read_exact(&mut buffer).unwrap(); + self.block_count = u8::from_le_bytes(buffer); + + for _ in 0..self.block_count { + let block = BosBlock::from_data(data); + let offset = -((size_of::<u8>() + size_of::<u32>()) as i64); + data.seek(SeekFrom::Current(offset)).unwrap(); + + match block.kind { + BosBlockKind::ImageBuffer => { + self.image_buffer = Some(BosImageBuffer::from_data(data)); + } + _ => { + data.seek(SeekFrom::Current(-offset)).unwrap(); + data.seek(SeekFrom::Current(block.size as i64)).unwrap(); + } + } + } + + self.block_count = self.build_block_count(); + self.bess.read(data); } } @@ -110,7 +173,8 @@ impl State for BosState { Ok(Self { magic: BOS_MAGIC_UINT, version: BOS_VERSION, - blocks: vec![], + block_count: 1, + image_buffer: Some(BosImageBuffer::from_gb(gb)?), bess: BessState::from_gb(gb)?, }) } @@ -125,7 +189,101 @@ impl State for BosState { pub struct BosBlock { kind: BosBlockKind, size: u32, - buffer: Vec<u8>, +} + +impl BosBlock { + pub fn new(kind: BosBlockKind, size: u32) -> Self { + Self { kind, size } + } + + pub fn from_data(data: &mut Cursor<Vec<u8>>) -> Self { + let mut instance = Self::default(); + instance.read(data); + instance + } +} + +impl Serialize for BosBlock { + fn write(&mut self, buffer: &mut Cursor<Vec<u8>>) { + buffer.write_all(&(self.kind as u8).to_le_bytes()).unwrap(); + buffer.write_all(&self.size.to_le_bytes()).unwrap(); + } + + fn read(&mut self, data: &mut Cursor<Vec<u8>>) { + let mut buffer = [0x00; 1]; + data.read_exact(&mut buffer).unwrap(); + self.kind = BosBlockKind::from_u8(u8::from_le_bytes(buffer)); + let mut buffer = [0x00; 4]; + data.read_exact(&mut buffer).unwrap(); + self.size = u32::from_le_bytes(buffer); + } +} + +impl Default for BosBlock { + fn default() -> Self { + Self::new(BosBlockKind::Name, 0) + } +} + +pub struct BosImageBuffer { + header: BosBlock, + image: [u8; FRAME_BUFFER_SIZE], +} + +impl BosImageBuffer { + pub fn new(image: [u8; FRAME_BUFFER_SIZE]) -> Self { + Self { + header: BosBlock::new( + BosBlockKind::ImageBuffer, + (size_of::<u8>() * FRAME_BUFFER_SIZE) as u32, + ), + image, + } + } + + pub fn from_data(data: &mut Cursor<Vec<u8>>) -> Self { + let mut instance = Self::default(); + instance.read(data); + instance + } + + pub fn save_bmp(&self, file_path: &str) -> Result<(), String> { + save_bmp( + file_path, + &self.image, + DISPLAY_WIDTH as u32, + DISPLAY_HEIGHT as u32, + )?; + Ok(()) + } +} + +impl Serialize for BosImageBuffer { + fn write(&mut self, buffer: &mut Cursor<Vec<u8>>) { + self.header.write(buffer); + buffer.write_all(&self.image).unwrap(); + } + + fn read(&mut self, data: &mut Cursor<Vec<u8>>) { + self.header.read(data); + data.read_exact(&mut self.image).unwrap(); + } +} + +impl State for BosImageBuffer { + fn from_gb(gb: &mut GameBoy) -> Result<Self, String> { + Ok(Self::new(*gb.ppu_i().frame_buffer)) + } + + fn to_gb(&self, _gb: &mut GameBoy) -> Result<(), String> { + Ok(()) + } +} + +impl Default for BosImageBuffer { + fn default() -> Self { + Self::new([0x00; FRAME_BUFFER_SIZE]) + } } #[derive(Default)] diff --git a/src/util.rs b/src/util.rs index c2397337b157a78a2cabacb0b731965bbbaf7a7e..4340ee9d837235adfc534204c414be3b03eeb999 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,7 @@ use std::{ cell::RefCell, fs::File, - io::{Read, Write}, + io::{BufWriter, Read, Write}, path::Path, rc::Rc, }; @@ -54,6 +54,54 @@ pub fn capitalize(string: &str) -> String { } } +pub fn save_bmp(path: &str, pixels: &[u8], width: u32, height: u32) -> Result<(), String> { + let file = match File::create(path) { + Ok(file) => file, + Err(_) => return Err(format!("Failed to open file: {}", path)), + }; + let mut writer = BufWriter::new(file); + + // writes the BMP file header + let file_size = 54 + (width * height * 3); + writer.write_all(&[0x42, 0x4d]).unwrap(); // "BM" magic number + writer.write_all(&file_size.to_le_bytes()).unwrap(); // file size + writer.write_all(&[0x00, 0x00]).unwrap(); // reserved + writer.write_all(&[0x00, 0x00]).unwrap(); // reserved + writer.write_all(&[0x36, 0x00, 0x00, 0x00]).unwrap(); // offset to pixel data + writer.write_all(&[0x28, 0x00, 0x00, 0x00]).unwrap(); // DIB header size + writer.write_all(&(width as i32).to_le_bytes()).unwrap(); // image width + writer.write_all(&(height as i32).to_le_bytes()).unwrap(); // image height + writer.write_all(&[0x01, 0x00]).unwrap(); // color planes + writer.write_all(&[0x18, 0x00]).unwrap(); // bits per pixel + writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // compression method + writer + .write_all(&[(width * height * 3) as u8, 0x00, 0x00, 0x00]) + .unwrap(); // image size + writer.write_all(&[0x13, 0x0b, 0x00, 0x00]).unwrap(); // horizontal resolution (72 DPI) + writer.write_all(&[0x13, 0x0b, 0x00, 0x00]).unwrap(); // vertical resolution (72 DPI) + writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // color palette + writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // important colors + + // iterates over the complete array of pixels in reverse order + // to account for the fact that BMP files are stored upside down + for y in (0..height).rev() { + for x in 0..width { + let [r, g, b] = [ + pixels[((y * width + x) * 3) as usize], + pixels[((y * width + x) * 3 + 1) as usize], + pixels[((y * width + x) * 3 + 2) as usize], + ]; + writer.write_all(&[b, g, r]).unwrap(); + } + let padding = (4 - ((width * 3) % 4)) % 4; + for _ in 0..padding { + writer.write_all(&[0x00]).unwrap(); + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use std::path::Path;