From 85fec0db9015586e0ac8459d66a5a58d5c00cce2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com>
Date: Mon, 14 Aug 2023 22:18:08 +0100
Subject: [PATCH] chore: initial support for BMP image storage

---
 src/state.rs | 166 +++++++++++++++++++++++++++++++++++++++++++++++++--
 src/util.rs  |  50 +++++++++++++++-
 2 files changed, 211 insertions(+), 5 deletions(-)

diff --git a/src/state.rs b/src/state.rs
index 8a1d3e74..e94be690 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 c2397337..4340ee9d 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;
-- 
GitLab