From 87fe0ae414f18c220c61303fbe612b50d9beb7f4 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Sun, 8 Dec 2024 00:12:12 -0300 Subject: [PATCH 1/3] core: add a binary This will just run a rom in the interpreter for a given amount of time/cycles. Useful for generating vcd traces or testing something else quickly. It compiles much faster than the gui or the unit tstes, because it has zero dependencies. --- core/src/bin/run.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 core/src/bin/run.rs diff --git a/core/src/bin/run.rs b/core/src/bin/run.rs new file mode 100644 index 0000000..4b38b21 --- /dev/null +++ b/core/src/bin/run.rs @@ -0,0 +1,72 @@ +use gameroy::{ + consts::CLOCK_SPEED, + gameboy::{cartridge::Cartridge, GameBoy}, + interpreter::Interpreter, +}; + +fn parse_timeout(timeout: &str) -> Option { + Some(if timeout.ends_with("s") { + timeout[..timeout.len() - 1].parse::().ok()? * CLOCK_SPEED + } else if timeout.ends_with("ms") { + timeout[..timeout.len() - 2].parse::().ok()? * CLOCK_SPEED / 1000 + } else { + timeout.parse::().ok()? + }) +} + +fn main() { + let mut args = std::env::args(); + let mut boot_rom_path = None; + let mut rom_path = None; + let mut timeout = CLOCK_SPEED; // 1 second + + // Skip program name + let _ = args.next(); + + while let Some(arg) = args.next() { + if arg == "--boot" { + boot_rom_path = Some(args.next().expect("Missing arg value")); + } else if arg == "--timeout" { + timeout = parse_timeout(&args.next().expect("Missing arg value")) + .expect("Invalid timeout value"); + } else if arg.starts_with("--") { + panic!("Unknown argument: {}", arg); + } else { + rom_path = Some(arg); + } + } + + println!( + "Running ROM: {}", + rom_path.as_ref().expect("No rom path provided") + ); + println!("Boot ROM: {}", boot_rom_path.as_deref().unwrap_or("None")); + + let rom = std::fs::read(rom_path.expect("No rom path provided")).unwrap(); + let boot_rom = boot_rom_path.map(|path| { + std::fs::read(&path) + .unwrap() + .try_into() + .expect("Boot ROM must be 256 bytes") + }); + + let cartridge = Cartridge::new(rom).expect("Invalid ROM"); + + println!( + "Cartridge: {:} ({})", + cartridge.kind_name(), + cartridge.header.cartridge_type + ); + + let mut gameboy = GameBoy::new(boot_rom, cartridge); + + let mut inter = Interpreter(&mut gameboy); + + while inter.0.clock_count < timeout { + inter.interpret_op(); + if inter.0.read(inter.0.cpu.pc) == 0x40 { + println!("LD B, B detected, stopping"); + break; + } + } +} From 7195185b40fe0b7ff55a8f6158346757b3aa559b Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Sun, 8 Dec 2024 00:12:31 -0300 Subject: [PATCH 2/3] core: handle too big roms --- core/src/gameboy/cartridge.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/core/src/gameboy/cartridge.rs b/core/src/gameboy/cartridge.rs index e553d2b..699720a 100644 --- a/core/src/gameboy/cartridge.rs +++ b/core/src/gameboy/cartridge.rs @@ -255,9 +255,16 @@ impl MbcSpecification { } Ok(size) => size, Err(err) => { - let size = *ROM_SIZES.iter().find(|&&x| x >= rom.len()).unwrap(); - writeln!(error, "{}, deducing size from ROM size as {}", err, size,).unwrap(); - size + match ROM_SIZES.iter().copied().find(|&x| x >= rom.len()) { + Some(size) => { + writeln!(error, "{}, deducing size from ROM size as {}", err, size,).unwrap(); + size + } + None => { + writeln!(error, "{}", err).unwrap(); + return None; + } + } } }; From 69aca8f55390a13ac6cbace3fb63e66be7285f20 Mon Sep 17 00:00:00 2001 From: Rodrigo Batista de Moraes Date: Wed, 14 Feb 2024 18:17:12 -0300 Subject: [PATCH 3/3] core: add wave trace generation VCD (value change dump) is a format used by EDA tools (like verilog simulators) that shows the change of signals over time. This will dump the change of every register and internal state of the emulator over time. This will create a file in `wave_trace/trace.vcd`, which can be opened on GTKWave or Surfer for example I have the idea of implementing it while playing with msinger/dmg-sim simulation of DMG CPU B in verilog. This will make it easier to compare my emulator with a simulation of the original hardware. --- .gitignore | 1 + Cargo.lock | 7 + core/Cargo.toml | 2 + core/src/bin/run.rs | 14 +- core/src/gameboy.rs | 41 ++++- core/src/gameboy/cpu.rs | 4 + core/src/gameboy/ppu.rs | 27 ++- core/src/gameboy/timer.rs | 21 ++- core/src/interpreter.rs | 96 +++++----- core/src/lib.rs | 3 + core/src/wave_trace.rs | 375 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 525 insertions(+), 66 deletions(-) create mode 100644 core/src/wave_trace.rs diff --git a/.gitignore b/.gitignore index 9dc587e..cd6898d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ log.txt test_rom/test.o core/test_output core/tests/gameboy-test-roms +core/vcd_trace license/license.html android/app/release gameroy.log diff --git a/Cargo.lock b/Cargo.lock index b0d5433..34ef074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1217,6 +1217,7 @@ dependencies = [ "rand", "rayon", "text-diff", + "vcd", ] [[package]] @@ -3610,6 +3611,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "vcd" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4505774fc84de82eb04caa74d859342b0daa376f4c6df45149593245dd62c004" + [[package]] name = "vec_map" version = "0.8.2" diff --git a/core/Cargo.toml b/core/Cargo.toml index 1237e80..f0b80c0 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,8 +18,10 @@ harness = false [features] io_trace = [] +wave_trace = ["dep:vcd"] [dependencies] +vcd = { version = "0.7.0", optional = true } [dev-dependencies] image = { version = "0.25.4", default-features = false, features = ["png"] } diff --git a/core/src/bin/run.rs b/core/src/bin/run.rs index 4b38b21..b682832 100644 --- a/core/src/bin/run.rs +++ b/core/src/bin/run.rs @@ -5,10 +5,10 @@ use gameroy::{ }; fn parse_timeout(timeout: &str) -> Option { - Some(if timeout.ends_with("s") { - timeout[..timeout.len() - 1].parse::().ok()? * CLOCK_SPEED - } else if timeout.ends_with("ms") { - timeout[..timeout.len() - 2].parse::().ok()? * CLOCK_SPEED / 1000 + Some(if let Some(value) = timeout.strip_suffix("s") { + value.parse::().ok()? * CLOCK_SPEED + } else if let Some(value) = timeout.strip_suffix("ms") { + value.parse::().ok()? * CLOCK_SPEED / 1000 } else { timeout.parse::().ok()? }) @@ -69,4 +69,10 @@ fn main() { break; } } + #[cfg(feature = "wave_trace")] + { + inter.0.update_all(); + println!("VCD committed on end: {}", inter.0.clock_count); + inter.0.vcd_writer.commit().unwrap(); + } } diff --git a/core/src/gameboy.rs b/core/src/gameboy.rs index 2eb04d9..f2e236a 100644 --- a/core/src/gameboy.rs +++ b/core/src/gameboy.rs @@ -71,6 +71,10 @@ pub struct GameBoy { /// trace of reads and writes. (kind | ((clock_count & !3) >> 1), address, value), kind: 0=GameBoy::IO_READ,1=GameBoy::IO_WRITE #[cfg(feature = "io_trace")] pub io_trace: RefCell>, + + /// VCD writer for the waveform tracing. + #[cfg(feature = "wave_trace")] + pub vcd_writer: crate::wave_trace::WaveTrace, } impl std::fmt::Debug for GameBoy { @@ -181,13 +185,12 @@ impl GameBoy { #[cfg(feature = "io_trace")] io_trace: Vec::new().into(), + + #[cfg(feature = "wave_trace")] + vcd_writer: crate::wave_trace::WaveTrace::new().unwrap(), }; - if this.boot_rom.is_none() { - this.reset_after_boot(); - } else { - this.reset(); - } + this.reset(); this } @@ -226,6 +229,14 @@ impl GameBoy { self.reset_after_boot(); return; } + self.reset_at_power_on(); + } + + /// Reset the gameboy to its state after powering on, before the boot rom is executed, even if + /// there is no boot rom present. + /// + /// Only used internally for setting a trace point in clock_count = 0. + pub(crate) fn reset_at_power_on(&mut self) { // TODO: Maybe I should reset the cartridge self.cpu = Cpu::default(); self.wram = [0xFF; 0x2000]; @@ -260,6 +271,9 @@ impl GameBoy { ime: cpu::ImeState::Disabled, halt_bug: false, state: cpu::CpuState::Running, + + #[cfg(feature = "wave_trace")] + op: 0, }; self.wram = [0xFF; 0x2000]; @@ -349,6 +363,12 @@ impl GameBoy { /// Advance the clock by 'count' cycles pub fn tick(&mut self, count: u64) { + #[cfg(feature = "wave_trace")] + { + self.vcd_writer + .trace_gameboy(self.clock_count, self) + .unwrap(); + } self.clock_count += count; } @@ -406,6 +426,11 @@ impl GameBoy { self.update_timer(); self.update_serial(); self.update_sound(); + + #[cfg(feature = "wave_trace")] + self.vcd_writer + .trace_gameboy(self.clock_count, self) + .unwrap(); } fn update_ppu(&self) { @@ -424,7 +449,11 @@ impl GameBoy { } fn update_timer(&self) { - if self.timer.borrow_mut().update(self.clock_count) { + if self.timer.borrow_mut().update( + self.clock_count, + #[cfg(feature = "wave_trace")] + &self.vcd_writer, + ) { self.interrupt_flag .set(self.interrupt_flag.get() | (1 << 2)); } diff --git a/core/src/gameboy/cpu.rs b/core/src/gameboy/cpu.rs index 811f2e4..45d9482 100644 --- a/core/src/gameboy/cpu.rs +++ b/core/src/gameboy/cpu.rs @@ -99,6 +99,10 @@ pub struct Cpu { pub ime: ImeState, pub state: CpuState, pub halt_bug: bool, + + /// The current opcode being executed. This is only used for debugging in the VCD trace. + #[cfg(feature = "wave_trace")] + pub op: u8, } impl fmt::Display for Cpu { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/core/src/gameboy/ppu.rs b/core/src/gameboy/ppu.rs index 79ed5a2..52281c3 100644 --- a/core/src/gameboy/ppu.rs +++ b/core/src/gameboy/ppu.rs @@ -320,11 +320,11 @@ pub struct Ppu { pub state: u8, /// When making the LY==LYC comparison, uses this value instead of ly to control the comparison /// timing. This is 0xFF if this will not update the stat. - ly_for_compare: u8, + pub ly_for_compare: u8, /// A rising edge on this signal causes a STAT interrupt. - stat_signal: bool, - ly_compare_signal: bool, + pub stat_signal: bool, + pub ly_compare_signal: bool, /// use this value instead of the current stat mode when controlling the stat interrupt signal, /// to control the timing. 0xff means that this will not trigger a interrupt. /// @@ -332,7 +332,7 @@ pub struct Ppu { /// Mode 1 - Vertical Blank /// Mode 2 - OAM Search /// Mode 3 - Drawing pixels - stat_mode_for_interrupt: u8, + pub stat_mode_for_interrupt: u8, /// Which clock cycle the PPU where last updated pub last_clock_count: u64, @@ -374,7 +374,7 @@ pub struct Ppu { /// The x position in the current scanline, from -(8 + scx%8) to 160. Negative values /// (represented by positives between 241 and 255) are use for detecting sprites that starts /// to the left of the screen, and for discarding pixels for scrolling. - scanline_x: u8, + pub scanline_x: u8, } fn dbg_fmt_hash(value: &T) -> impl std::fmt::Debug { @@ -942,6 +942,9 @@ impl Ppu { // Writing to wx do some time traveling shenanigans. Make sure they are not observable. debug_assert!(ppu.last_clock_count <= gb.clock_count); + #[cfg(feature = "wave_trace")] + gb.vcd_writer.trace_ppu(ppu.last_clock_count, ppu).unwrap(); + ppu.last_clock_count = gb.clock_count; if ppu.lcdc & 0x80 == 0 { @@ -959,9 +962,15 @@ impl Ppu { if ppu.next_clock_count >= gb.clock_count { Self::update_dma(gb, ppu, gb.clock_count); + + #[cfg(feature = "wave_trace")] + gb.vcd_writer.trace_ppu(gb.clock_count, ppu).unwrap(); } while ppu.next_clock_count < gb.clock_count { + #[cfg(feature = "wave_trace")] + let curr_ppu_clock_count = ppu.next_clock_count; + Self::update_dma(gb, ppu, ppu.next_clock_count); // println!("state: {}", state); match ppu.state { @@ -1026,7 +1035,10 @@ impl Ppu { 6 => { ppu.line_start_clock_count = ppu.next_clock_count; ppu.screen_x = 0; - if gb.clock_count > ppu.next_clock_count + 456 { + + let use_optimization = !cfg!(feature = "wave_trace"); + + if use_optimization && gb.clock_count > ppu.next_clock_count + 456 { if ppu.wy == ppu.ly { ppu.reach_window = true; } @@ -1518,6 +1530,9 @@ impl Ppu { } _ => unreachable!(), } + + #[cfg(feature = "wave_trace")] + gb.vcd_writer.trace_ppu(curr_ppu_clock_count, ppu).unwrap(); } Self::update_dma(gb, ppu, gb.clock_count); diff --git a/core/src/gameboy/timer.rs b/core/src/gameboy/timer.rs index db93087..df0d5cd 100644 --- a/core/src/gameboy/timer.rs +++ b/core/src/gameboy/timer.rs @@ -97,7 +97,15 @@ impl Timer { /// /// Update the Timer to the given `clock_count`, in O(1). See [Timer::update_cycle_by_cycle] for /// a more straigh-forward implementation. - pub fn update(&mut self, clock_count: u64) -> bool { + #[cfg_attr(feature = "wave_trace", allow(unreachable_code))] + pub fn update( + &mut self, + clock_count: u64, + #[cfg(feature = "wave_trace")] vcd_writer: &crate::wave_trace::WaveTrace, + ) -> bool { + #[cfg(feature = "wave_trace")] + return self.update_cycle_by_cycle(clock_count, vcd_writer); + debug_assert!(clock_count >= self.last_clock_count); if clock_count <= self.last_clock_count { self.next_interrupt = self.estimate_next_interrupt(); @@ -189,7 +197,11 @@ impl Timer { /// Return true if there is a interrupt /// /// Reference implementation to [Self::update]. Slower, but less prone to bugs. - pub fn update_cycle_by_cycle(&mut self, clock_count: u64) -> bool { + pub fn update_cycle_by_cycle( + &mut self, + clock_count: u64, + #[cfg(feature = "wave_trace")] vcd_writer: &crate::wave_trace::WaveTrace, + ) -> bool { let mut interrupt = false; for _clock in self.last_clock_count..clock_count { @@ -219,6 +231,9 @@ impl Timer { } self.last_counter_bit = counter_bit; + + #[cfg(feature = "wave_trace")] + vcd_writer.trace_timer(_clock, self).unwrap(); } self.last_clock_count = clock_count; @@ -305,7 +320,7 @@ impl Timer { } } -#[cfg(test)] +#[cfg(all(test, not(feature = "wave_trace")))] mod tests { use super::*; use rand::Rng; diff --git a/core/src/interpreter.rs b/core/src/interpreter.rs index 7619fea..1fad55f 100644 --- a/core/src/interpreter.rs +++ b/core/src/interpreter.rs @@ -80,8 +80,20 @@ impl Interpreter<'_> { return; } - use Condition::*; + #[cfg(feature = "wave_trace")] + { + const MB: usize = 1024 * 1024; + if self.0.vcd_writer.buffer_size() > 8 * MB { + self.0.update_all(); + println!("VCD committed: {}", self.0.clock_count); + self.0.vcd_writer.commit().unwrap(); + } + + self.0.cpu.op = self.0.read(self.0.cpu.pc); + } + let op = self.read_next_pc(); + let trace = false; if trace { println!( @@ -99,6 +111,8 @@ impl Interpreter<'_> { self.0.cpu.l, ); } + + use Condition::*; match op { // NOP 1:4 - - - - 0x00 => self.nop(), @@ -1289,15 +1303,30 @@ impl Interpreter<'_> { } } - fn gb_read(&self, address: u16) -> u8 { + fn gb_read(&mut self, address: u16) -> u8 { #[allow(clippy::let_and_return)] // being useful for debugging let value = self.0.read(address); + + #[cfg(feature = "wave_trace")] + self.0 + .vcd_writer + .trace_gameboy_ex(self.0.clock_count, self.0, Some((address, value, false))) + .unwrap(); + + // #[cfg(feature = "wave_trace")] + // self.0. + #[cfg(feature = "io_trace")] self.0.io_trace.borrow_mut().push(( GameBoy::IO_READ | ((self.0.clock_count & !3) as u8 >> 1), address, value, )); + + // bypass tick to avoid wave_trace + // self.0.tick(4); + self.0.clock_count += 4; + value } @@ -1308,22 +1337,30 @@ impl Interpreter<'_> { address, value, )); + + #[cfg(feature = "wave_trace")] + self.0 + .vcd_writer + .trace_gameboy_ex(self.0.clock_count, self.0, Some((address, value, true))) + .unwrap(); + self.0.write(address, value); + + // bypass tick to avoid wave_trace + // self.0.tick(4); + self.0.clock_count += 4; } fn gb_write16(&mut self, address: u16, value: u16) { let [a, b] = value.to_le_bytes(); self.gb_write(address, a); - self.0.tick(4); self.gb_write(address.wrapping_add(1), b); - self.0.tick(4); } /// Read from PC, tick 4 cycles, and increase it by 1 #[inline(always)] pub fn read_next_pc(&mut self) -> u8 { - let v = self.0.read(self.0.cpu.pc); - self.0.tick(4); + let v = self.gb_read(self.0.cpu.pc); if self.0.cpu.halt_bug { self.0.cpu.halt_bug = false; } else { @@ -1351,34 +1388,18 @@ impl Interpreter<'_> { Reg::Im8 => self.read_next_pc(), Reg::Im16 => { let address = self.read_next_pc16(); - let v = self.gb_read(address); - self.0.tick(4); - v - } - Reg::BC => { - let v = self.gb_read(self.0.cpu.bc()); - self.0.tick(4); - v - } - Reg::DE => { - let v = self.gb_read(self.0.cpu.de()); - self.0.tick(4); - v - } - Reg::HL => { - let v = self.gb_read(self.0.cpu.hl()); - self.0.tick(4); - v + self.gb_read(address) } + Reg::BC => self.gb_read(self.0.cpu.bc()), + Reg::DE => self.gb_read(self.0.cpu.de()), + Reg::HL => self.gb_read(self.0.cpu.hl()), Reg::HLI => { let v = self.gb_read(self.0.cpu.hl()); - self.0.tick(4); self.0.cpu.set_hl(add16(self.0.cpu.hl(), 1)); v } Reg::HLD => { let v = self.gb_read(self.0.cpu.hl()); - self.0.tick(4); self.0.cpu.set_hl(sub16(self.0.cpu.hl(), 1)); v } @@ -1397,33 +1418,26 @@ impl Interpreter<'_> { Reg::L => self.0.cpu.l = value, Reg::Im8 => { self.gb_write(add16(self.0.cpu.pc, 1), value); - self.0.tick(4); } Reg::Im16 => { let adress = self.read_next_pc16(); self.gb_write(adress, value); - self.0.tick(4); } Reg::BC => { self.gb_write(self.0.cpu.bc(), value); - self.0.tick(4); } Reg::DE => { self.gb_write(self.0.cpu.de(), value); - self.0.tick(4); } Reg::HL => { self.gb_write(self.0.cpu.hl(), value); - self.0.tick(4); } Reg::HLI => { self.gb_write(self.0.cpu.hl(), value); - self.0.tick(4); self.0.cpu.set_hl(add16(self.0.cpu.hl(), 1)); } Reg::HLD => { self.gb_write(self.0.cpu.hl(), value); - self.0.tick(4); self.0.cpu.set_hl(sub16(self.0.cpu.hl(), 1)); } Reg::SP => unreachable!(), @@ -1507,17 +1521,13 @@ impl Interpreter<'_> { let [lsb, msb] = value.to_le_bytes(); self.0.tick(4); // 1 M-cycle with SP in address buss self.gb_write(sub16(self.0.cpu.sp, 1), msb); - self.0.tick(4); // 1 M-cycle with SP-1 in address buss self.gb_write(sub16(self.0.cpu.sp, 2), lsb); - self.0.tick(4); // 1 M-cycle with SP-2 in address buss self.0.cpu.sp = sub16(self.0.cpu.sp, 2); } fn popr(&mut self) -> u16 { let lsp = self.gb_read(self.0.cpu.sp); - self.0.tick(4); // 1 M-cycle with SP in address buss let msp = self.gb_read(add16(self.0.cpu.sp, 1)); - self.0.tick(4); // 1 M-cycle with SP+1 in address buss self.0.cpu.sp = add16(self.0.cpu.sp, 2); u16::from_be_bytes([msp, lsp]) } @@ -1586,16 +1596,10 @@ impl Interpreter<'_> { pub fn loadh(&mut self, dst: Reg, src: Reg) { let src = match src { Reg::A => self.0.cpu.a, - Reg::C => { - let v = self.gb_read(0xFF00 | self.0.cpu.c as u16); - self.0.tick(4); - v - } + Reg::C => self.gb_read(0xFF00 | self.0.cpu.c as u16), Reg::Im8 => { let r8 = self.read_next_pc(); - let v = self.gb_read(0xFF00 | r8 as u16); - self.0.tick(4); - v + self.gb_read(0xFF00 | r8 as u16) } _ => unreachable!(), }; @@ -1604,12 +1608,10 @@ impl Interpreter<'_> { Reg::A => self.0.cpu.a = src, Reg::C => { self.gb_write(0xFF00 | self.0.cpu.c as u16, src); - self.0.tick(4); } Reg::Im8 => { let r8 = self.read_next_pc(); self.gb_write(0xFF00 | r8 as u16, src); - self.0.tick(4); } _ => unreachable!(), } diff --git a/core/src/lib.rs b/core/src/lib.rs index a668937..ad35932 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -6,3 +6,6 @@ pub mod gameboy; pub mod interpreter; pub mod parser; pub mod save_state; + +#[cfg(feature = "wave_trace")] +mod wave_trace; diff --git a/core/src/wave_trace.rs b/core/src/wave_trace.rs new file mode 100644 index 0000000..b0f633c --- /dev/null +++ b/core/src/wave_trace.rs @@ -0,0 +1,375 @@ +//! Trace the GameBoy state to a VCD wave file for debugging. Its main purpose is to compare the +//! emulator's state with the state of the [DMG-SIM](https://github.com/msinger/dmg-sim) and +//! [dmgcpu](https://github.com/emu-russia/dmgcpu) Verilog simulations. + +use std::cell::{Cell, RefCell}; +use std::fs::File; +use std::io::BufWriter; + +use crate::gameboy::cpu::Cpu; +use crate::gameboy::ppu::Ppu; +use crate::gameboy::timer::Timer; +use crate::gameboy::GameBoy; + +// NOTE: The actual clock period should be 1/(2^22 Hz) = 238.419 ns, but msinger's +// dmg-sim (the verilog simulation I am comparing to) uses a period of 240 ns. +const TIMESCALE: u64 = 1; // ns +const CYCLE_PERIOD: u64 = 240 / TIMESCALE; + +// Offset the timestampst to make it align with the other simulations. +const OFFSET: i64 = (-5625669120) / TIMESCALE as i64; + +// Convert clock count to timestamp +fn clock_to_timestamp(clock: u64) -> u64 { + (clock * CYCLE_PERIOD).saturating_add_signed(OFFSET) +} + +type MaxWidth = u16; +type WireIndex = u8; + +macro_rules! decl_regs { + ($name:ident, $module:literal, $var:ident : $type:ty, $($reg:ident, $width:expr => $value:expr;)*) => { + pub struct $name { + $($reg: WireIndex,)* + } + impl $name { + fn new(writer: &mut MyWriter) -> std::io::Result { + writer.add_module($module)?; + let this = Self { + $($reg: writer.add_wire($width, stringify!($reg))?,)* + }; + writer.close_module()?; + Ok(this) + } + + fn trace(&self, clock_count: u64, writer: &mut MyWriter, $var: $type) -> std::io::Result<()> { + $(writer.change(clock_count, self.$reg, ($value) as MaxWidth)?;)* + Ok(()) + } + } + }; +} + +decl_regs! { + GameboyRegs, "gameboy", gb: &GameBoy, + serial_data, 8 => gb.serial.borrow().serial_data; + serial_control, 8 => gb.serial.borrow().serial_control; + joypad_io, 8 => gb.joypad_io; + joypad, 8 => gb.joypad; + interrupt_flag, 8 => gb.interrupt_flag.get(); + dma, 8 => gb.dma; + interrupt_enabled, 8 => gb.interrupt_enabled; + v_blank_trigger, 1 => gb.v_blank_trigger.get() as u8; +} + +decl_regs! { + CpuRegs, "cpu", cpu: &Cpu, + f, 8 => cpu.f.0; + a, 8 => cpu.a; + c, 8 => cpu.c; + b, 8 => cpu.b; + e, 8 => cpu.e; + d, 8 => cpu.d; + l, 8 => cpu.l; + h, 8 => cpu.h; + sp, 16 => cpu.sp; + pc, 16 => cpu.pc; + ime, 2 => cpu.ime; + state, 2 => cpu.state; + halt_bug, 1 => cpu.halt_bug; + op, 8 => cpu.op; +} + +decl_regs! { + PpuRegs, "ppu", ppu: &Ppu, + lcdc, 8 => ppu.lcdc; + stat, 8 => ppu.stat; + scy, 8 => ppu.scy; + scx, 8 => ppu.scx; + ly, 8 => ppu.ly; + lyc, 8 => ppu.lyc; + bgp, 8 => ppu.bgp; + obp0, 8 => ppu.obp0; + obp1, 8 => ppu.obp1; + wy, 8 => ppu.wy; + wx, 8 => ppu.wx; + + state, 8 => ppu.state; + ly_for_compare, 8 => ppu.ly_for_compare; + stat_signal, 1 => ppu.stat_signal; + ly_compare_signal, 1 => ppu.ly_compare_signal; + stat_mode_for_interrupt, 2 => ppu.stat_mode_for_interrupt; + scanline_x, 8 => ppu.scanline_x; +} + +decl_regs! { + TimerRegs, "timer", timer: &Timer, + div, 16 => timer.div; + tima, 8 => timer.tima; + tma, 8 => timer.tma; + tac, 8 => timer.tac; + loading, 8 => timer.loading; +} + +struct Wire { + id: vcd::IdCode, + value: std::cell::Cell, + width: u8, + name: String, +} + +struct MyWriter { + writer: vcd::Writer>, + // Buffer of (timestamp, wire, value) + buffer: Vec<(u64, WireIndex, MaxWidth)>, + wires: Vec, + last_commit: u64, +} +impl MyWriter { + fn new(filename: &str) -> std::io::Result { + let file = std::fs::File::create(filename).unwrap(); + let buf = std::io::BufWriter::new(file); + let mut writer = vcd::Writer::new(buf); + + writer.timescale(TIMESCALE as u32, vcd::TimescaleUnit::NS)?; + + Ok(Self { + writer, + buffer: Vec::new(), + wires: Vec::new(), + last_commit: 0, + }) + } + + fn add_module(&mut self, name: &str) -> std::io::Result<()> { + self.writer.add_module(name) + } + + fn close_module(&mut self) -> std::io::Result<()> { + self.writer.upscope() + } + + fn add_wire(&mut self, width: u8, name: &str) -> std::io::Result { + let id = self.writer.add_wire(width as u32, name)?; + let index = self.wires.len(); + + if index > WireIndex::MAX as usize { + panic!("Too many wires"); + } + + let wire = Wire { + id, + width, + value: 0x8000.into(), + name: name.to_string(), + }; + + self.wires.push(wire); + + Ok(index as WireIndex) + } + + fn change( + &mut self, + clock_count: u64, + index: WireIndex, + value: MaxWidth, + ) -> std::io::Result<()> { + let timestamp = clock_to_timestamp(clock_count); + self.change_ns(timestamp, index, value) + } + + #[inline(never)] + fn change_ns( + &mut self, + timestamp: u64, + index: WireIndex, + value: MaxWidth, + ) -> std::io::Result<()> { + let wire = self.wires.get(index as usize).unwrap(); + debug_assert!( + self.last_commit <= timestamp, + "{} < {} at {}", + self.last_commit, + timestamp, + wire.name + ); + + if wire.value.get() == value { + return Ok(()); + } + wire.value.set(value); + + if self + .buffer + .last() + .map_or(false, |&(t, i, _)| t == timestamp && i == index) + { + self.buffer.pop(); + } + self.buffer.push((timestamp, index, value)); + + Ok(()) + } + + fn begin(&mut self) -> std::io::Result<()> { + self.writer.enddefinitions()?; + self.writer.begin(vcd::SimulationCommand::Dumpvars)?; + Ok(()) + } + + fn commit(&mut self) -> std::io::Result<()> { + self.buffer + .sort_unstable_by_key(|(timestamp, _, _)| *timestamp); + + for (timestamp, index, value) in self.buffer.drain(..) { + if timestamp != self.last_commit { + debug_assert!( + self.last_commit <= timestamp, + "{} < {}", + self.last_commit, + timestamp + ); + self.writer.timestamp(timestamp)?; + self.last_commit = timestamp; + } + + let wire = self.wires.get(index as usize).unwrap(); + if wire.width == 1 { + self.writer.change_scalar(wire.id, value == 1)?; + } else { + self.writer + .change_vector(wire.id, to_bits(wire.width, value))?; + } + } + + Ok(()) + } +} + +pub struct WaveTrace { + writer: RefCell, + last_clock_count: Cell, + clk: WireIndex, + address_bus: WireIndex, + data_bus: WireIndex, + read: WireIndex, + write: WireIndex, + gameboy_regs: GameboyRegs, + cpu_regs: CpuRegs, + ppu_regs: PpuRegs, + timer_regs: TimerRegs, +} +impl WaveTrace { + pub fn new() -> std::io::Result { + let filename = "wave_trace/trace.vcd"; + + // create dir if not exists + std::fs::create_dir_all("wave_trace").unwrap(); + + let mut writer = MyWriter::new(filename)?; + + writer.add_module("gameroy")?; + + let clk = writer.add_wire(1, "clk")?; + let address_bus = writer.add_wire(16, "address_bus")?; + let data_bus = writer.add_wire(8, "data_bus")?; + let read = writer.add_wire(1, "read")?; + let write = writer.add_wire(1, "write")?; + + let gameboy_regs = GameboyRegs::new(&mut writer)?; + let cpu_regs = CpuRegs::new(&mut writer)?; + let ppu_regs = PpuRegs::new(&mut writer)?; + let timer_regs = TimerRegs::new(&mut writer)?; + + writer.close_module()?; + + writer.begin()?; + + let this = Self { + writer: RefCell::new(writer), + last_clock_count: u64::MAX.into(), + clk, + address_bus, + data_bus, + read, + write, + gameboy_regs, + cpu_regs, + ppu_regs, + timer_regs, + }; + + Ok(this) + } + + pub fn trace_gameboy(&self, clock_count: u64, gameboy: &GameBoy) -> std::io::Result<()> { + self.trace_gameboy_ex(clock_count, gameboy, None) + } + + pub fn trace_gameboy_ex( + &self, + clock_count: u64, + gameboy: &GameBoy, + bus: Option<(u16, u8, bool)>, + ) -> std::io::Result<()> { + let mut writer = self.writer.borrow_mut(); + + if self.last_clock_count.get() != u64::MAX { + for c in self.last_clock_count.get()..clock_count { + let t = clock_to_timestamp(c); + let delta = CYCLE_PERIOD / 2; + writer.change_ns(t + delta, self.clk, 0)?; + writer.change_ns(t + 2 * delta, self.clk, 1)?; + } + } else { + writer.change(clock_count, self.clk, 1)?; + } + + if let Some(bus) = bus { + writer.change(clock_count - 4, self.address_bus, bus.0 as MaxWidth)?; + writer.change(clock_count - 4, self.data_bus, bus.1 as MaxWidth)?; + writer.change(clock_count - 4, self.read, !bus.2 as MaxWidth)?; + writer.change(clock_count - 4, self.write, bus.2 as MaxWidth)?; + } else { + writer.change(clock_count - 4, self.read, 0)?; + writer.change(clock_count - 4, self.write, 0)?; + } + + self.last_clock_count.set(clock_count); + + self.gameboy_regs.trace(clock_count, &mut writer, gameboy)?; + self.cpu_regs + .trace(clock_count, &mut writer, &gameboy.cpu)?; + + Ok(()) + } + + pub fn trace_ppu(&self, clock_count: u64, ppu: &Ppu) -> std::io::Result<()> { + let mut writer = self.writer.borrow_mut(); + + self.ppu_regs.trace(clock_count, &mut writer, ppu)?; + + Ok(()) + } + + pub fn trace_timer(&self, clock_count: u64, timer: &Timer) -> std::io::Result<()> { + let mut writer = self.writer.borrow_mut(); + + self.timer_regs.trace(clock_count, &mut writer, timer)?; + + Ok(()) + } + + pub fn buffer_size(&self) -> usize { + self.writer.borrow().buffer.len() * std::mem::size_of::<(u64, WireIndex, MaxWidth)>() + } + + pub fn commit(&self) -> std::io::Result<()> { + self.writer.borrow_mut().commit() + } +} + +fn to_bits(n: u8, value: MaxWidth) -> impl Iterator { + (0..n).rev().map(move |i| ((value >> i) & 1 == 1).into()) +}