From eefec196647137e0e583d7dc0598ea92b8f8728c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Magalh=C3=A3es?= <joamag@gmail.com>
Date: Tue, 18 Apr 2023 00:17:41 +0100
Subject: [PATCH] feat: initial working save image solution The solution makes
 use of the chrono module to obtain time and the image module to save PNG
 files. The Web version is still pending support for printing.

---
 CHANGELOG.md              |   1 +
 README.md                 |   2 +
 frontends/sdl/Cargo.toml  |   6 +++
 frontends/sdl/src/main.rs |  19 +++++++-
 src/devices/printer.rs    | 100 ++++++++++++++++++++++++++++++++++----
 src/gb.rs                 |  10 ++--
 6 files changed, 124 insertions(+), 14 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc69b4e5..0bb0627c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 * Support for serial data transfer - [#19](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/19)
+* Support for printing of images using Printer emulation - [#19](https://gitlab.stage.hive.pt/joamag/boytacean/-/issues/19)
 
 ### Changed
 
diff --git a/README.md b/README.md
index 55064486..e0b5316e 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,8 @@ A Game Boy emulator that is written in Rust 🦀.
 * Simple navigable source-code
 * Web and SDL front-ends
 * Audio, with a pretty accurate APU
+* Serial Data Transfer (Link Cable) support
+* Game Boy Printer emulation
 * Support for multiple MBCs: MBC1, MBC2, MBC3, and MBC5
 * Variable CPU clock speed
 * Accurate PPU - passes [dmg-acid2](https://github.com/mattcurrie/dmg-acid2) tests
diff --git a/frontends/sdl/Cargo.toml b/frontends/sdl/Cargo.toml
index 70fabd1d..5a683763 100644
--- a/frontends/sdl/Cargo.toml
+++ b/frontends/sdl/Cargo.toml
@@ -13,6 +13,12 @@ debug = ["boytacean/debug"]
 [dependencies.boytacean]
 path = "../.."
 
+[dependencies.image]
+version = "0.24"
+
+[dependencies.chrono]
+version = "0.4"
+
 [dependencies.sdl2]
 version = "0.35"
 features = ["ttf", "image", "gfx", "mixer", "static-link", "use-vcpkg"]
diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs
index e9ae117a..f6d1fc83 100644
--- a/frontends/sdl/src/main.rs
+++ b/frontends/sdl/src/main.rs
@@ -6,13 +6,16 @@ pub mod graphics;
 
 use audio::Audio;
 use boytacean::{
+    devices::printer::PrinterDevice,
     gb::{AudioProvider, GameBoy},
     pad::PadKey,
     ppu::{PaletteInfo, PpuMode, DISPLAY_HEIGHT, DISPLAY_WIDTH},
 };
+use chrono::Utc;
 use graphics::{surface_from_bytes, Graphics};
+use image::ColorType;
 use sdl2::{event::Event, keyboard::Keycode, pixels::PixelFormatEnum, Sdl};
-use std::{cmp::max, time::SystemTime};
+use std::{cmp::max, path::Path, time::SystemTime};
 
 /// The scale at which the screen is going to be drawn
 /// meaning the ratio between Game Boy resolution and
@@ -444,7 +447,19 @@ fn main() {
     // creates a new Game Boy instance and loads both the boot ROM
     // and the initial game ROM to "start the engine"
     let mut game_boy = GameBoy::new();
-    game_boy.attach_printer_serial();
+    let mut printer = Box::<PrinterDevice>::default();
+    printer.set_callback(|image_buffer| {
+        let file_name = format!("printer-{}.png", Utc::now().format("%Y%m%d-%H%M%S"));
+        image::save_buffer(
+            Path::new(&file_name),
+            image_buffer,
+            160,
+            (image_buffer.len() / 4 / 160) as u32,
+            ColorType::Rgba8,
+        )
+        .unwrap();
+    });
+    game_boy.attach_serial(printer);
     game_boy.load_boot_default();
 
     // creates a new generic emulator structure then starts
diff --git a/src/devices/printer.rs b/src/devices/printer.rs
index c262fc43..6dde7f6e 100644
--- a/src/devices/printer.rs
+++ b/src/devices/printer.rs
@@ -1,6 +1,15 @@
-use std::fmt::{self, Display, Formatter};
+use std::{
+    fmt::{self, Display, Formatter}
+};
 
-use crate::{serial::SerialDevice, warnln};
+use crate::{ppu::PaletteAlpha, serial::SerialDevice, warnln};
+
+const PRINTER_PALETTE: PaletteAlpha = [
+    [0xff, 0xff, 0xff, 0xff],
+    [0xaa, 0xaa, 0xaa, 0xff],
+    [0x55, 0x55, 0x55, 0xff],
+    [0x00, 0x00, 0x00, 0xff],
+];
 
 #[derive(Clone, Copy, PartialEq, Eq)]
 enum PrinterState {
@@ -63,7 +72,7 @@ impl Display for PrinterState {
 #[derive(Clone, Copy, PartialEq, Eq)]
 enum PrinterCommand {
     Init = 0x01,
-    Start = 0x02,
+    Print = 0x02,
     Data = 0x04,
     Status = 0x0f,
     Other = 0xff,
@@ -73,7 +82,7 @@ impl PrinterCommand {
     pub fn description(&self) -> &'static str {
         match self {
             PrinterCommand::Init => "Init",
-            PrinterCommand::Start => "Start",
+            PrinterCommand::Print => "Print",
             PrinterCommand::Data => "Data",
             PrinterCommand::Status => "Status",
             PrinterCommand::Other => "Other",
@@ -83,7 +92,7 @@ impl PrinterCommand {
     fn from_u8(value: u8) -> Self {
         match value {
             0x01 => PrinterCommand::Init,
-            0x02 => PrinterCommand::Start,
+            0x02 => PrinterCommand::Print,
             0x04 => PrinterCommand::Data,
             0x0f => PrinterCommand::Status,
             _ => PrinterCommand::Other,
@@ -107,6 +116,10 @@ pub struct PrinterDevice {
     status: u8,
     byte_out: u8,
     data: [u8; 0x280],
+    image: [u8; 160 * 200],
+    image_offset: u16,
+    image_buffer: Vec<u8>,
+    callback: fn(image_buffer: &Vec<u8>)
 }
 
 impl PrinterDevice {
@@ -121,6 +134,10 @@ impl PrinterDevice {
             status: 0x0,
             byte_out: 0x0,
             data: [0x00; 0x280],
+            image: [0x00; 160 * 200],
+            image_offset: 0,
+            image_buffer: Vec::new(),
+            callback: |_| {}
         }
     }
 
@@ -133,7 +150,27 @@ impl PrinterDevice {
         self.checksum = 0x0;
         self.status = 0x0;
         self.byte_out = 0x0;
-        self.data = [0x00; 0x280]
+        self.data = [0x00; 0x280];
+        self.image = [0x00; 160 * 200];
+        self.image_offset = 0;
+        
+        self.clear_image_buffer()
+    }
+
+    pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) {
+        self.callback = callback;
+    }
+
+    pub fn image_buffer(&self) -> &Vec<u8> {
+        &self.image_buffer
+    }
+
+    pub fn image_buffer_mut(&mut self) -> &mut Vec<u8> {
+        &mut self.image_buffer
+    }
+
+    pub fn clear_image_buffer(&mut self) {
+        self.image_buffer.clear();
     }
 
     fn run_command(&mut self, command: PrinterCommand) {
@@ -141,14 +178,30 @@ impl PrinterDevice {
             PrinterCommand::Init => {
                 self.status = 0x00;
                 self.byte_out = self.status;
+                self.image_offset = 0;
+                self.image_buffer.clear();
             }
-            PrinterCommand::Start => {
+            PrinterCommand::Print => {
+                let palette_index = self.data[2];
+
+                for index in 0..self.image_offset {
+                    let value = self.image[index as usize];
+                    let pixel_offset = (palette_index >> (value << 1)) & 0x03;
+                    let pixel = PRINTER_PALETTE[pixel_offset as usize];
+                    self.image_buffer.push(pixel[0]);
+                    self.image_buffer.push(pixel[1]);
+                    self.image_buffer.push(pixel[2]);
+                    self.image_buffer.push(pixel[3]);
+                }
+
+                (self.callback)(&self.image_buffer);
+
                 self.byte_out = self.status;
                 self.status = 0x06;
             }
             PrinterCommand::Data => {
                 if self.command_length == 0x280 {
-                    println!("Printer: Going to copy the image for printing");
+                    self.flush_image();
                 }
                 // in case the command is of size 0 we assume this is
                 // an EOF and we ignore this data operation
@@ -166,7 +219,8 @@ impl PrinterDevice {
                 self.byte_out = self.status;
 
                 // in case the current status is printing let's
-                // mark it as done
+                // mark it as done, resetting the status back to
+                // the original value
                 if self.status == 0x06 {
                     // @TODO: check if this value should be 0x04 instead
                     // this seems to be a bug with the print demo
@@ -178,6 +232,34 @@ impl PrinterDevice {
             }
         }
     }
+
+    fn flush_image(&mut self) {
+        // sets the initial value of the index that will point to
+        // the data that is going to be copied to the image buffer
+        let mut index = 0;
+
+        // iterates over the two rows that are going to be printed
+        // keep in mind that the printer only allows 2 lines at each
+        // time to be printed
+        for _ in 0..2 {
+            for col in 0..20 {
+                for y in 0..8 {
+                    let mut first = self.data[index];
+                    let mut second = self.data[index + 1];
+                    for x in 0..8 {
+                        let offset = self.image_offset as usize + (col * 8) + (y * 160) + x;
+                        self.image[offset] = (first >> 7) | ((second >> 6) & 0x02);
+
+                        first <<= 1;
+                        second <<= 1;
+                    }
+                    index += 2
+                }
+            }
+
+            self.image_offset += 160 * 8;
+        }
+    }
 }
 
 impl SerialDevice for PrinterDevice {
diff --git a/src/gb.rs b/src/gb.rs
index e107c389..0a7880f7 100644
--- a/src/gb.rs
+++ b/src/gb.rs
@@ -8,7 +8,7 @@ use crate::{
     pad::{Pad, PadKey},
     ppu::{Ppu, PpuMode, Tile, FRAME_BUFFER_SIZE},
     rom::Cartridge,
-    serial::Serial,
+    serial::{Serial, SerialDevice},
     timer::Timer,
     util::read_file,
 };
@@ -358,12 +358,16 @@ impl GameBoy {
         self.apu().set_clock_freq(value);
     }
 
+    pub fn attach_serial(&mut self, device: Box<dyn SerialDevice>) {
+        self.serial().set_device(device);
+    }
+
     pub fn attach_stdout_serial(&mut self) {
-        self.serial().set_device(Box::<StdoutDevice>::default());
+        self.attach_serial(Box::<StdoutDevice>::default());
     }
 
     pub fn attach_printer_serial(&mut self) {
-        self.serial().set_device(Box::<PrinterDevice>::default());
+        self.attach_serial(Box::<PrinterDevice>::default());
     }
 }
 
-- 
GitLab