diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2b41f641926786a69f6c0b73accda78945171d..d65e876308247c70c589c920c4258457b9c370d8 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 sprite drawing, works wit Tetris +* Support for sprite drawing, works with Tetris +* Support for timers ### Changed diff --git a/examples/sdl/src/main.rs b/examples/sdl/src/main.rs index 2c916122b4e30b75f89b4b75335f7aae23769f08..2e236739c81b0488159c8132e66c0d5408420ba3 100644 --- a/examples/sdl/src/main.rs +++ b/examples/sdl/src/main.rs @@ -34,7 +34,7 @@ fn start_sdl() -> Graphics { // creates the system window that is going to be used to // show the emulator and sets it to the central are o screen - let mut window = video_subsystem + let window = video_subsystem .window( TITLE, 2 as u32 * DISPLAY_WIDTH as u32, //@todo check screen scale @@ -84,7 +84,8 @@ fn main() { let mut game_boy = GameBoy::new(); game_boy.load_boot_default(); - game_boy.load_rom_file("../../res/roms.prop/tetris.gb"); + //game_boy.load_rom_file("../../res/roms.prop/tetris.gb"); + game_boy.load_rom_file("../../res/roms.prop/dr_mario.gb"); //game_boy.load_rom_file("../../res/roms.prop/alleyway.gb"); //game_boy.load_rom_file("../../res/roms/firstwhite.gb"); diff --git a/src/cpu.rs b/src/cpu.rs index 1e29d760c946baf923b91ef179fcb6b9b657f110..9d538684346b700d201f9e220673d215ea31d877 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -5,6 +5,7 @@ use crate::{ mmu::Mmu, pad::Pad, ppu::Ppu, + timer::Timer, }; pub const PREFIX: u8 = 0xcb; @@ -129,6 +130,27 @@ impl Cpu { return 16; } + + // @todo aggregate the handling of these interrupts + if (self.mmu.ie & 0x04 == 0x04) && self.mmu.timer().int_tima() { + println!("Going to run Timer interrupt handler (0x50)"); + + self.disable_int(); + self.push_word(pc); + self.pc = 0x50; + + // acknowledges that the timer interrupt has been + // properly handled + self.mmu.timer().ack_tima(); + + // in case the CPU is currently halted waiting + // for an interrupt, releases it + if self.halted { + self.halted = false; + } + + return 16; + } } // in case the CPU is currently in the halted state @@ -192,6 +214,11 @@ impl Cpu { self.mmu().pad() } + #[inline(always)] + pub fn timer(&mut self) -> &mut Timer { + self.mmu().timer() + } + #[inline(always)] pub fn halted(&self) -> bool { self.halted diff --git a/src/data.rs b/src/data.rs index c9330da6e7c417932c73e6ade7f54d794db89d5f..c77a3e8e5cc6a85a32c0a09832a9d8e89e14cd2e 100644 --- a/src/data.rs +++ b/src/data.rs @@ -9,8 +9,6 @@ pub enum BootRom { MgbBootix, } -/// Static data corresponding to the DMG boot ROM -/// allows freely using the emulator without external dependency. pub const DMG_BOOT: [u8; 256] = [ 49, 254, 255, 175, 33, 255, 159, 50, 203, 124, 32, 251, 33, 38, 255, 14, 17, 62, 128, 50, 226, 12, 62, 243, 226, 50, 62, 119, 119, 62, 252, 224, 71, 17, 4, 1, 33, 16, 128, 26, 205, 149, 0, diff --git a/src/gb.rs b/src/gb.rs index b58146b668921115368ef86be340f7302646fa51..acfd5d9825dea000a104bef7fed1ce42213fd263 100644 --- a/src/gb.rs +++ b/src/gb.rs @@ -4,6 +4,7 @@ use crate::{ mmu::Mmu, pad::{Pad, PadKey}, ppu::{Ppu, Tile, FRAME_BUFFER_SIZE}, + timer::Timer, util::read_file, }; @@ -22,9 +23,10 @@ pub struct GameBoy { impl GameBoy { #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))] pub fn new() -> Self { - let pad = Pad::new(); let ppu = Ppu::new(); - let mmu = Mmu::new(ppu, pad); + let pad = Pad::new(); + let timer = Timer::new(); + let mmu = Mmu::new(ppu, pad, timer); let cpu = Cpu::new(mmu); Self { cpu: cpu } } @@ -50,6 +52,7 @@ impl GameBoy { pub fn clock(&mut self) -> u8 { let cycles = self.cpu_clock(); self.ppu_clock(cycles); + self.timer_clock(cycles); cycles } @@ -61,6 +64,10 @@ impl GameBoy { self.ppu().clock(cycles) } + pub fn timer_clock(&mut self, cycles: u8) { + self.timer().clock(cycles) + } + pub fn load_rom(&mut self, data: &[u8]) { self.cpu.mmu().write_rom(0x0000, data); } @@ -144,6 +151,10 @@ impl GameBoy { self.cpu.pad() } + pub fn timer(&mut self) -> &mut Timer { + self.cpu.timer() + } + pub fn frame_buffer(&mut self) -> &Box<[u8; FRAME_BUFFER_SIZE]> { &(self.ppu().frame_buffer) } diff --git a/src/lib.rs b/src/lib.rs index cd9b622a2584ae2037e005e24466eedd594e1cfd..11af82a5047fa0b409b5bbf4eccfad99daa9bec5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,4 +5,5 @@ pub mod inst; pub mod mmu; pub mod pad; pub mod ppu; +pub mod timer; pub mod util; diff --git a/src/mmu.rs b/src/mmu.rs index a64f6a6aca548f381c13b93907e3d366ab6ed516..e3dae1ddcfd9fe68afa9179e48b6ca229dcc045e 100644 --- a/src/mmu.rs +++ b/src/mmu.rs @@ -1,4 +1,4 @@ -use crate::{pad::Pad, ppu::Ppu}; +use crate::{pad::Pad, ppu::Ppu, timer::Timer}; pub const BIOS_SIZE: usize = 256; pub const ROM_SIZE: usize = 32768; @@ -6,24 +6,29 @@ pub const RAM_SIZE: usize = 8192; pub const ERAM_SIZE: usize = 8192; pub struct Mmu { + /// Register that controls the interrupts that are considered + /// to be enabled and should be triggered. + pub ie: u8, + + /// Reference to the PPU (Pixel Processing Unit) that is going + /// to be used both for VRAM reading/writing and to forward + /// some of the access operations. ppu: Ppu, pad: Pad, + timer: Timer, boot_active: bool, boot: [u8; BIOS_SIZE], rom: [u8; ROM_SIZE], ram: [u8; RAM_SIZE], eram: [u8; RAM_SIZE], - - /// Registers that controls the interrupts that are considered - /// to be enabled and should be triggered. - pub ie: u8, } impl Mmu { - pub fn new(ppu: Ppu, pad: Pad) -> Self { + pub fn new(ppu: Ppu, pad: Pad, timer: Timer) -> Self { Self { ppu: ppu, pad: pad, + timer: timer, boot_active: true, boot: [0u8; BIOS_SIZE], rom: [0u8; ROM_SIZE], @@ -49,6 +54,10 @@ impl Mmu { &mut self.pad } + pub fn timer(&mut self) -> &mut Timer { + &mut self.timer + } + pub fn boot_active(&self) -> bool { self.boot_active } @@ -87,28 +96,30 @@ impl Mmu { 0x000 | 0x100 | 0x200 | 0x300 | 0x400 | 0x500 | 0x600 | 0x700 | 0x800 | 0x900 | 0xa00 | 0xb00 | 0xc00 | 0xd00 => self.ram[(addr & 0x1fff) as usize], 0xe00 => self.ppu.oam[(addr & 0x009f) as usize], - 0xf00 => { - if addr == 0xffff { - self.ie - } else if addr >= 0xff80 { - self.ppu.hram[(addr & 0x007f) as usize] - } else { - match addr & 0x00f0 { - 0x00 => match addr & 0x00ff { - 0x00 => self.pad.read(addr), - _ => { - println!("Reading from unknown IO control 0x{:04x}", addr); - 0x00 - } - }, - 0x40 | 0x50 | 0x60 | 0x70 => self.ppu.read(addr), + 0xf00 => match addr & 0x00ff { + 0x0f => { + let value = if self.ppu.int_vblank() { 0x01 } else { 0x00 } + | if self.timer.int_tima() { 0x04 } else { 0x00 }; + value + } + 0x80..=0xfe => self.ppu.hram[(addr & 0x007f) as usize], + 0xff => self.ie, + _ => match addr & 0x00f0 { + 0x00 => match addr & 0x00ff { + 0x00 => self.pad.read(addr), + 0x04..=0x07 => self.timer.read(addr), _ => { println!("Reading from unknown IO control 0x{:04x}", addr); 0x00 } + }, + 0x40 | 0x50 | 0x60 | 0x70 => self.ppu.read(addr), + _ => { + println!("Reading from unknown IO control 0x{:04x}", addr); + 0x00 } - } - } + }, + }, addr => panic!("Reading from unknown location 0x{:04x}", addr), }, addr => panic!("Reading from unknown location 0x{:04x}", addr), @@ -159,15 +170,18 @@ impl Mmu { self.ppu.oam[(addr & 0x009f) as usize] = value; self.ppu.update_object(addr, value); } - 0xf00 => { - if addr == 0xffff { - self.ie = value; - } else if addr >= 0xff80 { - self.ppu.hram[(addr & 0x007f) as usize] = value; - } else { + 0xf00 => match addr & 0x00ff { + 0x0f => { + self.ppu.set_int_vblank(value & 0x01 == 0x01); + self.timer.set_int_tima(value & 0x04 == 0x04); + } + 0x80..=0xfe => self.ppu.hram[(addr & 0x007f) as usize] = value, + 0xff => self.ie = value, + _ => { match addr & 0x00f0 { 0x00 => match addr & 0x00ff { 0x00 => self.pad.write(addr, value), + 0x04..=0x07 => self.timer.write(addr, value), _ => println!("Writing to unknown IO control 0x{:04x}", addr), }, 0x40 | 0x60 | 0x70 => { @@ -189,7 +203,7 @@ impl Mmu { _ => println!("Writing to unknown IO control 0x{:04x}", addr), } } - } + }, addr => panic!("Writing in unknown location 0x{:04x}", addr), }, addr => panic!("Writing in unknown location 0x{:04x}", addr), diff --git a/src/ppu.rs b/src/ppu.rs index 6cb2b802f9487f591eea7ee7747c0d94afd69bfa..c2073b2fde7864f4103b46f4840f55cf6669964f 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -327,22 +327,22 @@ impl Ppu { match self.mode { PpuMode::OamRead => { if self.mode_clock >= 80 { - self.mode_clock = 0; self.mode = PpuMode::VramRead; + self.mode_clock = self.mode_clock - 80; } } PpuMode::VramRead => { if self.mode_clock >= 172 { self.render_line(); - self.mode_clock = 0; self.mode = PpuMode::HBlank; + self.mode_clock = self.mode_clock - 172; } } PpuMode::HBlank => { if self.mode_clock >= 204 { // increments the register that holds the - // information about the current line in drawign + // information about the current line in drawing self.ly += 1; // in case we've reached the end of the @@ -354,7 +354,7 @@ impl Ppu { self.mode = PpuMode::OamRead; } - self.mode_clock = 0; + self.mode_clock = self.mode_clock - 204; } } PpuMode::VBlank => { @@ -369,7 +369,7 @@ impl Ppu { self.ly = 0; } - self.mode_clock = 0; + self.mode_clock = self.mode_clock - 456; } } } @@ -498,6 +498,10 @@ impl Ppu { self.int_vblank } + pub fn set_int_vblank(&mut self, value: bool) { + self.int_vblank = value; + } + pub fn ack_vblank(&mut self) { self.int_vblank = false; } @@ -705,13 +709,13 @@ impl Ppu { // the object is only considered visible if it's a priority // or if the underlying pixel is transparent (zero value) let is_visible = obj.priority || self.color_buffer[color_offset] == 0; - if is_visible { + let pixel = tile_row[if obj.xflip { 7 - x } else { x }]; + if is_visible && pixel != 0 { // obtains the current pixel data from the tile row and // re-maps it according to the object palette - let pixel = tile_row[if obj.xflip { 7 - x } else { x }]; let color = palette[pixel as usize]; - // set the color pixel in the frame buffer + // sets the color pixel in the frame buffer self.frame_buffer[frame_offset] = color[0]; self.frame_buffer[frame_offset + 1] = color[1]; self.frame_buffer[frame_offset + 2] = color[2]; diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000000000000000000000000000000000000..0169e9e8ceec2700463b66134338174179ef1c15 --- /dev/null +++ b/src/timer.rs @@ -0,0 +1,89 @@ +pub struct Timer { + div: u8, + tima: u8, + tma: u8, + tac: u8, + div_clock: u16, + tima_clock: u16, + tima_enabled: bool, + tima_ratio: u16, + int_tima: bool, +} + +impl Timer { + pub fn new() -> Self { + Self { + div: 0, + tima: 0, + tma: 0, + tac: 0x0, + div_clock: 0, + tima_clock: 0, + tima_enabled: false, + tima_ratio: 1024, + int_tima: false, + } + } + + pub fn clock(&mut self, cycles: u8) { + self.div_clock += cycles as u16; + self.tima_clock += cycles as u16; + + if self.div_clock >= 256 { + self.div = self.div.wrapping_add(1); + self.div_clock = self.div_clock - 256; + } + + if self.tima_enabled && self.tima_clock >= self.tima_ratio { + if self.tima == 0xff { + self.int_tima = true; + self.tima = self.tma; + } + + self.tima = self.tima.wrapping_add(1); + self.tima_clock = self.tima_clock - self.tima_ratio; + } + } + + pub fn read(&mut self, addr: u16) -> u8 { + match addr & 0x00ff { + 0x04 => self.div, + 0x05 => self.tima, + 0x06 => self.tma, + 0x07 => self.tac, + addr => panic!("Reding from unknown Timer location 0x{:04x}", addr), + } + } + + pub fn write(&mut self, addr: u16, value: u8) { + match addr & 0x00ff { + 0x04 => self.div = 0, + 0x05 => self.tima = value, + 0x06 => self.tma = value, + 0x07 => { + self.tac = value; + match value & 0x03 { + 0x00 => self.tima_ratio = 1024, + 0x01 => self.tima_ratio = 16, + 0x02 => self.tima_ratio = 64, + 0x03 => self.tima_ratio = 256, + value => panic!("Invalid TAC value 0x{:02x}", value), + } + self.tima_enabled = value & 0x04 == 0x04; + } + addr => panic!("Writing to unknown Timer location 0x{:04x}", addr), + } + } + + pub fn int_tima(&self) -> bool { + self.int_tima + } + + pub fn set_int_tima(&mut self, value: bool) { + self.int_tima = value; + } + + pub fn ack_tima(&mut self) { + self.int_tima = false; + } +}