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 new file mode 100644 index 0000000..b682832 --- /dev/null +++ b/core/src/bin/run.rs @@ -0,0 +1,78 @@ +use gameroy::{ + consts::CLOCK_SPEED, + gameboy::{cartridge::Cartridge, GameBoy}, + interpreter::Interpreter, +}; + +fn parse_timeout(timeout: &str) -> Option { + 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()? + }) +} + +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; + } + } + #[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/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; + } + } } }; 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()) +}