diff --git a/Cargo.lock b/Cargo.lock index caf42da..1048ebf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,33 +74,52 @@ dependencies = [ ] [[package]] -name = "argh" -version = "0.1.6" +name = "anstream" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f023c76cd7975f9969f8e29f0e461decbdc7f51048ce43427107a3d192f1c9bf" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" dependencies = [ - "argh_derive", - "argh_shared", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", ] [[package]] -name = "argh_derive" -version = "0.1.6" +name = "anstyle" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ad219abc0c06ca788aface2e3a1970587e3413ab70acd20e54b6ec524c1f8f" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" dependencies = [ - "argh_shared", - "heck", - "proc-macro2", - "quote", - "syn 1.0.82", + "utf8parse", ] [[package]] -name = "argh_shared" -version = "0.1.6" +name = "anstyle-query" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] [[package]] name = "arrayvec" @@ -125,7 +144,7 @@ checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn", ] [[package]] @@ -309,6 +328,46 @@ dependencies = [ "winapi", ] +[[package]] +name = "clap" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -319,6 +378,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "com-rs" version = "0.2.1" @@ -473,7 +538,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.43", + "syn", ] [[package]] @@ -484,16 +549,16 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core", "quote", - "syn 2.0.43", + "syn", ] [[package]] name = "deemgee" version = "0.1.0" dependencies = [ - "argh", "bmp", "chrono", + "clap", "config", "deemgee-opcode", "env_logger", @@ -514,7 +579,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.43", + "syn", ] [[package]] @@ -604,7 +669,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn", ] [[package]] @@ -745,12 +810,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" @@ -1100,7 +1162,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.43", + "syn", ] [[package]] @@ -1246,7 +1308,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.43", + "syn", ] [[package]] @@ -1495,7 +1557,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn", ] [[package]] @@ -1586,17 +1648,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "syn" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - [[package]] name = "syn" version = "2.0.43" @@ -1634,7 +1685,7 @@ checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn", ] [[package]] @@ -1718,6 +1769,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "version_check" version = "0.9.4" @@ -1768,7 +1825,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.43", + "syn", "wasm-bindgen-shared", ] @@ -1802,7 +1859,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.43", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/deemgee-opcode/src/lib.rs b/deemgee-opcode/src/lib.rs index 7014dd2..c0f8c02 100644 --- a/deemgee-opcode/src/lib.rs +++ b/deemgee-opcode/src/lib.rs @@ -76,7 +76,7 @@ pub fn opcode(item: TokenStream) -> TokenStream { }*/ let regs = quote::quote! { - log::info!("\nSTART OF {}\n-- Registers --\nAF: {:04X}\nBC: {:04X}\nDE: {:04X}\nHL: {:04X}\nSP: {:04X}\nPC: {:04X}\nZero: {}\nSubtract: {}\nHalf-Carry: {}\nCarry: {}\n-- Interrupts --\nIME: {}\nIE VBlank: {}\nIE LCD Stat: {}\nIE Timer: {}\nIE Serial: {}\nIE Joypad: {}\nIF VBlank: {}\nIF LCD Stat: {}\nIF Timer: {}\nIF Serial: {}\nIF Joypad: {}\nEND OF {}", #name_s, state.registers.get_af(), state.registers.get_bc(), state.registers.get_de(), state.registers.get_hl(), state.registers.get_sp(), state.registers.pc, state.registers.get_zero(), state.registers.get_subtract(), state.registers.get_half_carry(), state.registers.get_carry(), state.interrupts.ime, state.interrupts.read_ie_vblank(), state.interrupts.read_ie_lcd_stat(), state.interrupts.read_ie_timer(), state.interrupts.read_ie_serial(), state.interrupts.read_ie_joypad(), state.interrupts.read_if_vblank(), state.interrupts.read_if_lcd_stat(), state.interrupts.read_if_timer(), state.interrupts.read_if_serial(), state.interrupts.read_if_joypad(), #name_s); + log::debug!("\nSTART OF {}\n-- Registers --\nAF: {:04X}\nBC: {:04X}\nDE: {:04X}\nHL: {:04X}\nSP: {:04X}\nPC: {:04X}\nZero: {}\nSubtract: {}\nHalf-Carry: {}\nCarry: {}\n-- Interrupts --\nIME: {}\nIE VBlank: {}\nIE LCD Stat: {}\nIE Timer: {}\nIE Serial: {}\nIE Joypad: {}\nIF VBlank: {}\nIF LCD Stat: {}\nIF Timer: {}\nIF Serial: {}\nIF Joypad: {}\nEND OF {}", #name_s, state.registers.get_af(), state.registers.get_bc(), state.registers.get_de(), state.registers.get_hl(), state.registers.get_sp(), state.registers.pc, state.registers.get_zero(), state.registers.get_subtract(), state.registers.get_half_carry(), state.registers.get_carry(), state.interrupts.ime, state.interrupts.read_ie_vblank(), state.interrupts.read_ie_lcd_stat(), state.interrupts.read_ie_timer(), state.interrupts.read_ie_serial(), state.interrupts.read_ie_joypad(), state.interrupts.read_if_vblank(), state.interrupts.read_if_lcd_stat(), state.interrupts.read_if_timer(), state.interrupts.read_if_serial(), state.interrupts.read_if_joypad(), #name_s); }; let match_statement = quote::quote! { diff --git a/deemgee/Cargo.toml b/deemgee/Cargo.toml index 282eb10..7100d92 100644 --- a/deemgee/Cargo.toml +++ b/deemgee/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -argh = "0.1.6" bmp = "0.5.0" chrono = "0.4.19" +clap = { version = "4.4.12", features = ["derive"] } config = "0.13.4" deemgee-opcode = { path = "../deemgee-opcode" } env_logger = "0.10.1" diff --git a/deemgee/src/gameboy.rs b/deemgee/src/gameboy.rs index 0c3f29e..258c1d5 100644 --- a/deemgee/src/gameboy.rs +++ b/deemgee/src/gameboy.rs @@ -1,7 +1,7 @@ mod cpu; mod interrupts; mod joypad; -mod mapper; +pub mod mapper; mod memory; mod ppu; mod serial; @@ -40,12 +40,83 @@ impl DmaState { } } +pub struct RingBuffer { + buffer: [T; SIZE], + size: usize, + write_ptr: usize, + read_ptr: usize, +} + +impl RingBuffer { + pub fn new() -> Self { + RingBuffer { buffer: [T::default(); SIZE], size: 0, write_ptr: 0, read_ptr: 0 } + } + + pub fn push(&mut self, value: T) { + self.buffer[self.write_ptr] = value; + if self.size < SIZE { + self.size += 1; + } else { + self.read_ptr += 1; + self.read_ptr %= SIZE; + } + self.write_ptr += 1; + self.write_ptr %= SIZE; + } + + pub fn to_vec(&self) -> Vec { + let mut out = Vec::new(); + let mut offset = self.read_ptr; + + for _ in 0..self.size { + out.push(self.buffer[offset]); + + offset += 1; + offset %= SIZE; + } + + out + } +} + +#[test] +fn test_ringbuffer() { + let mut ringbuffer: RingBuffer = RingBuffer::new(); + + for x in 0..16 { + ringbuffer.push(x); + } + + assert_eq!( + ringbuffer.to_vec().as_slice(), + &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + ); + ringbuffer.push(16); + assert_eq!( + ringbuffer.to_vec().as_slice(), + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + ); + ringbuffer.push(17); + assert_eq!( + ringbuffer.to_vec().as_slice(), + &[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] + ); + + for x in 18..32 { + ringbuffer.push(x); + } + assert_eq!( + ringbuffer.to_vec().as_slice(), + &[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31] + ); +} + pub struct Gameboy { pub ppu: Ppu, - memory: Memory, - cartridge: Option>, - interrupts: Interrupts, - timer: Timer, + pub memory: Memory, + pub cartridge: Option>, + pub interrupts: Interrupts, + pub timer: Timer, pub registers: Registers, pub joypad: Joypad, pub serial: Serial, @@ -61,10 +132,13 @@ pub struct Gameboy { pub halt: bool, pub halt_bug: bool, pub used_halt_bug: bool, + pub stop: bool, + + pub pc_history: RingBuffer, } impl Gameboy { - pub fn new(bootrom: [u8; 0x100]) -> Self { + pub fn new(bootrom: Option<[u8; 0x100]>) -> Self { Self { memory: Memory::new(bootrom), cartridge: None, @@ -74,7 +148,10 @@ impl Gameboy { serial: Serial::new(), dma: DmaState::new(), ppu: Ppu::new(), - registers: Registers::default(), + registers: match bootrom.is_some() { + true => Registers::default(), + false => Registers::post_rom(), + }, sound: Sound::default(), single_step: false, breakpoints: [false; u16::MAX as usize + 1], @@ -85,6 +162,8 @@ impl Gameboy { halt: false, halt_bug: false, used_halt_bug: false, + stop: false, + pc_history: RingBuffer::new(), } } @@ -112,6 +191,8 @@ impl Gameboy { match bytes[0x147] { 0 => self.cartridge = Some(Box::new(NoMBC::new(bytes))), 1 => self.cartridge = Some(Box::new(MBC1::new(bytes))), + 2 => self.cartridge = Some(Box::new(MBC1::new(bytes))), + 3 => self.cartridge = Some(Box::new(MBC1::new(bytes))), other => unimplemented!("Cartidge type: {:#X}", other), } } @@ -120,13 +201,16 @@ impl Gameboy { log::info!("\n-- Registers --\nAF: {:04X}\nBC: {:04X}\nDE: {:04X}\nHL: {:04X}\nSP: {:04X}\nPC: {:04X}\nZero: {}\nSubtract: {}\nHalf-Carry: {}\nCarry: {}\n-- Interrupts --\nIME: {}\nIE VBlank: {}\nIE LCD Stat: {}\nIE Timer: {}\nIE Serial: {}\nIE Joypad: {}\nIF VBlank: {}\nIF LCD Stat: {}\nIF Timer: {}\nIF Serial: {}\nIF Joypad: {}\n", self.registers.get_af(), self.registers.get_bc(), self.registers.get_de(), self.registers.get_hl(), self.registers.get_sp(), self.registers.pc, self.registers.get_zero(), self.registers.get_subtract(), self.registers.get_half_carry(), self.registers.get_carry(), self.interrupts.ime, self.interrupts.read_ie_vblank(), self.interrupts.read_ie_lcd_stat(), self.interrupts.read_ie_timer(), self.interrupts.read_ie_serial(), self.interrupts.read_ie_joypad(), self.interrupts.read_if_vblank(), self.interrupts.read_if_lcd_stat(), self.interrupts.read_if_timer(), self.interrupts.read_if_serial(), self.interrupts.read_if_joypad()); } - pub fn tick(&mut self) -> bool { + pub fn tick(&mut self) -> (bool, Option) { if self.breakpoints[self.registers.pc as usize] && !self.single_step { self.single_step = true; log::info!("Breakpoint hit @ {:#X}", self.registers.pc); } + let mut diff = None; + if self.trigger_bp || (self.single_step && self.registers.cycle == 0) { + let entered_step = chrono::Utc::now(); self.trigger_bp = false; self.single_step = true; let mut input = String::new(); @@ -189,11 +273,21 @@ impl Gameboy { log::info!("Continuing"); exit = false; } + "timer" => { + println!("-- Timer Info --\n{:#?}\n-- End of Timer Info --", self.timer) + } "p" | "pause" => { self.single_step = true; log::info!("Single step activated"); exit = false; } + "pch" => { + println!("-- Start of PC History (new to old) --"); + for (idx, pc) in self.pc_history.to_vec().iter().rev().enumerate() { + println!("{}: {:#04X}", idx + 1, pc); + } + println!("-- End of PC History --"); + } "s" | "step" | "" => { self.log_next_opcode(); exit = false; @@ -208,6 +302,17 @@ impl Gameboy { "dumpfb" => { println!("Written to: {}", self.ppu.dump_fb()); } + "dumpoam" => { + for x in 0..self.ppu.oam.len() { + if x % 0x10 == 0 { + print!("\n{:X}: ", 0xFE00 + x) + } + + let mem_val = self.ppu.oam[x]; + print!("{:02X} ", mem_val); + } + println!(); + } "dumpvram" => { for x in 0..0x200 { if x % 0x10 == 0 { @@ -241,8 +346,9 @@ impl Gameboy { Err(stdin_err) => panic!("Failed to lock stdin: {:?}", stdin_err), } + diff = Some((chrono::Utc::now() - entered_step).num_milliseconds()); if exit { - return false; + return (false, diff); } } if self.timer.tick() { @@ -255,7 +361,7 @@ impl Gameboy { if self.serial.tick() { self.interrupts.write_if_serial(true); } - redraw_requested + (redraw_requested, diff) } fn tick_dma(&mut self) { @@ -271,6 +377,12 @@ impl Gameboy { } } else if self.dma.base <= 0x9F { self.ppu.dma_read_vram(offset) + } else if self.dma.base <= 0xDF { + let address = (self.dma.base as u16) << 8 | offset as u16; + self.memory.wram[address as usize - 0xC000] + } else if self.dma.base <= 0xFD { + let address = (self.dma.base as u16) << 8 | offset as u16; + self.memory.wram[address as usize - 0xE000] } else { 0xFF }; @@ -285,19 +397,19 @@ impl Gameboy { 0xFF00 => self.joypad.cpu_read(), 0xFF01 => self.serial.sb, 0xFF02 => self.serial.sc, - 0xFF03 => 0, // Unused + 0xFF03 => 0xFF, // Unused 0xFF04 => self.timer.div, 0xFF05 => self.timer.tima, 0xFF06 => self.timer.tma, 0xFF07 => self.timer.read_tac(), - 0xFF08..=0xFF0E => 0, // Unused + 0xFF08..=0xFF0E => 0xFF, // Unused 0xFF0F => self.interrupts.interrupt_flag, 0xFF10 => self.sound.nr10, 0xFF11 => self.sound.nr11, 0xFF12 => self.sound.nr12, 0xFF13 => self.sound.nr13, 0xFF14 => self.sound.nr14, - 0xFF15 => 0, + 0xFF15 => 0xFF, 0xFF16 => self.sound.nr21, 0xFF17 => self.sound.nr22, 0xFF18 => self.sound.nr23, @@ -307,7 +419,7 @@ impl Gameboy { 0xFF1C => self.sound.nr32, 0xFF1D => self.sound.nr33, 0xFF1E => self.sound.nr34, - 0xFF1F => 0, + 0xFF1F => 0xFF, 0xFF20 => self.sound.nr41, 0xFF21 => self.sound.nr42, 0xFF22 => self.sound.nr43, @@ -315,7 +427,7 @@ impl Gameboy { 0xFF24 => self.sound.nr50, 0xFF25 => self.sound.nr51, 0xFF26 => self.sound.nr52, - 0xFF27..=0xFF2F => 0, + 0xFF27..=0xFF2F => 0xFF, 0xFF30..=0xFF3F => self.sound.wave_pattern_ram[address as usize - 0xFF30], 0xFF40 => self.ppu.lcdc, 0xFF41 => self.ppu.stat, @@ -324,18 +436,18 @@ impl Gameboy { 0xFF44 => self.ppu.ly, 0xFF45 => self.ppu.lyc, 0xFF46 => self.dma.base, - 0xFF47..=0xFF49 => 0, + 0xFF47..=0xFF49 => 0xFF, 0xFF4A => self.ppu.wy, 0xFF4B => self.ppu.wx, - 0xFF4C..=0xFF4E => 0, // Unused - 0xFF4F => 0, // CGB VRAM Bank Select + 0xFF4C..=0xFF4E => 0xFF, // Unused + 0xFF4F => 0xFF, // CGB VRAM Bank Select 0xFF50 => self.memory.bootrom_disabled as u8, - 0xFF51..=0xFF55 => 0, // CGB VRAM DMA - 0xFF56..=0xFF67 => 0, // Unused - 0xFF68..=0xFF69 => 0, // BJ/OBJ Palettes - 0xFF6A..=0xFF6F => 0, // Unused - 0xFF70 => 0, // CGB WRAM Bank Select - 0xFF71..=0xFF7F => 0, // Unused + 0xFF51..=0xFF55 => 0xFF, // CGB VRAM DMA + 0xFF56..=0xFF67 => 0xFF, // Unused + 0xFF68..=0xFF69 => 0xFF, // BJ/OBJ Palettes + 0xFF6A..=0xFF6F => 0xFF, // Unused + 0xFF70 => 0xFF, // CGB WRAM Bank Select + 0xFF71..=0xFF7F => 0xFF, // Unused _ => unreachable!("IO Read Invalid"), } } @@ -346,12 +458,12 @@ impl Gameboy { 0xFF01 => self.serial.sb = value, 0xFF02 => self.serial.sc = value, 0xFF03 => {} // Unused - 0xFF04 => self.timer.div = value, + 0xFF04 => self.timer.div = 0, 0xFF05 => self.timer.tima = value, 0xFF06 => self.timer.tma = value, 0xFF07 => self.timer.write_tac(value), 0xFF08..=0xFF0E => {} // Unused - 0xFF0F => self.interrupts.interrupt_flag = value & 0b1_1111, + 0xFF0F => self.interrupts.interrupt_flag = value | !0b1_1111, 0xFF10 => self.sound.nr10 = value, 0xFF11 => self.sound.nr11 = value, 0xFF12 => self.sound.nr12 = value, @@ -412,30 +524,63 @@ impl Gameboy { let mut out = [0u8; 0xFFFF]; for address in 0..0xFFFF { - out[address as usize] = match address { - 0..=0xFF if !self.memory.bootrom_disabled => self.memory.bootrom[address as usize], - 0..=0x7FFF => match self.cartridge.as_ref() { - Some(mapper) => mapper.read_rom_u8(address), - None => 0, - }, - 0x8000..=0x9FFF => self.ppu.cpu_read_vram(address), - 0xA000..=0xBFFF => match self.cartridge.as_ref() { - Some(mapper) => mapper.read_eram_u8(address), - None => 0, - }, - 0xC000..=0xDFFF => self.memory.wram[address as usize - 0xC000], - 0xE000..=0xFDFF => self.memory.wram[address as usize - 0xE000], - 0xFE00..=0xFE9F => self.ppu.cpu_read_oam(address), - 0xFEA0..=0xFEFF => 0, - 0xFF00..=0xFF7F => self.cpu_read_io(address), - 0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80], - 0xFFFF => self.interrupts.interrupt_enable, - }; + out[address as usize] = self.debug_read_u8(address); } out } + /// Warning: This bypasses the memory bus and only exists for + /// debugging/testing purposes + pub fn debug_read_u8(&self, address: u16) -> u8 { + match address { + 0..=0xFF if !self.memory.bootrom_disabled => self.memory.bootrom[address as usize], + 0..=0x7FFF => match self.cartridge.as_ref() { + Some(mapper) => mapper.read_rom_u8(address), + None => 0, + }, + 0x8000..=0x9FFF => self.ppu.cpu_read_vram(address), + 0xA000..=0xBFFF => match self.cartridge.as_ref() { + Some(mapper) => mapper.read_eram_u8(address), + None => 0, + }, + 0xC000..=0xDFFF => self.memory.wram[address as usize - 0xC000], + 0xE000..=0xFDFF => self.memory.wram[address as usize - 0xE000], + 0xFE00..=0xFE9F => self.ppu.cpu_read_oam(address), + 0xFEA0..=0xFEFF => 0, + 0xFF00..=0xFF7F => self.cpu_read_io(address), + 0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80], + 0xFFFF => self.interrupts.interrupt_enable, + } + } + + /// Warning: This bypasses the memory bus and only exists for + /// debugging/testing purposes + #[allow(unused)] + pub fn debug_write_u8(&mut self, address: u16, value: u8) { + match address { + 0..=0xFF if !self.memory.bootrom_disabled => {} + 0..=0x7FFF => { + if let Some(mapper) = self.cartridge.as_mut() { + mapper.write_rom_u8(address, value) + } + } + 0x8000..=0x9FFF => self.ppu.cpu_write_vram(address, value), + 0xA000..=0xBFFF => { + if let Some(mapper) = self.cartridge.as_mut() { + mapper.write_eram_u8(address, value) + } + } + 0xC000..=0xDFFF => self.memory.wram[address as usize - 0xC000] = value, + 0xE000..=0xFDFF => self.memory.wram[address as usize - 0xE000] = value, + 0xFE00..=0xFE9F => self.ppu.cpu_write_oam(address, value), + 0xFEA0..=0xFEFF => {} + 0xFF00..=0xFF7F => self.cpu_write_io(address, value), + 0xFF80..=0xFFFE => self.memory.hram[address as usize - 0xFF80] = value, + 0xFFFF => self.interrupts.interrupt_enable = value & 0b1_1111, + } + } + fn internal_cpu_read_u8(&self, address: u16) -> u8 { if self.dma.remaining_cycles == 0 { match address { diff --git a/deemgee/src/gameboy/cpu.rs b/deemgee/src/gameboy/cpu.rs index 88359ee..82e7072 100644 --- a/deemgee/src/gameboy/cpu.rs +++ b/deemgee/src/gameboy/cpu.rs @@ -43,7 +43,7 @@ pub enum CycleResult { FinishedKeepPc, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Registers { pub a: u8, pub f: u8, @@ -67,12 +67,50 @@ pub struct Registers { pub in_interrupt_vector: Option, } +impl PartialEq for Registers { + fn eq(&self, other: &Self) -> bool { + self.a == other.a + && self.f == other.f + && self.b == other.b + && self.c == other.c + && self.d == other.d + && self.e == other.e + && self.h == other.h + && self.l == other.l + && self.sp == other.sp + && self.pc == other.pc + } +} + impl Registers { - define_register!(a, f); + pub fn get_af(&self) -> u16 { + (self.a as u16) << 8 | self.f as u16 + } + + pub fn set_af(&mut self, value: u16) { + self.a = (value >> 8) as u8; + self.f = value as u8 & 0b1111_0000; + } define_register!(b, c); define_register!(d, e); define_register!(h, l); + pub fn post_rom() -> Self { + Self { + a: 0x01, + f: 0xB0, + b: 0x00, + c: 0x13, + d: 0x00, + e: 0xD8, + h: 0x01, + l: 0x4D, + sp: 0xFFFE, + pc: 0x0100, + ..Default::default() + } + } + /// This is just a helper function for macros utilizing ident pasting pub fn get_sp(&self) -> u16 { self.sp @@ -110,7 +148,7 @@ pub fn tick_cpu(state: &mut Gameboy) { state.interrupts.write_if_joypad(true); } - if state.registers.cycle == 0 && state.halt { + if state.registers.cycle == 0 && (state.halt || state.stop) { if (state.interrupts.read_ie_vblank() && state.interrupts.read_if_vblank()) || (state.interrupts.read_ie_lcd_stat() && state.interrupts.read_if_lcd_stat()) || (state.interrupts.read_ie_timer() && state.interrupts.read_if_timer()) @@ -118,6 +156,7 @@ pub fn tick_cpu(state: &mut Gameboy) { || (state.interrupts.read_ie_joypad() && state.interrupts.read_if_joypad()) { state.halt = false; + state.stop = false; } else { return; } @@ -149,8 +188,7 @@ pub fn tick_cpu(state: &mut Gameboy) { } if state.registers.cycle == 0 && state.interrupts.ei_queued { - state.interrupts.ime = state.interrupts.ei_queued; - state.interrupts.ei_queued = false; + state.interrupts.cycle_passed = true; } if state.registers.cycle == 0 && state.halt_bug { @@ -176,6 +214,7 @@ pub fn tick_cpu(state: &mut Gameboy) { CycleResult::NeedsMore } 4 => { + let original_pc = state.registers.pc; state.registers.pc = match idx { 0 => 0x40, 1 => 0x48, @@ -186,7 +225,11 @@ pub fn tick_cpu(state: &mut Gameboy) { }; state.registers.in_interrupt_vector = None; state.registers.opcode_bytecount = Some(0); - log::info!("Triggering interrupt to {:#X}", state.registers.pc); + log::debug!( + "Triggering interrupt to {:#X} from {:#X}", + state.registers.pc, + original_pc + ); CycleResult::Finished } _ => unreachable!(), @@ -194,16 +237,19 @@ pub fn tick_cpu(state: &mut Gameboy) { } else { let opcode = match state.registers.current_opcode { Some(opcode) => opcode, - None => match state.registers.mem_read_hold.take() { - Some(opcode) => { - state.registers.current_opcode = Some(opcode); - opcode + None => { + state.pc_history.push(state.registers.pc); + match state.registers.mem_read_hold.take() { + Some(opcode) => { + state.registers.current_opcode = Some(opcode); + opcode + } + None => { + state.cpu_read_u8(state.registers.pc); + return; + } } - None => { - state.cpu_read_u8(state.registers.pc); - return; - } - }, + } }; let result: CycleResult = match opcode { @@ -223,6 +269,7 @@ pub fn tick_cpu(state: &mut Gameboy) { 0x0d => alu::dec_c, 0x0e => load_store_move::ld_c_imm_u8, 0x0F => alu::rrca, + 0x10 => misc::halt, 0x11 => load_store_move::ld_de_imm_u16, 0x12 => load_store_move::ld_deref_de_a, 0x13 => alu::inc_de, @@ -439,6 +486,7 @@ pub fn tick_cpu(state: &mut Gameboy) { 0xE5 => load_store_move::push_hl, 0xE6 => alu::and_a_imm_u8, 0xE7 => flow::rst_0x20, + 0xE8 => alu::add_sp_imm_i8, 0xE9 => flow::jp_hl, 0xEA => load_store_move::ld_deref_imm_u16_a, 0xEB | 0xEC | 0xED => { @@ -479,6 +527,12 @@ pub fn tick_cpu(state: &mut Gameboy) { state.registers.pc = state.registers.pc.overflowing_add(1).0; } + if state.interrupts.cycle_passed && state.interrupts.ei_queued { + state.interrupts.cycle_passed = false; + state.interrupts.ei_queued = false; + state.interrupts.ime = true; + } + if result == CycleResult::Finished { match state.registers.opcode_bytecount { Some(len) => state.registers.pc = state.registers.pc.overflowing_add(len as u16).0, diff --git a/deemgee/src/gameboy/cpu/alu.rs b/deemgee/src/gameboy/cpu/alu.rs index b2e50b0..05a8e7d 100644 --- a/deemgee/src/gameboy/cpu/alu.rs +++ b/deemgee/src/gameboy/cpu/alu.rs @@ -17,7 +17,7 @@ pub fn sub_with_carry(lhs: u8, rhs: u8, carry: bool) -> CarryResult { let (result, second_carry) = first_res.overflowing_sub(carry_u8); let carry = first_carry || second_carry; - let half_carry = (lhs & 0xF) < (rhs & 0xF) + carry_u8; + let half_carry = (lhs & 0xF).wrapping_sub(rhs & 0xF).wrapping_sub(carry_u8) & 0x10 != 0; CarryResult { result, carry, half_carry } } @@ -29,21 +29,21 @@ pub fn add_with_carry(lhs: u8, rhs: u8, carry: bool) -> CarryResult { let (result, second_carry) = first_res.overflowing_add(carry_u8); let carry = first_carry || second_carry; - let half_carry = (lhs & 0xF) + (rhs & 0xF) > 0xF; + let half_carry = (lhs & 0xF) + (rhs & 0xF) + carry_u8 > 0xF; CarryResult { result, carry, half_carry } } pub fn add(lhs: u8, rhs: u8) -> CarryResult { let (result, carry) = lhs.overflowing_add(rhs); - let half_carry = (lhs & 0xF) + (rhs & 0xF) > 0xF; + let half_carry = (lhs & 0xF) + (rhs & 0xF) & 0x10 != 0; CarryResult { result, carry, half_carry } } pub fn sub(lhs: u8, rhs: u8) -> CarryResult { let (result, carry) = lhs.overflowing_sub(rhs); - let half_carry = (lhs & 0xF) < (rhs & 0xF); + let half_carry = (lhs & 0xF).wrapping_sub(rhs & 0xF) & 0x10 != 0; CarryResult { result, carry, half_carry } } @@ -454,7 +454,7 @@ opcode!(dec_deref_hl, 0x35, "DEC (HL)", false, 1, { state.cpu_write_u8(state.registers.get_hl(), result); state.registers.set_zero(result == 0); - state.registers.set_subtract(false); + state.registers.set_subtract(true); state.registers.set_half_carry(half_carry); CycleResult::NeedsMore }, @@ -469,7 +469,7 @@ opcode!(rla, 0x17, "RLA", false, 1, { state.registers.a <<= 1; if state.registers.get_carry() { - state.registers.a = state.registers.a.wrapping_add(1); + state.registers.a |= 1; } state.registers.set_zero(false); @@ -487,7 +487,7 @@ opcode!(rra, 0x1f, "RRA", false, 1, { state.registers.a >>= 1; if state.registers.get_carry() { - state.registers.a = state.registers.a.wrapping_add(1 << 7); + state.registers.a |= 1 << 7; } state.registers.set_zero(false); @@ -845,7 +845,7 @@ opcode!(rlca, 0x7, "RLCA", false, 1, { state.registers.a <<= 1; state.registers.a |= carry as u8; - state.registers.set_zero(state.registers.a == 0); + state.registers.set_zero(false); state.registers.set_subtract(false); state.registers.set_half_carry(false); state.registers.set_carry(carry); @@ -859,7 +859,7 @@ opcode!(rrca, 0xF, "RRCA", false, 1, { state.registers.a >>= 1; state.registers.a |= (carry as u8) << 7; - state.registers.set_zero(state.registers.a == 0); + state.registers.set_zero(false); state.registers.set_subtract(false); state.registers.set_half_carry(false); state.registers.set_carry(carry); @@ -872,19 +872,32 @@ opcode!(daa, 0x27, "DAA", false, 1, { let mut value = 0; let mut carry = false; - if state.registers.get_half_carry() || (!state.registers.get_subtract() && (state.registers.a & 0xF) > 9) { - value |= 0x06; - } + match state.registers.get_subtract() { + false => { + if state.registers.get_carry() || state.registers.a > 0x99 { + value |= 0x60; + carry = true; + } - if state.registers.get_carry() || (!state.registers.get_subtract() && state.registers.a > 0x99) { - value |= 0x60; - carry = true; - } + if state.registers.get_half_carry() || state.registers.a & 0xf > 0x9 { + value |= 0x06; + } - state.registers.a = match state.registers.get_subtract() { - true => state.registers.a.wrapping_sub(value), - false => state.registers.a.wrapping_add(value) - }; + state.registers.a = state.registers.a.wrapping_add(value); + }, + true => { + if state.registers.get_carry() { + value |= 0x60; + carry = true; + } + + if state.registers.get_half_carry() { + value |= 0x06; + } + + state.registers.a = state.registers.a.wrapping_sub(value); + } + } state.registers.set_half_carry(false); state.registers.set_carry(carry); @@ -892,3 +905,57 @@ opcode!(daa, 0x27, "DAA", false, 1, { CycleResult::Finished } }); + +opcode!(add_sp_imm_i8, 0xE8, "ADD sp,i8", false, 2, { + 0 => { + state.cpu_read_u8(state.registers.pc + 1); + CycleResult::NeedsMore + }, + 1 => { + let value = state.registers.take_mem() as i8; + + let CarryResult { carry, half_carry, .. } = add((state.registers.sp & 0xff) as u8, value as u8); + + state.registers.set_half_carry(half_carry); + state.registers.set_carry(carry); + + let CarryResult { result, carry, .. } = match value < 0 { + true => sub((state.registers.sp & 0xff) as u8, value.abs() as u8), + false => add((state.registers.sp & 0xff) as u8, value as u8), + }; + + state.registers.sp &= 0xFF00; + state.registers.sp |= result as u16; + + state.registers.set_hold(match carry { + true => match value < 0 { + true => -1i8 as u16, + false => 1, + }, + false => 0 + }); + + CycleResult::NeedsMore + + }, + 2 => { + CycleResult::NeedsMore + }, + 3 => { + let carry = state.registers.take_hold() as i8; + + match carry { + 1 => { + state.registers.sp = state.registers.sp.wrapping_add(1 << 8); + }, + -1 => { + state.registers.sp = state.registers.sp.wrapping_sub(1 << 8); + }, + _ => {} + } + + state.registers.set_zero(false); + state.registers.set_subtract(false); + CycleResult::Finished + } +}); diff --git a/deemgee/src/gameboy/cpu/load_store_move.rs b/deemgee/src/gameboy/cpu/load_store_move.rs index 8a39319..97d0a46 100644 --- a/deemgee/src/gameboy/cpu/load_store_move.rs +++ b/deemgee/src/gameboy/cpu/load_store_move.rs @@ -242,7 +242,7 @@ opcode!(ld_hl_sp_i8, 0xF8, "LD HL,SP+i8", false, 2, { let val = state.registers.take_mem() as i8; let rhs = if val < 0 { - state.registers.sp.overflowing_sub((val as u8 & !(1u8 << 7)) as u16).0 + state.registers.sp.overflowing_sub(val.abs() as u16).0 } else { state.registers.sp.overflowing_add(val as u16).0 }; diff --git a/deemgee/src/gameboy/cpu/misc.rs b/deemgee/src/gameboy/cpu/misc.rs index 61c2be6..a6a2c29 100644 --- a/deemgee/src/gameboy/cpu/misc.rs +++ b/deemgee/src/gameboy/cpu/misc.rs @@ -33,3 +33,39 @@ opcode!(halt, 0x76, "HALT", false, 1, { CycleResult::Finished } }); + +opcode!(stop, 0x10, "STOP", false, 1, { + 0 => { + CycleResult::NeedsMore + }, + 1 => { + let button_held = state.joypad.cpu_read() & 0b1111 != 0; + let interrupt_pending = state.interrupts.interrupt_enable & state.interrupts.interrupt_flag != 0; + + match button_held { + true => match interrupt_pending { + true => { + state.registers.pc = state.registers.pc.wrapping_add(1); + }, + false => { + state.registers.pc = state.registers.pc.wrapping_add(2); + state.halt = true; + } + }, + false => match interrupt_pending { + true => { + state.registers.pc = state.registers.pc.wrapping_add(1); + state.stop = true; + state.timer.div = 0; + }, + false => { + state.registers.pc = state.registers.pc.wrapping_add(2); + state.stop = true; + state.timer.div = 0; + } + }, + } + + CycleResult::FinishedKeepPc + } +}); diff --git a/deemgee/src/gameboy/cpu/prefixed.rs b/deemgee/src/gameboy/cpu/prefixed.rs index b11c77c..d29235c 100644 --- a/deemgee/src/gameboy/cpu/prefixed.rs +++ b/deemgee/src/gameboy/cpu/prefixed.rs @@ -25,6 +25,7 @@ pub fn prefixed_handler(state: &mut Gameboy) -> CycleResult { 0x03 => rlc_e, 0x04 => rlc_h, 0x05 => rlc_l, + 0x06 => rlc_deref_hl, 0x07 => rlc_a, 0x08 => rrc_b, 0x09 => rrc_c, @@ -32,6 +33,7 @@ pub fn prefixed_handler(state: &mut Gameboy) -> CycleResult { 0x0b => rrc_e, 0x0c => rrc_h, 0x0d => rrc_l, + 0x0e => rrc_deref_hl, 0x0f => rrc_a, 0x10 => rl_b, 0x11 => rl_c, @@ -39,6 +41,7 @@ pub fn prefixed_handler(state: &mut Gameboy) -> CycleResult { 0x13 => rl_e, 0x14 => rl_h, 0x15 => rl_l, + 0x16 => rl_deref_hl, 0x17 => rl_a, 0x18 => rr_b, 0x19 => rr_c, @@ -46,27 +49,31 @@ pub fn prefixed_handler(state: &mut Gameboy) -> CycleResult { 0x1b => rr_e, 0x1c => rr_h, 0x1d => rr_l, + 0x1e => rr_deref_hl, 0x1f => rr_a, - 0x20 => sra_b, - 0x21 => sra_c, - 0x22 => sra_d, - 0x23 => sra_e, - 0x24 => sra_h, - 0x25 => sra_l, - 0x27 => sra_a, - 0x28 => sla_b, - 0x29 => sla_c, - 0x2a => sla_d, - 0x2b => sla_e, - 0x2c => sla_h, - 0x2d => sla_l, - 0x2f => sla_a, + 0x20 => sla_b, + 0x21 => sla_c, + 0x22 => sla_d, + 0x23 => sla_e, + 0x24 => sla_h, + 0x25 => sla_l, + 0x26 => sla_deref_hl, + 0x27 => sla_a, + 0x28 => sra_b, + 0x29 => sra_c, + 0x2a => sra_d, + 0x2b => sra_e, + 0x2c => sra_h, + 0x2d => sra_l, + 0x2e => sra_deref_hl, + 0x2f => sra_a, 0x30 => swap_b, 0x31 => swap_c, 0x32 => swap_d, 0x33 => swap_e, 0x34 => swap_h, 0x35 => swap_l, + 0x36 => swap_deref_hl, 0x37 => swap_a, 0x38 => srl_b, 0x39 => srl_c, @@ -74,6 +81,7 @@ pub fn prefixed_handler(state: &mut Gameboy) -> CycleResult { 0x3b => srl_e, 0x3c => srl_h, 0x3d => srl_l, + 0x3e => srl_deref_hl, 0x3f => srl_a, 0x40 => bit_0_b, 0x41 => bit_0_c, @@ -267,10 +275,6 @@ pub fn prefixed_handler(state: &mut Gameboy) -> CycleResult { 0xFD => set_7_l, 0xFE => set_7_deref_hl, 0xFF => set_7_a, - unknown => panic!( - "Unrecognized prefixed opcode: {:#X}\nRegisters: {:#X?}", - unknown, state.registers - ), }(state); res @@ -405,6 +409,29 @@ define_rlc_reg!(0x04, h); define_rlc_reg!(0x05, l); define_rlc_reg!(0x07, a); +opcode!(rlc_deref_hl, 0x06, "RLC [HL]", true, 2, { + 1 => { + state.cpu_read_u8(state.registers.get_hl()); + CycleResult::NeedsMore + }, + 2 => { + let mut value = state.registers.take_mem(); + let carry = value >> 7 == 1; + value <<= 1; + value |= carry as u8; + + state.registers.set_zero(value == 0); + state.registers.set_subtract(false); + state.registers.set_half_carry(false); + state.registers.set_carry(carry); + state.cpu_write_u8(state.registers.get_hl(), value); + CycleResult::NeedsMore + }, + 3 => { + CycleResult::Finished + } +}); + macro_rules! define_rrc_reg { ($op:literal, $reg:ident) => { paste::paste! { @@ -433,6 +460,30 @@ define_rrc_reg!(0x0C, h); define_rrc_reg!(0x0D, l); define_rrc_reg!(0x0F, a); +opcode!(rrc_deref_hl, 0x0E, "RRC [HL]", true, 2, { + 1 => { + state.cpu_read_u8(state.registers.get_hl()); + CycleResult::NeedsMore + }, + 2 => { + let mut value = state.registers.take_mem(); + let carry = value & 0b1 == 1; + value >>= 1; + value |= (carry as u8) << 7; + + state.registers.set_zero(value == 0); + state.registers.set_subtract(false); + state.registers.set_half_carry(false); + state.registers.set_carry(carry); + + state.cpu_write_u8(state.registers.get_hl(), value); + CycleResult::NeedsMore + }, + 3 => { + CycleResult::Finished + } +}); + macro_rules! define_rl_reg { ($op:literal, $reg:ident) => { paste::paste! { @@ -464,13 +515,63 @@ define_rl_reg!(0x14, h); define_rl_reg!(0x15, l); define_rl_reg!(0x17, a); +opcode!(rl_deref_hl, 0x16, "RL [HL]", true, 2, { + 1 => { + state.cpu_read_u8(state.registers.get_hl()); + CycleResult::NeedsMore + }, + 2 => { + let mut value = state.registers.take_mem(); + let carry = value >> 7 == 1; + value <<= 1; + + if state.registers.get_carry() { + value |= 1; + } + + state.registers.set_zero(value == 0); + state.registers.set_subtract(false); + state.registers.set_half_carry(false); + state.registers.set_carry(carry); + + state.cpu_write_u8(state.registers.get_hl(), value); + CycleResult::NeedsMore + }, + 3 => { + CycleResult::Finished + } +}); + +opcode!(sla_deref_hl, 0x26, "SLA [HL]", true, 2, { + 1 => { + state.cpu_read_u8(state.registers.get_hl()); + CycleResult::NeedsMore + }, + 2 => { + let mut value = state.registers.take_mem(); + let carry = value >> 7 == 1; + value <<= 1; + + state.registers.set_zero(value == 0); + state.registers.set_subtract(false); + state.registers.set_half_carry(false); + state.registers.set_carry(carry); + + state.cpu_write_u8(state.registers.get_hl(), value); + CycleResult::NeedsMore + }, + 3 => { + CycleResult::Finished + } +}); + macro_rules! define_sla_reg { ($op:literal, $reg:ident) => { paste::paste! { opcode!([], $op, std::concat!("SLA ", std::stringify!($reg)), true, 2, { 1 => { - let carry = state.registers.$reg & (0b1 << 7) == 1; - state.registers.$reg = ((state.registers.$reg as i8) << 1) as u8; + let carry = state.registers.$reg >> 7 == 1; + state.registers.$reg <<= 1; state.registers.set_zero(state.registers.$reg == 0); state.registers.set_subtract(false); @@ -491,13 +592,40 @@ define_sla_reg!(0x24, h); define_sla_reg!(0x25, l); define_sla_reg!(0x27, a); +opcode!(sra_deref_hl, 0x2E, "SRA [HL]", true, 2, { + 1 => { + state.cpu_read_u8(state.registers.get_hl()); + CycleResult::NeedsMore + }, + 2 => { + let mut value = state.registers.take_mem(); + let carry = value & 0b1 == 1; + let msb = value & (1 << 7); + value >>= 1; + value |= msb; + + state.registers.set_zero(value == 0); + state.registers.set_subtract(false); + state.registers.set_half_carry(false); + state.registers.set_carry(carry); + + state.cpu_write_u8(state.registers.get_hl(), value); + CycleResult::NeedsMore + }, + 3 => { + CycleResult::Finished + } +}); + macro_rules! define_sra_reg { ($op:literal, $reg:ident) => { paste::paste! { opcode!([], $op, std::concat!("SRA ", std::stringify!($reg)), true, 2, { 1 => { let carry = state.registers.$reg & 0b1 == 1; - state.registers.$reg = ((state.registers.$reg as i8) >> 1) as u8; + let msb = state.registers.$reg & (1 << 7); + state.registers.$reg >>= 1; + state.registers.$reg |= msb; state.registers.set_zero(state.registers.$reg == 0); state.registers.set_subtract(false); @@ -544,6 +672,51 @@ define_swap_reg!(0x34, h); define_swap_reg!(0x35, l); define_swap_reg!(0x37, a); +opcode!(swap_deref_hl, 0x36, "SWAP [HL]", true, 2, { + 1 => { + state.cpu_read_u8(state.registers.get_hl()); + CycleResult::NeedsMore + }, + 2 => { + let mut value = state.registers.take_mem(); + value = (value >> 4) | (value << 4); + + state.registers.set_zero(value == 0); + state.registers.set_subtract(false); + state.registers.set_half_carry(false); + state.registers.set_carry(false); + + state.cpu_write_u8(state.registers.get_hl(), value); + CycleResult::NeedsMore + }, + 3 => { + CycleResult::Finished + } +}); + +opcode!(srl_deref_hl, 0x3E, "SRL [HL]", true, 2, { + 1 => { + state.cpu_read_u8(state.registers.get_hl()); + CycleResult::NeedsMore + }, + 2 => { + let mut value = state.registers.take_mem(); + let carry = value & 0b1 == 1; + value >>= 1; + + state.registers.set_zero(value == 0); + state.registers.set_subtract(false); + state.registers.set_half_carry(false); + state.registers.set_carry(carry); + + state.cpu_write_u8(state.registers.get_hl(), value); + CycleResult::NeedsMore + }, + 3 => { + CycleResult::Finished + } +}); + macro_rules! define_srl_reg { ($op:literal, $reg:ident) => { paste::paste! { @@ -579,6 +752,10 @@ macro_rules! define_rr_reg { let carry = state.registers.$reg & 0b1 == 1; state.registers.$reg >>= 1; + if state.registers.get_carry() { + state.registers.$reg |= 1 << 7; + } + state.registers.set_zero(state.registers.$reg == 0); state.registers.set_subtract(false); state.registers.set_half_carry(false); @@ -598,6 +775,33 @@ define_rr_reg!(0x1c, h); define_rr_reg!(0x1d, l); define_rr_reg!(0x1f, a); +opcode!(rr_deref_hl, 0x1E, "RR [HL]", true, 2, { + 1 => { + state.cpu_read_u8(state.registers.get_hl()); + CycleResult::NeedsMore + }, + 2 => { + let mut value = state.registers.take_mem(); + let carry = value & 0b1 == 1; + value >>= 1; + + if state.registers.get_carry() { + value |= 1 << 7; + } + + state.registers.set_zero(value == 0); + state.registers.set_subtract(false); + state.registers.set_half_carry(false); + state.registers.set_carry(carry); + + state.cpu_write_u8(state.registers.get_hl(), value); + CycleResult::NeedsMore + }, + 3 => { + CycleResult::Finished + } +}); + macro_rules! define_res_idx_reg { ($op:literal, $idx:literal, $reg:ident) => { paste::paste! { diff --git a/deemgee/src/gameboy/interrupts.rs b/deemgee/src/gameboy/interrupts.rs index ae1801c..7e934c0 100644 --- a/deemgee/src/gameboy/interrupts.rs +++ b/deemgee/src/gameboy/interrupts.rs @@ -18,13 +18,20 @@ macro_rules! define_bitfield_u8_gs { pub struct Interrupts { pub ime: bool, pub ei_queued: bool, + pub cycle_passed: bool, pub interrupt_enable: u8, pub interrupt_flag: u8, } impl Interrupts { pub fn new() -> Self { - Self { ime: false, interrupt_enable: 0, interrupt_flag: 0, ei_queued: false } + Self { + ime: false, + interrupt_enable: 0, + interrupt_flag: 0b11100000, + ei_queued: false, + cycle_passed: false, + } } define_bitfield_u8_gs!(ie_vblank, 0, interrupt_enable); diff --git a/deemgee/src/gameboy/mapper.rs b/deemgee/src/gameboy/mapper.rs index 8c09e6c..657b29c 100644 --- a/deemgee/src/gameboy/mapper.rs +++ b/deemgee/src/gameboy/mapper.rs @@ -9,8 +9,8 @@ pub trait Mapper { } pub struct NoMBC { - rom: [u8; 0x8000], - ram: Option<[u8; 0x2000]>, + pub rom: [u8; 0x8000], + pub ram: Option<[u8; 0x2000]>, } impl NoMBC { @@ -36,9 +36,7 @@ impl Mapper for NoMBC { self.rom[address as usize] } - fn write_rom_u8(&mut self, address: u16, value: u8) { - self.rom[address as usize] = value - } + fn write_rom_u8(&mut self, _address: u16, _value: u8) {} fn read_eram_u8(&self, address: u16) -> u8 { let decoded_address = address - 0xA000; diff --git a/deemgee/src/gameboy/mapper/mbc1.rs b/deemgee/src/gameboy/mapper/mbc1.rs index 62d94ba..cfdd3fd 100644 --- a/deemgee/src/gameboy/mapper/mbc1.rs +++ b/deemgee/src/gameboy/mapper/mbc1.rs @@ -85,7 +85,7 @@ impl Mapper for MBC1 { (self.rom_bank_number | (self.extra_2_bit_reg << 5)) as usize * 0x4000 } else { self.rom_bank_number as usize * 0x4000 - }] + } + (address as usize - 0x4000)] } } @@ -101,14 +101,14 @@ impl Mapper for MBC1 { fn read_eram_u8(&self, _address: u16) -> u8 { match self.ram.as_ref() { - Some(_ram) => unimplemented!(), + Some(_ram) => 0, None => 0, } } fn write_eram_u8(&mut self, _address: u16, _value: u8) { match self.ram.as_ref() { - Some(_ram) => unimplemented!(), + Some(_ram) => {} None => {} } } diff --git a/deemgee/src/gameboy/memory.rs b/deemgee/src/gameboy/memory.rs index 4a4b772..b0901ee 100644 --- a/deemgee/src/gameboy/memory.rs +++ b/deemgee/src/gameboy/memory.rs @@ -7,7 +7,12 @@ pub struct Memory { } impl Memory { - pub fn new(bootrom: [u8; 0x100]) -> Self { - Self { wram: [0; 0x2000], hram: [0; 0xAF], bootrom, bootrom_disabled: false } + pub fn new(bootrom: Option<[u8; 0x100]>) -> Self { + Self { + wram: [0; 0x2000], + hram: [0; 0xAF], + bootrom: bootrom.unwrap_or_else(|| [0u8; 0x100]), + bootrom_disabled: bootrom.is_none(), + } } } diff --git a/deemgee/src/gameboy/timer.rs b/deemgee/src/gameboy/timer.rs index 4f299f5..99c39b6 100644 --- a/deemgee/src/gameboy/timer.rs +++ b/deemgee/src/gameboy/timer.rs @@ -1,3 +1,4 @@ +#[derive(Debug)] pub struct Timer { pub enable: bool, pub clock: TimerClock, @@ -6,6 +7,7 @@ pub struct Timer { pub tima: u8, pub tima_counter: u16, pub tma: u8, + overflow: bool, } impl Timer { @@ -18,23 +20,32 @@ impl Timer { div: 0, div_counter: 0, tima_counter: 0, + overflow: false, } } pub fn tick(&mut self) -> bool { - self.div_counter = self.div_counter.overflowing_add(1).0; + self.div_counter = self.div_counter.wrapping_add(1); if self.div_counter == 0 { - self.div = self.div.overflowing_add(1).0; + self.div = self.div.wrapping_add(1); } if self.enable { - self.tima_counter = self.tima_counter.overflowing_add(1).0; + self.tima_counter = self.tima_counter.wrapping_add(4); if self.tima_counter >= self.clock.cycles() { - self.tima = self.tima.overflowing_add(1).0; + self.tima_counter = 0; + self.tima = self.tima.wrapping_add(1); - return self.tima == 0; + self.overflow = self.tima == 0; + return false; } } + + if self.overflow { + self.tima = self.tma; + self.overflow = false; + return true; + } false } @@ -44,7 +55,14 @@ impl Timer { pub fn write_tac(&mut self, value: u8) { self.enable = (value >> 2) & 0b1 == 1; - self.clock = TimerClock::from_tac_clock(value); + self.tima_counter = 0; + let new_clock = TimerClock::from_tac_clock(value); + if self.clock == TimerClock::C16 && new_clock == TimerClock::C1024 && self.enable { + self.tima = self.tima.wrapping_add(1); + + self.overflow = self.tima == 0; + } + self.clock = new_clock; } } diff --git a/deemgee/src/lib.rs b/deemgee/src/lib.rs new file mode 100644 index 0000000..b12534b --- /dev/null +++ b/deemgee/src/lib.rs @@ -0,0 +1,24 @@ +pub mod gameboy; +#[allow(unused)] +mod settings; +#[allow(unused)] +mod window; + +pub fn setup_test_emulator( + test_opcodes: [u8; ROM_LENGTH], +) -> gameboy::Gameboy { + let mut gameboy = gameboy::Gameboy::new(None); + + let mut cartridge = gameboy::mapper::NoMBC { rom: [0u8; 0x8000], ram: None }; + + (&mut cartridge.rom[0x100..ROM_LENGTH + 0x100]).copy_from_slice(&test_opcodes); + + gameboy.cartridge = Some(Box::new(cartridge)); + + gameboy.tick(); // Prefetch instruction + assert!(gameboy.registers.mem_read_hold.is_some()); // Assert prefetch happened and opcode is now sitting in the memory bus + assert_eq!(gameboy.registers.cycle, 0); // Assert tick really did just prefetch instruction and not run the opcode at + // all + + gameboy +} diff --git a/deemgee/src/main.rs b/deemgee/src/main.rs index 35bd513..93abe91 100644 --- a/deemgee/src/main.rs +++ b/deemgee/src/main.rs @@ -3,12 +3,13 @@ mod settings; mod window; use std::{ + borrow::Cow, path::PathBuf, sync::mpsc::{channel, Receiver, Sender}, }; -use argh::FromArgs; use chrono::{Duration, Utc}; +use clap::Parser; use gameboy::Gameboy; use settings::DeemgeeConfig; use sha1::{Digest, Sha1}; @@ -16,15 +17,18 @@ use window::EmulatorWindowEvent; use crate::window::GameboyEvent; -#[derive(Debug, FromArgs)] +#[derive(Debug, Parser)] /// DMG Emulator pub struct CliArgs { /// bootrom path - #[argh(positional)] - pub bootrom: PathBuf, + #[clap(long)] + pub bootrom: Option, /// game path - #[argh(positional)] + #[clap(long)] pub rom: Option, + // enter in debu g mode + #[clap(short, long)] + pub debug: bool, } #[derive(Debug, thiserror::Error)] @@ -44,18 +48,27 @@ pub enum DmgError { fn main() { env_logger::init(); - let args: CliArgs = argh::from_env(); + let args: CliArgs = CliArgs::parse(); let config = DeemgeeConfig::from_file(); let (window_side_tx, gb_side_rx) = channel::(); let (gb_side_tx, window_side_rx) = channel::(); + let rom_name = args.rom.as_ref().and_then(|path| { + path.file_name().and_then(|name| name.to_str().map(str::to_string).map(Cow::Owned)) + }); + let jh = std::thread::Builder::new() .name(String::from("mewmulator")) .spawn(move || run_gameboy(config, args, gb_side_rx, gb_side_tx).unwrap()) .unwrap(); - window::run_window(config, window_side_rx, window_side_tx); + window::run_window( + &rom_name.unwrap_or(Cow::Borrowed("NO GAME")), + config, + window_side_rx, + window_side_tx, + ); jh.join().unwrap(); } @@ -66,34 +79,48 @@ pub fn run_gameboy( rx: Receiver, tx: Sender, ) -> Result<(), DmgError> { - if !args.bootrom.is_file() { - return Err(DmgError::BootromNotFound); + let mut bootrom = None; + if let Some(bootrom_path) = args.bootrom { + if !bootrom_path.is_file() { + return Err(DmgError::BootromNotFound); + } + + let brom_md = std::fs::metadata(bootrom_path.as_path())?; + + if brom_md.len() != 256 { + return Err(DmgError::BootromInvalidSize(brom_md.len())); + } + + let mut bootrom_slice = [0u8; 0x100]; + + let bootrom_vec = std::fs::read(bootrom_path)?; + + if bootrom_vec.len() != 256 { + return Err(DmgError::BootromInvalidSize(bootrom_vec.len() as u64)); + } + + let mut hash_ctx = Sha1::new(); + hash_ctx.update(&bootrom_vec); + let digest = hash_ctx.finalize(); + + if digest.as_slice() + != b"\x4e\xd3\x1e\xc6\xb0\xb1\x75\xbb\x10\x9c\x0e\xb5\xfd\x3d\x19\x3d\xa8\x23\x33\x9f" + { + return Err(DmgError::BootromInvalidHash); + } + + bootrom_slice.copy_from_slice(&bootrom_vec); + + bootrom = Some(bootrom_slice) } - let brom_md = std::fs::metadata(args.bootrom.as_path())?; + let mut gameboy = Gameboy::new(bootrom); - if brom_md.len() != 256 { - return Err(DmgError::BootromInvalidSize(brom_md.len())); + if args.debug { + gameboy.single_step = true; + tx.send(GameboyEvent::Framebuffer(gameboy.ppu.write_fb())).unwrap(); } - let bootrom = std::fs::read(args.bootrom)?; - - if bootrom.len() != 256 { - return Err(DmgError::BootromInvalidSize(bootrom.len() as u64)); - } - - let mut hash_ctx = Sha1::new(); - hash_ctx.update(&bootrom); - let digest = hash_ctx.finalize(); - - if digest.as_slice() - != b"\x4e\xd3\x1e\xc6\xb0\xb1\x75\xbb\x10\x9c\x0e\xb5\xfd\x3d\x19\x3d\xa8\x23\x33\x9f" - { - return Err(DmgError::BootromInvalidHash); - } - - let mut gameboy = Gameboy::new(bootrom.as_slice().try_into().unwrap()); - if let Some(rom) = args.rom { if !rom.is_file() { return Err(DmgError::GameNotFound); @@ -105,7 +132,6 @@ pub fn run_gameboy( } let mut goal = chrono::Utc::now() + Duration::milliseconds(1000 / 60); - let mut paused = false; let mut frame_counter = 0; 'outer: loop { @@ -129,7 +155,9 @@ pub fn run_gameboy( window::EmulatorWindowEvent::RightToggle => { gameboy.joypad.set_right(!gameboy.joypad.right) } - window::EmulatorWindowEvent::PauseToggle => paused = !paused, + window::EmulatorWindowEvent::PauseToggle => { + gameboy.single_step = !gameboy.single_step + } window::EmulatorWindowEvent::LogToggle => { gameboy.log_instructions = !gameboy.log_instructions } @@ -143,23 +171,26 @@ pub fn run_gameboy( } } - if !paused { - let redraw_needed = gameboy.tick(); - if redraw_needed { - let now = chrono::Utc::now(); - frame_counter += 1; - tx.send(GameboyEvent::Framebuffer(gameboy.ppu.write_fb())).unwrap(); - let delta = goal - now; - let delta_ms = delta.num_milliseconds(); - if delta_ms > 0 { - std::thread::sleep(std::time::Duration::from_millis(delta_ms as u64)); - } - goal = goal + Duration::milliseconds(1000 / 60); + let (redraw_needed, time_spent_debugging) = gameboy.tick(); - if frame_counter == 60 { - log::info!("Rendered 60 frames"); - frame_counter = 0; - } + if let Some(diff) = time_spent_debugging { + goal = goal + Duration::milliseconds(diff); + } + + if redraw_needed { + let now = chrono::Utc::now(); + frame_counter += 1; + tx.send(GameboyEvent::Framebuffer(gameboy.ppu.write_fb())).unwrap(); + let delta = goal - now; + let delta_ms = delta.num_milliseconds(); + if delta_ms > 0 { + std::thread::sleep(std::time::Duration::from_millis(delta_ms as u64)); + } + goal = goal + Duration::milliseconds(1000 / 60); + + if frame_counter == 60 { + log::debug!("Rendered 60 frames"); + frame_counter = 0; } } } diff --git a/deemgee/src/window.rs b/deemgee/src/window.rs index 03abcf6..7ba6a8a 100644 --- a/deemgee/src/window.rs +++ b/deemgee/src/window.rs @@ -93,6 +93,7 @@ impl Keymap { } pub fn run_window( + rom_name: &str, config: DeemgeeConfig, rx: Receiver, tx: Sender, @@ -100,7 +101,9 @@ pub fn run_window( let event_loop = EventLoop::new().unwrap(); let mut input = WinitInputHelper::new(); - let window = { WindowBuilder::new().with_title("OwO").build(&event_loop).unwrap() }; + let window = { + WindowBuilder::new().with_title(format!("Meow - {}", rom_name)).build(&event_loop).unwrap() + }; let mut pixels = { let window_size = window.inner_size(); diff --git a/deemgee/tests/flow_opcodes.rs b/deemgee/tests/flow_opcodes.rs new file mode 100644 index 0000000..ab99de0 --- /dev/null +++ b/deemgee/tests/flow_opcodes.rs @@ -0,0 +1,480 @@ +use deemgee::setup_test_emulator; + +macro_rules! conditional_jump_relative_testgen { + ($flag:ident, $not_opcode:literal, $opcode:literal) => { + paste::paste! { + #[test] + fn []() { + let mut emulator = setup_test_emulator([$not_opcode, 0x1]); + + emulator.registers.[](false); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x103); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$not_opcode, 0x1]); + + emulator.registers.[](true); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x102); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode, 0x1]); + + emulator.registers.[](true); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x103); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode, 0x1]); + + emulator.registers.[](false); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x102); + } + } + }; +} + +conditional_jump_relative_testgen!(zero, 0x20, 0x28); +conditional_jump_relative_testgen!(carry, 0x30, 0x38); + +#[test] +fn test_jr_i8() { + let mut emulator = setup_test_emulator([0x18, 0x1]); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x103); +} + +#[test] +fn test_jp_u16() { + let mut emulator = setup_test_emulator([0xC3, 0xFE, 0xCA]); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); +} + +#[test] +fn test_jp_hl() { + let mut emulator = setup_test_emulator([0xE9]); + + emulator.registers.set_hl(0xCAFE); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); +} + +macro_rules! conditional_jump_testgen { + ($flag:ident, $not_opcode:literal, $opcode:literal) => { + paste::paste! { + #[test] + fn []() { + let mut emulator = setup_test_emulator([$not_opcode, 0xFE, 0xCA]); + + emulator.registers.[](false); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$not_opcode, 0xFE, 0xCA]); + + emulator.registers.[](true); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x103); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode, 0xFE, 0xCA]); + + emulator.registers.[](true); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode, 0xFE, 0xCA]); + + emulator.registers.[](false); + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x103); + } + } + }; +} + +conditional_jump_testgen!(zero, 0xC2, 0xCA); +conditional_jump_testgen!(carry, 0xD2, 0xDA); + +#[test] +fn test_call_u16() { + let mut emulator = setup_test_emulator([0xCD, 0xFE, 0xCA]); + + let orignal_sp = emulator.registers.sp; + + emulator.tick(); // <-- Read first u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Read second u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Push next instruction PC hi to stack + assert_eq!(emulator.registers.sp, orignal_sp - 1); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Push next instruction PC lo to stack + assert_eq!(emulator.registers.sp, orignal_sp - 2); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); + + assert_eq!(emulator.debug_read_u8(orignal_sp - 1), 0x01); + assert_eq!(emulator.debug_read_u8(orignal_sp - 2), 0x03); +} + +macro_rules! conditional_call_testgen { + ($flag:ident, $not_opcode:literal, $opcode:literal) => { + paste::paste! { + #[test] + fn []() { + let mut emulator = setup_test_emulator([$not_opcode, 0xFE, 0xCA]); + + let orignal_sp = emulator.registers.sp; + + emulator.registers.[](false); + + emulator.tick(); // <-- Read first u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Read second u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Check flag + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Push next instruction PC hi to stack + assert_eq!(emulator.registers.sp, orignal_sp - 1); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Push next instruction PC lo to stack + assert_eq!(emulator.registers.sp, orignal_sp - 2); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); + + assert_eq!(emulator.debug_read_u8(orignal_sp - 1), 0x01); + assert_eq!(emulator.debug_read_u8(orignal_sp - 2), 0x03); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$not_opcode, 0xFE, 0xCA]); + + let orignal_sp = emulator.registers.sp; + + emulator.registers.[](true); + + emulator.tick(); // <-- Read first u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Read second u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Check flag + assert_eq!(emulator.registers.pc, 0x103); + + assert_eq!(emulator.registers.sp, orignal_sp); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode, 0xFE, 0xCA]); + + let orignal_sp = emulator.registers.sp; + + emulator.registers.[](true); + + emulator.tick(); // <-- Read first u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Read second u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Check flag + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Push next instruction PC hi to stack + assert_eq!(emulator.registers.sp, orignal_sp - 1); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Push next instruction PC lo to stack + assert_eq!(emulator.registers.sp, orignal_sp - 2); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); + + assert_eq!(emulator.debug_read_u8(orignal_sp - 1), 0x01); + assert_eq!(emulator.debug_read_u8(orignal_sp - 2), 0x03); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode, 0xFE, 0xCA]); + + let orignal_sp = emulator.registers.sp; + + emulator.registers.[](false); + + emulator.tick(); // <-- Read first u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Read second u8 + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); // <-- Check flag + assert_eq!(emulator.registers.pc, 0x103); + + assert_eq!(emulator.registers.sp, orignal_sp); + } + } + }; +} + +conditional_call_testgen!(zero, 0xC4, 0xCC); +conditional_call_testgen!(carry, 0xD4, 0xDC); + +#[test] +fn test_ret() { + let mut emulator = setup_test_emulator([0xC9]); + + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xCA); + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xFE); + + let orignal_sp = emulator.registers.sp; + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + assert_eq!(emulator.registers.sp, orignal_sp + 1); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + assert_eq!(emulator.registers.sp, orignal_sp + 2); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); +} + +#[test] +fn test_reti() { + let mut emulator = setup_test_emulator([0xD9]); + + emulator.interrupts.ime = false; + + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xCA); + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xFE); + + let orignal_sp = emulator.registers.sp; + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + assert_eq!(emulator.registers.sp, orignal_sp + 1); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + assert_eq!(emulator.registers.sp, orignal_sp + 2); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); + assert_eq!(emulator.interrupts.ime, true); +} + +macro_rules! conditional_ret_testgen { + ($flag:ident, $not_opcode:literal, $opcode:literal) => { + paste::paste! { + #[test] + fn []() { + let mut emulator = setup_test_emulator([$not_opcode]); + + emulator.registers.[](false); + + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xCA); + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xFE); + + let orignal_sp = emulator.registers.sp; + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + assert_eq!(emulator.registers.sp, orignal_sp + 1); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + assert_eq!(emulator.registers.sp, orignal_sp + 2); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$not_opcode]); + + emulator.registers.[](true); + + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xCA); + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xFE); + + let orignal_sp = emulator.registers.sp; + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x101); + assert_eq!(emulator.registers.sp, orignal_sp); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode]); + + emulator.registers.[](true); + + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xCA); + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xFE); + + let orignal_sp = emulator.registers.sp; + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + assert_eq!(emulator.registers.sp, orignal_sp + 1); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + assert_eq!(emulator.registers.sp, orignal_sp + 2); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0xCAFE); + } + + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode]); + + emulator.registers.[](false); + + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xCA); + emulator.registers.sp = emulator.registers.sp.overflowing_sub(1).0; + emulator.debug_write_u8(emulator.registers.sp, 0xFE); + + let orignal_sp = emulator.registers.sp; + + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, 0x101); + assert_eq!(emulator.registers.sp, orignal_sp); + } + } + }; +} + +conditional_ret_testgen!(zero, 0xC0, 0xC8); +conditional_ret_testgen!(carry, 0xD0, 0xD8); + +macro_rules! rst_testgen { + ($opcode:literal, $addr:literal) => { + paste::paste! { + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode]); + + let original_sp = emulator.registers.sp; + + emulator.tick(); + assert_eq!(emulator.registers.sp, original_sp); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.sp, original_sp - 1); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.sp, original_sp - 2); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.pc, $addr); + + assert_eq!(emulator.debug_read_u8(original_sp - 1), 0x01); + assert_eq!(emulator.debug_read_u8(original_sp - 2), 0x01); + } + } + }; +} + +rst_testgen!(0xC7, 0x0); +rst_testgen!(0xCF, 0x08); +rst_testgen!(0xD7, 0x10); +rst_testgen!(0xDF, 0x18); +rst_testgen!(0xE7, 0x20); +rst_testgen!(0xEF, 0x28); +rst_testgen!(0xF7, 0x30); +rst_testgen!(0xFF, 0x38); diff --git a/deemgee/tests/load_store_move_opcodes.rs b/deemgee/tests/load_store_move_opcodes.rs new file mode 100644 index 0000000..03a01de --- /dev/null +++ b/deemgee/tests/load_store_move_opcodes.rs @@ -0,0 +1,64 @@ +use deemgee::setup_test_emulator; + +macro_rules! ld_reg_imm_u16_testgen { + ($hireg:ident, $loreg:ident, $opcode:literal) => { + paste::paste! { + #[test] + fn []() { + let mut emulator = setup_test_emulator([$opcode, 0xFE, 0xCA]); + + emulator.registers.$hireg = 0x00; + emulator.registers.$loreg = 0x00; + + emulator.tick(); + assert_eq!(emulator.registers.$hireg, 0x00); + assert_eq!(emulator.registers.$loreg, 0x00); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.$hireg, 0x00); + assert_eq!(emulator.registers.$loreg, 0xFE); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.$hireg, 0xCA); + assert_eq!(emulator.registers.$loreg, 0xFE); + assert_eq!(emulator.registers.pc, 0x103); + } + } + }; +} + +ld_reg_imm_u16_testgen!(b, c, 0x01); +ld_reg_imm_u16_testgen!(d, e, 0x11); +ld_reg_imm_u16_testgen!(h, l, 0x21); + +#[test] +fn test_ld_reg_sp_imm_u16() { + let mut emulator = setup_test_emulator([0x31, 0xFE, 0xCA]); + + emulator.registers.sp = 0x0000; + + emulator.tick(); + assert_eq!(emulator.registers.sp, 0x0000); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.sp, 0x00FE); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.sp, 0xCAFE); + assert_eq!(emulator.registers.pc, 0x103); +} + +#[test] +fn test_ld_sp_hl() { + let mut emulator = setup_test_emulator([0xF9]); + + emulator.registers.sp = 0x0000; + emulator.registers.set_hl(0xCAFE); + + emulator.tick(); + assert_eq!(emulator.registers.sp, 0x00FE); + assert_eq!(emulator.registers.pc, 0x100); + emulator.tick(); + assert_eq!(emulator.registers.sp, 0xCAFE); + assert_eq!(emulator.registers.pc, 0x101); +} diff --git a/deemgee/tests/misc_opcodes.rs b/deemgee/tests/misc_opcodes.rs new file mode 100644 index 0000000..23d66f6 --- /dev/null +++ b/deemgee/tests/misc_opcodes.rs @@ -0,0 +1,69 @@ +use deemgee::setup_test_emulator; + +#[test] +fn test_nop() { + let mut emulator = setup_test_emulator([0x00]); + + let expected_register_state = { + let mut state = emulator.registers.clone(); + state.pc += 1; + state + }; + + emulator.tick(); + assert_eq!(emulator.registers, expected_register_state); +} + +#[test] +fn test_di() { + let mut emulator = setup_test_emulator([0xF3]); + + let expected_register_state = { + let mut state = emulator.registers.clone(); + state.pc += 1; + state + }; + + emulator.interrupts.ime = true; + + emulator.tick(); + assert_eq!(emulator.registers, expected_register_state); + assert!(!emulator.interrupts.ime); +} + +#[test] +fn test_ei() { + let mut emulator = setup_test_emulator([0xFB]); + + let expected_register_state = { + let mut state = emulator.registers.clone(); + state.pc += 1; + state + }; + + emulator.interrupts.ime = false; + + emulator.tick(); + assert_eq!(emulator.registers, expected_register_state); + assert!(!emulator.interrupts.ime); + emulator.tick(); // <-- Execute the NOP that comes after as the `EI` instruction only takes + // effect a cycle after + assert!(emulator.interrupts.ime); +} + +#[test] +fn test_halt() { + let mut emulator = setup_test_emulator([0x76]); + + let expected_register_state = { + let mut state = emulator.registers.clone(); + state.pc += 1; + state + }; + + emulator.interrupts.ime = true; + + emulator.tick(); + assert_eq!(emulator.registers, expected_register_state); + assert!(emulator.halt); +}