diff --git a/CHANGELOG.md b/CHANGELOG.md index 4932f7f1f3a25e1726bdec45d8da6f5e6056bdb8..0bb0627ccfb530d355b92a2a55717515bbfef075 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ 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/doc/inspiration.md b/doc/inspiration.md index 2ca9d3f86d306b9e90d047429112d68ab2f12283..b097ac1a6a98ae3624712ddae1b57f93dbf7624e 100644 --- a/doc/inspiration.md +++ b/doc/inspiration.md @@ -26,6 +26,7 @@ * [GitHub - simias/gb-rs (Rust)](https://github.com/simias/gb-rs) * [GitHub - RubenG123/frosty (Rust)](https://github.com/RubenG123/frosty) * [GitHub - calvinbaart/gameboy (TypeScript)](https://github.com/calvinbaart/gameboy) +* [GitHub - HFO4/gameboy.live (Go)](https://github.com/HFO4/gameboy.live) * [GitHub - djhworld/gomeboycolor-wasm (Go)](https://github.com/djhworld/gomeboycolor-wasm) * [GitHub - torch2424/wasmboy (WASM)](https://github.com/torch2424/wasmboy) @@ -33,10 +34,15 @@ * [YouTube - The Ultimate Game Boy Talk (33c3)](https://www.youtube.com/watch?v=HyzD8pNlpwI) +## Peripherals + +* [In Depth: The Game Boy Printer](https://shonumi.github.io/articles/art2.html) + ## Other * [GitHub - gbdev/awesome-gbdev](https://github.com/gbdev/awesome-gbdev) * [GitHub - Hacktix/Bootix](https://github.com/Hacktix/Bootix) +* [GitHub - gbdk-2020/gbdk-2020 (Best GB C SDK)](https://github.com/gbdk-2020/gbdk-2020) * [GitHub - fcambus/jsemu (Emulators written in JavaScript)](https://github.com/fcambus/jsemu) * [Gameboy Doctor: debug and fix your gameboy emulator](https://robertheaton.com/gameboy-doctor) * [Game Boy Boot ROMs](https://gbdev.gg8.se/files/roms/bootroms) diff --git a/frontends/sdl/Cargo.toml b/frontends/sdl/Cargo.toml index 4c68892c503034d1a0266613cd999d9180cba59b..5a68376361c2cd4fe2ec12e9279e6ab2f44b97de 100644 --- a/frontends/sdl/Cargo.toml +++ b/frontends/sdl/Cargo.toml @@ -7,9 +7,18 @@ license = "Apache-2.0" keywords = ["gameboy", "emulator", "rust", "sdl"] edition = "2018" +[features] +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/README.md b/frontends/sdl/README.md index 75c153a41485dcef6f204b8b9d1450bde176794e..163d1b2e9ff09f07c11d104e7a28e0beea1101c3 100644 --- a/frontends/sdl/README.md +++ b/frontends/sdl/README.md @@ -23,3 +23,9 @@ To reload the code continuously use the cargo watch tool: cargo install cargo-watch cargo watch -x run ``` + +There are some feature flags that control the verbosity of the emulator to run in debug mode use: + +```bash +cargo run --features debug +``` diff --git a/frontends/sdl/src/main.rs b/frontends/sdl/src/main.rs index 9c6f6d0a0f0bc90136b6f4152f7a676c45200d24..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 @@ -20,11 +23,11 @@ use std::{cmp::max, time::SystemTime}; const SCREEN_SCALE: f32 = 2.0; /// The base title to be used in the window. -static TITLE: &str = "Boytacean"; +const TITLE: &str = "Boytacean"; /// Base audio volume to be used as the basis of the /// amplification level of the volume -static VOLUME: f32 = 64.0; +const VOLUME: f32 = 64.0; pub struct Benchmark { count: usize, @@ -209,7 +212,7 @@ impl Emulator { } pub fn toggle_audio(&mut self) { - let apu_enabled = self.system.get_apu_enabled(); + let apu_enabled = self.system.apu_enabled(); self.system.set_apu_enabled(!apu_enabled); } @@ -444,6 +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(); + 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 @@ -451,7 +467,7 @@ fn main() { // ROM file and starts running it let mut emulator = Emulator::new(game_boy); emulator.start(SCREEN_SCALE); - emulator.load_rom(Some("../../res/roms/demo/pocket.gb")); + emulator.load_rom(Some("../../res/roms/test/gbprinter.gb")); emulator.toggle_palette(); emulator.run(); } diff --git a/res/roms/test/gbprinter.gb b/res/roms/test/gbprinter.gb new file mode 100644 index 0000000000000000000000000000000000000000..aa8c6d2b565f7808ddf0eec31765436bc2dca61c Binary files /dev/null and b/res/roms/test/gbprinter.gb differ diff --git a/src/cpu.rs b/src/cpu.rs index 9be9b16944dfd31c5fd9c66401c6058f64551e71..ba115c2e54d6ffa5077b0db04d48539d976701d7 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -7,6 +7,7 @@ use crate::{ mmu::Mmu, pad::Pad, ppu::Ppu, + serial::Serial, timer::Timer, }; @@ -191,6 +192,26 @@ impl Cpu { return 24; } // @TODO aggregate the handling of these interrupts + else if (self.mmu.ie & 0x08 == 0x08) && self.mmu.serial().int_serial() { + debugln!("Going to run Serial interrupt handler (0x58)"); + + self.disable_int(); + self.push_word(pc); + self.pc = 0x58; + + // acknowledges that the serial interrupt has been + // properly handled + self.mmu.serial().ack_serial(); + + // in case the CPU is currently halted waiting + // for an interrupt, releases it + if self.halted { + self.halted = false; + } + + return 24; + } + // @TODO aggregate the handling of these interrupts else if (self.mmu.ie & 0x10 == 0x10) && self.mmu.pad().int_pad() { debugln!("Going to run JoyPad interrupt handler (0x60)"); @@ -297,6 +318,11 @@ impl Cpu { self.mmu().timer() } + #[inline(always)] + pub fn serial(&mut self) -> &mut Serial { + self.mmu().serial() + } + #[inline(always)] pub fn halted(&self) -> bool { self.halted diff --git a/src/devices/mod.rs b/src/devices/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..ada19f9138884470af7746f6fa424080b2601f8a --- /dev/null +++ b/src/devices/mod.rs @@ -0,0 +1,2 @@ +pub mod printer; +pub mod stdout; diff --git a/src/devices/printer.rs b/src/devices/printer.rs new file mode 100644 index 0000000000000000000000000000000000000000..23c46b4e0c8b2d03d85826217f634289934d03f1 --- /dev/null +++ b/src/devices/printer.rs @@ -0,0 +1,321 @@ +use std::fmt::{self, Display, Formatter}; + +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 { + MagicBytes1 = 0x00, + MagicBytes2 = 0x01, + Identification = 0x02, + Compression = 0x03, + LengthLow = 0x04, + LengthHigh = 0x05, + Data = 0x06, + ChecksumLow = 0x07, + ChecksumHigh = 0x08, + KeepAlive = 0x09, + Status = 0x0a, + Other = 0xff, +} + +impl PrinterState { + pub fn description(&self) -> &'static str { + match self { + PrinterState::MagicBytes1 => "Magic Bytes 1", + PrinterState::MagicBytes2 => "Magic Bytes 2", + PrinterState::Identification => "Identification", + PrinterState::Compression => "Compression", + PrinterState::LengthLow => "Length Low", + PrinterState::LengthHigh => "Length High", + PrinterState::Data => "Data", + PrinterState::ChecksumLow => "Checksum Low", + PrinterState::ChecksumHigh => "Checksum High", + PrinterState::KeepAlive => "Keep Alive", + PrinterState::Status => "Status", + PrinterState::Other => "Other", + } + } + + fn from_u8(value: u8) -> Self { + match value { + 0x00 => PrinterState::MagicBytes1, + 0x01 => PrinterState::MagicBytes2, + 0x02 => PrinterState::Identification, + 0x03 => PrinterState::Compression, + 0x04 => PrinterState::LengthLow, + 0x05 => PrinterState::LengthHigh, + 0x06 => PrinterState::Data, + 0x07 => PrinterState::ChecksumLow, + 0x08 => PrinterState::ChecksumHigh, + 0x09 => PrinterState::KeepAlive, + 0x0a => PrinterState::Status, + _ => PrinterState::Other, + } + } +} + +impl Display for PrinterState { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.description()) + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum PrinterCommand { + Init = 0x01, + Print = 0x02, + Data = 0x04, + Status = 0x0f, + Other = 0xff, +} + +impl PrinterCommand { + pub fn description(&self) -> &'static str { + match self { + PrinterCommand::Init => "Init", + PrinterCommand::Print => "Print", + PrinterCommand::Data => "Data", + PrinterCommand::Status => "Status", + PrinterCommand::Other => "Other", + } + } + + fn from_u8(value: u8) -> Self { + match value { + 0x01 => PrinterCommand::Init, + 0x02 => PrinterCommand::Print, + 0x04 => PrinterCommand::Data, + 0x0f => PrinterCommand::Status, + _ => PrinterCommand::Other, + } + } +} + +impl Display for PrinterCommand { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.description()) + } +} + +pub struct PrinterDevice { + state: PrinterState, + command: PrinterCommand, + compression: bool, + command_length: u16, + length_left: u16, + checksum: u16, + status: u8, + byte_out: u8, + data: [u8; 0x280], + image: [u8; 160 * 200], + image_offset: u16, + callback: fn(image_buffer: &Vec<u8>), +} + +impl PrinterDevice { + pub fn new() -> Self { + Self { + state: PrinterState::MagicBytes1, + command: PrinterCommand::Other, + compression: false, + command_length: 0, + length_left: 0, + checksum: 0x0, + status: 0x0, + byte_out: 0x0, + data: [0x00; 0x280], + image: [0x00; 160 * 200], + image_offset: 0, + callback: |_| {}, + } + } + + pub fn reset(&mut self) { + self.state = PrinterState::MagicBytes1; + self.command = PrinterCommand::Other; + self.compression = false; + self.command_length = 0; + self.length_left = 0; + self.checksum = 0x0; + self.status = 0x0; + self.byte_out = 0x0; + self.data = [0x00; 0x280]; + self.image = [0x00; 160 * 200]; + self.image_offset = 0; + } + + pub fn set_callback(&mut self, callback: fn(image_buffer: &Vec<u8>)) { + self.callback = callback; + } + + fn run_command(&mut self, command: PrinterCommand) { + match command { + PrinterCommand::Init => { + self.status = 0x00; + self.byte_out = self.status; + self.image_offset = 0; + } + PrinterCommand::Print => { + let mut image_buffer = Vec::new(); + 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]; + image_buffer.push(pixel[0]); + image_buffer.push(pixel[1]); + image_buffer.push(pixel[2]); + image_buffer.push(pixel[3]); + } + + (self.callback)(&image_buffer); + + self.byte_out = self.status; + self.status = 0x06; + } + PrinterCommand::Data => { + if self.command_length == 0x280 { + self.flush_image(); + } + // in case the command is of size 0 we assume this is + // an EOF and we ignore this data operation + else if self.command_length == 0x0 { + } else { + warnln!( + "Printer: Wrong size for data: {:04x} bytes", + self.command_length + ); + } + self.status = 0x08; + self.byte_out = self.status; + } + PrinterCommand::Status => { + self.byte_out = self.status; + + // in case the current status is printing let's + // mark it as done, resetting the status back to + // the original value + if self.status == 0x06 { + self.status = 0x00; + } + } + PrinterCommand::Other => { + warnln!("Printer: Invalid command: {:02x}", self.state as u8); + } + } + } + + 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 { + fn send(&mut self) -> u8 { + self.byte_out + } + + fn receive(&mut self, byte: u8) { + self.byte_out = 0x00; + + match self.state { + PrinterState::MagicBytes1 => { + if byte != 0x88 { + warnln!("Printer: Invalid magic byte 1: {:02x}", byte); + return; + } + self.command = PrinterCommand::Other; + self.command_length = 0; + } + PrinterState::MagicBytes2 => { + if byte != 0x33 { + if byte != 0x88 { + self.state = PrinterState::MagicBytes1; + } + warnln!("Printer: Invalid magic byte 2: {:02x}", byte); + return; + } + } + PrinterState::Identification => self.command = PrinterCommand::from_u8(byte), + PrinterState::Compression => { + self.compression = byte & 0x01 == 0x01; + if self.compression { + warnln!("Printer: Using compressed data, currently unsupported"); + } + } + PrinterState::LengthLow => self.length_left = byte as u16, + PrinterState::LengthHigh => self.length_left |= (byte as u16) << 8, + PrinterState::Data => { + self.data[self.command_length as usize] = byte; + self.command_length += 1; + self.length_left -= 1; + } + PrinterState::ChecksumLow => self.checksum = byte as u16, + PrinterState::ChecksumHigh => { + self.checksum |= (byte as u16) << 8; + self.byte_out = 0x81; + } + PrinterState::KeepAlive => { + self.run_command(self.command); + } + PrinterState::Status => { + self.state = PrinterState::MagicBytes1; + return; + } + PrinterState::Other => { + warnln!("Printer: Invalid state: {:02x}", self.state as u8); + return; + } + } + + if self.state != PrinterState::Data { + self.state = PrinterState::from_u8(self.state as u8 + 1); + } + + if self.state == PrinterState::Data && self.length_left == 0 { + self.state = PrinterState::from_u8(self.state as u8 + 1); + } + } + + fn allow_slave(&self) -> bool { + false + } +} + +impl Default for PrinterDevice { + fn default() -> Self { + Self::new() + } +} diff --git a/src/devices/stdout.rs b/src/devices/stdout.rs new file mode 100644 index 0000000000000000000000000000000000000000..a2f7528c0ba72eef722536923dc8f5c6651fe722 --- /dev/null +++ b/src/devices/stdout.rs @@ -0,0 +1,36 @@ +use std::io::{stdout, Write}; + +use crate::serial::SerialDevice; + +pub struct StdoutDevice { + flush: bool, +} + +impl StdoutDevice { + pub fn new(flush: bool) -> Self { + Self { flush } + } +} + +impl SerialDevice for StdoutDevice { + fn send(&mut self) -> u8 { + 0xff + } + + fn receive(&mut self, byte: u8) { + print!("{}", byte as char); + if self.flush { + stdout().flush().unwrap(); + } + } + + fn allow_slave(&self) -> bool { + false + } +} + +impl Default for StdoutDevice { + fn default() -> Self { + Self::new(true) + } +} diff --git a/src/gb.rs b/src/gb.rs index 5fc389dc5f42b3f1f09b86c1a0e84c4e67b24209..af0735a72d6f91991b4329749bd1564fc8767b50 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -2,11 +2,13 @@ use crate::{ apu::Apu, cpu::Cpu, data::{BootRom, CGB_BOOT, DMG_BOOT, DMG_BOOTIX, MGB_BOOTIX, SGB_BOOT}, + devices::{printer::PrinterDevice, stdout::StdoutDevice}, gen::{COMPILATION_DATE, COMPILATION_TIME, COMPILER, COMPILER_VERSION}, mmu::Mmu, pad::{Pad, PadKey}, ppu::{Ppu, PpuMode, Tile, FRAME_BUFFER_SIZE}, rom::Cartridge, + serial::{Serial, SerialDevice}, timer::Timer, util::read_file, }; @@ -44,6 +46,9 @@ pub struct GameBoy { /// If the timer is enabled, it will be clocked. timer_enabled: bool, + /// If the serial is enabled, it will be clocked. + serial_enabled: bool, + /// The current frequency at which the Game Boy /// emulator is being handled. This is a "hint" that /// may help components to adjust their internal @@ -86,13 +91,15 @@ impl GameBoy { let apu = Apu::default(); let pad = Pad::default(); let timer = Timer::default(); - let mmu = Mmu::new(ppu, apu, pad, timer); + let serial = Serial::default(); + let mmu = Mmu::new(ppu, apu, pad, timer, serial); let cpu = Cpu::new(mmu); Self { cpu, ppu_enabled: true, apu_enabled: true, timer_enabled: true, + serial_enabled: true, clock_freq: GameBoy::CPU_FREQ, } } @@ -100,6 +107,8 @@ impl GameBoy { pub fn reset(&mut self) { self.ppu().reset(); self.apu().reset(); + self.timer().reset(); + self.serial().reset(); self.mmu().reset(); self.cpu.reset(); } @@ -115,6 +124,9 @@ impl GameBoy { if self.timer_enabled { self.timer_clock(cycles); } + if self.serial_enabled { + self.serial_clock(cycles); + } cycles } @@ -142,6 +154,10 @@ impl GameBoy { self.timer().clock(cycles) } + pub fn serial_clock(&mut self, cycles: u8) { + self.serial().clock(cycles) + } + pub fn ppu_ly(&mut self) -> u8 { self.ppu().ly() } @@ -325,6 +341,14 @@ impl GameBoy { self.timer_enabled = value; } + pub fn serial_enabled(&self) -> bool { + self.serial_enabled + } + + pub fn set_serial_enabled(&mut self, value: bool) { + self.serial_enabled = value; + } + pub fn clock_freq(&self) -> u32 { self.clock_freq } @@ -333,6 +357,14 @@ impl GameBoy { self.clock_freq = value; self.apu().set_clock_freq(value); } + + pub fn attach_stdout_serial(&mut self) { + self.attach_serial(Box::<StdoutDevice>::default()); + } + + pub fn attach_printer_serial(&mut self) { + self.attach_serial(Box::<PrinterDevice>::default()); + } } /// Gameboy implementations that are meant with performance @@ -378,6 +410,10 @@ impl GameBoy { self.cpu.timer() } + pub fn serial(&mut self) -> &mut Serial { + self.cpu.serial() + } + pub fn frame_buffer(&mut self) -> &[u8; FRAME_BUFFER_SIZE] { &(self.ppu().frame_buffer) } @@ -423,6 +459,10 @@ impl GameBoy { let data = read_file(path); self.load_rom(&data) } + + pub fn attach_serial(&mut self, device: Box<dyn SerialDevice>) { + self.serial().set_device(device); + } } #[cfg(feature = "wasm")] diff --git a/src/lib.rs b/src/lib.rs index 7decfab2b80948c67ae0c8c3765191dda99b7743..80f4f5b8e995c1081e8319a0c9b396a73503375a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod apu; pub mod cpu; pub mod data; +pub mod devices; pub mod gb; pub mod gen; pub mod inst; @@ -11,5 +12,6 @@ pub mod mmu; pub mod pad; pub mod ppu; pub mod rom; +pub mod serial; pub mod timer; pub mod util; diff --git a/src/mmu.rs b/src/mmu.rs index bc4a9ddd43ca2d8a814f4d0af2c09bce448d2a6d..f57748c29388a5f66619880ef89397db8bb94f99 100644 --- a/src/mmu.rs +++ b/src/mmu.rs @@ -1,4 +1,4 @@ -use crate::{apu::Apu, debugln, pad::Pad, ppu::Ppu, rom::Cartridge, timer::Timer}; +use crate::{apu::Apu, debugln, pad::Pad, ppu::Ppu, rom::Cartridge, serial::Serial, timer::Timer}; pub const BOOT_SIZE: usize = 2304; pub const RAM_SIZE: usize = 8192; @@ -26,6 +26,10 @@ pub struct Mmu { /// that is memory mapped. timer: Timer, + /// The serial data transfer controller to be used to control the + /// link cable connection, this component is memory mapped. + serial: Serial, + /// The cartridge ROM that is currently loaded into the system, /// going to be used to access ROM and external RAM banks. rom: Cartridge, @@ -49,12 +53,13 @@ pub struct Mmu { } impl Mmu { - pub fn new(ppu: Ppu, apu: Apu, pad: Pad, timer: Timer) -> Self { + pub fn new(ppu: Ppu, apu: Apu, pad: Pad, timer: Timer, serial: Serial) -> Self { Self { ppu, apu, pad, timer, + serial, rom: Cartridge::new(), boot_active: true, boot: [0u8; BOOT_SIZE], @@ -91,6 +96,10 @@ impl Mmu { &mut self.timer } + pub fn serial(&mut self) -> &mut Serial { + &mut self.serial + } + pub fn boot_active(&self) -> bool { self.boot_active } @@ -141,6 +150,9 @@ impl Mmu { | 0xa00 | 0xb00 | 0xc00 | 0xd00 => self.ram[(addr & 0x1fff) as usize], 0xe00 => self.ppu.read(addr), 0xf00 => match addr & 0x00ff { + // 0xFF01-0xFF02 - Serial data transfer + 0x01..=0x02 => self.serial.read(addr), + // 0xFF0F — IF: Interrupt flag 0x0f => { @@ -148,6 +160,7 @@ impl Mmu { (if self.ppu.int_vblank() { 0x01 } else { 0x00 } | if self.ppu.int_stat() { 0x02 } else { 0x00 } | if self.timer.int_tima() { 0x04 } else { 0x00 } + | if self.serial.int_serial() { 0x08 } else { 0x00 } | if self.pad.int_pad() { 0x10 } else { 0x00 }) } @@ -216,11 +229,15 @@ impl Mmu { } 0xe00 => self.ppu.write(addr, value), 0xf00 => match addr & 0x00ff { + // 0xFF01-0xFF02 - Serial data transfer + 0x01..=0x02 => self.serial.write(addr, value), + // 0xFF0F — IF: Interrupt flag 0x0f => { self.ppu.set_int_vblank(value & 0x01 == 0x01); self.ppu.set_int_stat(value & 0x02 == 0x02); self.timer.set_int_tima(value & 0x04 == 0x04); + self.serial.set_int_serial(value & 0x08 == 0x08); self.pad.set_int_pad(value & 0x10 == 0x10); } diff --git a/src/pad.rs b/src/pad.rs index 36c10a1715f80741916a3d6dd55bce5e28d67532..f9e6ef97d3b6ab49edec7ea9f1a8f778958a6597 100644 --- a/src/pad.rs +++ b/src/pad.rs @@ -131,14 +131,17 @@ impl Pad { } } + #[inline(always)] pub fn int_pad(&self) -> bool { self.int_pad } + #[inline(always)] pub fn set_int_pad(&mut self, value: bool) { self.int_pad = value; } + #[inline(always)] pub fn ack_pad(&mut self) { self.set_int_pad(false); } diff --git a/src/ppu.rs b/src/ppu.rs index 651e0626170b091143e3721d8cf72f152be81fb6..99f6042cb14dd7b8fe9df3deb1e2bab20b3efa6c 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -14,6 +14,7 @@ pub const HRAM_SIZE: usize = 128; pub const OAM_SIZE: usize = 260; pub const PALETTE_SIZE: usize = 4; pub const RGB_SIZE: usize = 3; +pub const RGBA_SIZE: usize = 4; pub const TILE_WIDTH: usize = 8; pub const TILE_HEIGHT: usize = 8; pub const TILE_DOUBLE_HEIGHT: usize = 16; @@ -48,10 +49,18 @@ pub const PALETTE_COLORS: Palette = [[255, 255, 255], [192, 192, 192], [96, 96, /// with the size of RGB (3 bytes). pub type Pixel = [u8; RGB_SIZE]; +/// Defines a transparent Game Boy pixel type as a buffer +/// with the size of RGBA (4 bytes). +pub type PixelAlpha = [u8; RGBA_SIZE]; + /// Defines a type that represents a color palette /// within the Game Boy context. pub type Palette = [Pixel; PALETTE_SIZE]; +/// Defines a type that represents a color palette +/// with alpha within the Game Boy context. +pub type PaletteAlpha = [PixelAlpha; PALETTE_SIZE]; + /// Represents a palette with the metadata that is /// associated with it. #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -642,26 +651,32 @@ impl Ppu { self.frame_index } + #[inline(always)] pub fn int_vblank(&self) -> bool { self.int_vblank } + #[inline(always)] pub fn set_int_vblank(&mut self, value: bool) { self.int_vblank = value; } + #[inline(always)] pub fn ack_vblank(&mut self) { self.set_int_vblank(false); } + #[inline(always)] pub fn int_stat(&self) -> bool { self.int_stat } + #[inline(always)] pub fn set_int_stat(&mut self, value: bool) { self.int_stat = value; } + #[inline(always)] pub fn ack_stat(&mut self) { self.set_int_stat(false); } diff --git a/src/serial.rs b/src/serial.rs new file mode 100644 index 0000000000000000000000000000000000000000..cd8c53c90c8a4f9af93b766893df04ff8d080160 --- /dev/null +++ b/src/serial.rs @@ -0,0 +1,209 @@ +use crate::warnln; + +pub trait SerialDevice { + fn send(&mut self) -> u8; + fn receive(&mut self, byte: u8); + + /// Whether the serial device "driver" supports slave mode + /// simulating an external clock source. Or if instead the + /// clock should always be generated by the running device. + fn allow_slave(&self) -> bool; +} + +pub struct Serial { + data: u8, + control: u8, + shift_clock: bool, + clock_speed: bool, + transferring: bool, + timer: i16, + length: u16, + bit_count: u8, + byte_receive: u8, + int_serial: bool, + device: Box<dyn SerialDevice>, +} + +impl Serial { + pub fn new() -> Self { + Self { + data: 0x0, + control: 0x0, + shift_clock: false, + clock_speed: false, + transferring: false, + timer: 0, + length: 512, + bit_count: 0, + byte_receive: 0x0, + int_serial: false, + device: Box::<NullDevice>::default(), + } + } + + pub fn reset(&mut self) { + self.data = 0x0; + self.control = 0x0; + self.shift_clock = false; + self.clock_speed = false; + self.transferring = false; + self.timer = 0; + self.length = 512; + self.bit_count = 0; + self.byte_receive = 0x0; + self.int_serial = false; + } + + pub fn clock(&mut self, cycles: u8) { + if !self.transferring { + return; + } + + self.timer = self.timer.saturating_sub(cycles as i16); + if self.timer <= 0 { + let bit = (self.byte_receive >> (7 - self.bit_count)) & 0x01; + self.data = (self.data << 1) | bit; + + self.tick_transfer(); + + self.timer = self.length as i16; + } + } + + pub fn read(&mut self, addr: u16) -> u8 { + match addr & 0x00ff { + 0x01 => self.data, + 0x02 => { + (if self.shift_clock { 0x01 } else { 0x00 } + | if self.clock_speed { 0x02 } else { 0x00 } + | if self.transferring { 0x80 } else { 0x00 }) + } + _ => { + warnln!("Reding from unknown Serial location 0x{:04x}", addr); + 0xff + } + } + } + + pub fn write(&mut self, addr: u16, value: u8) { + match addr & 0x00ff { + 0x01 => self.data = value, + 0x02 => { + self.shift_clock = value & 0x01 == 0x01; + self.clock_speed = value & 0x02 == 0x02; + self.transferring = value & 0x80 == 0x80; + + // in case the clock is meant to be set by the attached device + // and the current Game Boy is meant to be running in slave mode + // then checks if the attached device "driver" allows clock set + // by external device and if not then ignores the transfer request + // by immediately disabling the transferring flag + if !self.shift_clock && !self.device.allow_slave() { + self.transferring = false; + } + + // in case a transfer of byte has been requested and + // this is the then we need to start the transfer setup + if self.transferring { + // @TODO: if the GBC mode exists there should + // be special check logic here + //self.length = if self.gb.is_cgb() && self.clock_speed { 16 } else { 512 }; + self.length = 512; + self.bit_count = 0; + self.timer = self.length as i16; + + // executes the send and receive operation immediately + // this is considered an operational optimization with + // no real effect on the emulation (ex: not timing issues) + self.byte_receive = self.device.send(); + self.device.receive(self.data); + } + } + _ => warnln!("Writing to unknown Serial location 0x{:04x}", addr), + } + } + + pub fn send(&self) -> bool { + if self.shift_clock { + true + } else { + self.data & 0x80 == 0x80 + } + } + + pub fn receive(&mut self, bit: bool) { + if !self.shift_clock { + self.data = (self.data << 1) | bit as u8; + self.tick_transfer(); + } + } + + #[inline(always)] + pub fn int_serial(&self) -> bool { + self.int_serial + } + + #[inline(always)] + pub fn set_int_serial(&mut self, value: bool) { + self.int_serial = value; + } + + #[inline(always)] + pub fn ack_serial(&mut self) { + self.set_int_serial(false); + } + + pub fn device(&self) -> &dyn SerialDevice { + self.device.as_ref() + } + + pub fn set_device(&mut self, device: Box<dyn SerialDevice>) { + self.device = device; + } + + fn tick_transfer(&mut self) { + self.bit_count += 1; + if self.bit_count == 8 { + self.transferring = false; + self.length = 0; + self.bit_count = 0; + + // signals the interrupt for the serial + // transfer completion, indicating that + // a new byte is ready to be read + self.int_serial = true; + } + } +} + +impl Default for Serial { + fn default() -> Self { + Self::new() + } +} + +pub struct NullDevice {} + +impl NullDevice { + pub fn new() -> Self { + Self {} + } +} + +impl SerialDevice for NullDevice { + fn send(&mut self) -> u8 { + 0xff + } + + fn receive(&mut self, _: u8) {} + + fn allow_slave(&self) -> bool { + false + } +} + +impl Default for NullDevice { + fn default() -> Self { + Self::new() + } +} diff --git a/src/timer.rs b/src/timer.rs index 537c14c6714406aa2045ab6a578a8f121f59a119..e423663620d9d65a02d48345a681b45a27d9c4fe 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -100,14 +100,17 @@ impl Timer { } } + #[inline(always)] pub fn int_tima(&self) -> bool { self.int_tima } + #[inline(always)] pub fn set_int_tima(&mut self, value: bool) { self.int_tima = value; } + #[inline(always)] pub fn ack_tima(&mut self) { self.set_int_tima(false); }