diff --git a/CHANGELOG.md b/CHANGELOG.md index cc69b4e5733251e22e9d201891d29bbc9bf0f56b..0bb0627ccfb530d355b92a2a55717515bbfef075 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 5506448669d561bbf8d6f9487df007f96683e5ef..e0b5316e569c16368e3752f378ab355ee2143cdc 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 70fabd1dbd1e4ac50407cfc1a8e4ea616568adc4..5a68376361c2cd4fe2ec12e9279e6ab2f44b97de 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 e9ae117a50e53277ea1c43758de58ad804fc48cd..f6d1fc838e0a703f84ea7d6f80e4d2f0b564db8c 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 c262fc43efef5ac510fc3c7e6ea8c3b465ad6478..6dde7f6edbbe3b1bfbc275b40dbeb3b5503bf3f6 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 e107c389a5820a06f68ced464798a07410728a7e..0a7880f7da907de19684f3a02205c5570e6ae245 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()); } }