Skip to content

Commit 69aca8f

Browse files
committed
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.
1 parent 7195185 commit 69aca8f

File tree

11 files changed

+525
-66
lines changed

11 files changed

+525
-66
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ log.txt
66
test_rom/test.o
77
core/test_output
88
core/tests/gameboy-test-roms
9+
core/vcd_trace
910
license/license.html
1011
android/app/release
1112
gameroy.log

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ harness = false
1818

1919
[features]
2020
io_trace = []
21+
wave_trace = ["dep:vcd"]
2122

2223
[dependencies]
24+
vcd = { version = "0.7.0", optional = true }
2325

2426
[dev-dependencies]
2527
image = { version = "0.25.4", default-features = false, features = ["png"] }

core/src/bin/run.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use gameroy::{
55
};
66

77
fn parse_timeout(timeout: &str) -> Option<u64> {
8-
Some(if timeout.ends_with("s") {
9-
timeout[..timeout.len() - 1].parse::<u64>().ok()? * CLOCK_SPEED
10-
} else if timeout.ends_with("ms") {
11-
timeout[..timeout.len() - 2].parse::<u64>().ok()? * CLOCK_SPEED / 1000
8+
Some(if let Some(value) = timeout.strip_suffix("s") {
9+
value.parse::<u64>().ok()? * CLOCK_SPEED
10+
} else if let Some(value) = timeout.strip_suffix("ms") {
11+
value.parse::<u64>().ok()? * CLOCK_SPEED / 1000
1212
} else {
1313
timeout.parse::<u64>().ok()?
1414
})
@@ -69,4 +69,10 @@ fn main() {
6969
break;
7070
}
7171
}
72+
#[cfg(feature = "wave_trace")]
73+
{
74+
inter.0.update_all();
75+
println!("VCD committed on end: {}", inter.0.clock_count);
76+
inter.0.vcd_writer.commit().unwrap();
77+
}
7278
}

core/src/gameboy.rs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ pub struct GameBoy {
7171
/// trace of reads and writes. (kind | ((clock_count & !3) >> 1), address, value), kind: 0=GameBoy::IO_READ,1=GameBoy::IO_WRITE
7272
#[cfg(feature = "io_trace")]
7373
pub io_trace: RefCell<Vec<(u8, u16, u8)>>,
74+
75+
/// VCD writer for the waveform tracing.
76+
#[cfg(feature = "wave_trace")]
77+
pub vcd_writer: crate::wave_trace::WaveTrace,
7478
}
7579

7680
impl std::fmt::Debug for GameBoy {
@@ -181,13 +185,12 @@ impl GameBoy {
181185

182186
#[cfg(feature = "io_trace")]
183187
io_trace: Vec::new().into(),
188+
189+
#[cfg(feature = "wave_trace")]
190+
vcd_writer: crate::wave_trace::WaveTrace::new().unwrap(),
184191
};
185192

186-
if this.boot_rom.is_none() {
187-
this.reset_after_boot();
188-
} else {
189-
this.reset();
190-
}
193+
this.reset();
191194

192195
this
193196
}
@@ -226,6 +229,14 @@ impl GameBoy {
226229
self.reset_after_boot();
227230
return;
228231
}
232+
self.reset_at_power_on();
233+
}
234+
235+
/// Reset the gameboy to its state after powering on, before the boot rom is executed, even if
236+
/// there is no boot rom present.
237+
///
238+
/// Only used internally for setting a trace point in clock_count = 0.
239+
pub(crate) fn reset_at_power_on(&mut self) {
229240
// TODO: Maybe I should reset the cartridge
230241
self.cpu = Cpu::default();
231242
self.wram = [0xFF; 0x2000];
@@ -260,6 +271,9 @@ impl GameBoy {
260271
ime: cpu::ImeState::Disabled,
261272
halt_bug: false,
262273
state: cpu::CpuState::Running,
274+
275+
#[cfg(feature = "wave_trace")]
276+
op: 0,
263277
};
264278

265279
self.wram = [0xFF; 0x2000];
@@ -349,6 +363,12 @@ impl GameBoy {
349363

350364
/// Advance the clock by 'count' cycles
351365
pub fn tick(&mut self, count: u64) {
366+
#[cfg(feature = "wave_trace")]
367+
{
368+
self.vcd_writer
369+
.trace_gameboy(self.clock_count, self)
370+
.unwrap();
371+
}
352372
self.clock_count += count;
353373
}
354374

@@ -406,6 +426,11 @@ impl GameBoy {
406426
self.update_timer();
407427
self.update_serial();
408428
self.update_sound();
429+
430+
#[cfg(feature = "wave_trace")]
431+
self.vcd_writer
432+
.trace_gameboy(self.clock_count, self)
433+
.unwrap();
409434
}
410435

411436
fn update_ppu(&self) {
@@ -424,7 +449,11 @@ impl GameBoy {
424449
}
425450

426451
fn update_timer(&self) {
427-
if self.timer.borrow_mut().update(self.clock_count) {
452+
if self.timer.borrow_mut().update(
453+
self.clock_count,
454+
#[cfg(feature = "wave_trace")]
455+
&self.vcd_writer,
456+
) {
428457
self.interrupt_flag
429458
.set(self.interrupt_flag.get() | (1 << 2));
430459
}

core/src/gameboy/cpu.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ pub struct Cpu {
9999
pub ime: ImeState,
100100
pub state: CpuState,
101101
pub halt_bug: bool,
102+
103+
/// The current opcode being executed. This is only used for debugging in the VCD trace.
104+
#[cfg(feature = "wave_trace")]
105+
pub op: u8,
102106
}
103107
impl fmt::Display for Cpu {
104108
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

core/src/gameboy/ppu.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,19 +320,19 @@ pub struct Ppu {
320320
pub state: u8,
321321
/// When making the LY==LYC comparison, uses this value instead of ly to control the comparison
322322
/// timing. This is 0xFF if this will not update the stat.
323-
ly_for_compare: u8,
323+
pub ly_for_compare: u8,
324324

325325
/// A rising edge on this signal causes a STAT interrupt.
326-
stat_signal: bool,
327-
ly_compare_signal: bool,
326+
pub stat_signal: bool,
327+
pub ly_compare_signal: bool,
328328
/// use this value instead of the current stat mode when controlling the stat interrupt signal,
329329
/// to control the timing. 0xff means that this will not trigger a interrupt.
330330
///
331331
/// Mode 0 - Horizontal Blank
332332
/// Mode 1 - Vertical Blank
333333
/// Mode 2 - OAM Search
334334
/// Mode 3 - Drawing pixels
335-
stat_mode_for_interrupt: u8,
335+
pub stat_mode_for_interrupt: u8,
336336

337337
/// Which clock cycle the PPU where last updated
338338
pub last_clock_count: u64,
@@ -374,7 +374,7 @@ pub struct Ppu {
374374
/// The x position in the current scanline, from -(8 + scx%8) to 160. Negative values
375375
/// (represented by positives between 241 and 255) are use for detecting sprites that starts
376376
/// to the left of the screen, and for discarding pixels for scrolling.
377-
scanline_x: u8,
377+
pub scanline_x: u8,
378378
}
379379

380380
fn dbg_fmt_hash<T: std::hash::Hash>(value: &T) -> impl std::fmt::Debug {
@@ -942,6 +942,9 @@ impl Ppu {
942942
// Writing to wx do some time traveling shenanigans. Make sure they are not observable.
943943
debug_assert!(ppu.last_clock_count <= gb.clock_count);
944944

945+
#[cfg(feature = "wave_trace")]
946+
gb.vcd_writer.trace_ppu(ppu.last_clock_count, ppu).unwrap();
947+
945948
ppu.last_clock_count = gb.clock_count;
946949

947950
if ppu.lcdc & 0x80 == 0 {
@@ -959,9 +962,15 @@ impl Ppu {
959962

960963
if ppu.next_clock_count >= gb.clock_count {
961964
Self::update_dma(gb, ppu, gb.clock_count);
965+
966+
#[cfg(feature = "wave_trace")]
967+
gb.vcd_writer.trace_ppu(gb.clock_count, ppu).unwrap();
962968
}
963969

964970
while ppu.next_clock_count < gb.clock_count {
971+
#[cfg(feature = "wave_trace")]
972+
let curr_ppu_clock_count = ppu.next_clock_count;
973+
965974
Self::update_dma(gb, ppu, ppu.next_clock_count);
966975
// println!("state: {}", state);
967976
match ppu.state {
@@ -1026,7 +1035,10 @@ impl Ppu {
10261035
6 => {
10271036
ppu.line_start_clock_count = ppu.next_clock_count;
10281037
ppu.screen_x = 0;
1029-
if gb.clock_count > ppu.next_clock_count + 456 {
1038+
1039+
let use_optimization = !cfg!(feature = "wave_trace");
1040+
1041+
if use_optimization && gb.clock_count > ppu.next_clock_count + 456 {
10301042
if ppu.wy == ppu.ly {
10311043
ppu.reach_window = true;
10321044
}
@@ -1518,6 +1530,9 @@ impl Ppu {
15181530
}
15191531
_ => unreachable!(),
15201532
}
1533+
1534+
#[cfg(feature = "wave_trace")]
1535+
gb.vcd_writer.trace_ppu(curr_ppu_clock_count, ppu).unwrap();
15211536
}
15221537

15231538
Self::update_dma(gb, ppu, gb.clock_count);

core/src/gameboy/timer.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,15 @@ impl Timer {
9797
///
9898
/// Update the Timer to the given `clock_count`, in O(1). See [Timer::update_cycle_by_cycle] for
9999
/// a more straigh-forward implementation.
100-
pub fn update(&mut self, clock_count: u64) -> bool {
100+
#[cfg_attr(feature = "wave_trace", allow(unreachable_code))]
101+
pub fn update(
102+
&mut self,
103+
clock_count: u64,
104+
#[cfg(feature = "wave_trace")] vcd_writer: &crate::wave_trace::WaveTrace,
105+
) -> bool {
106+
#[cfg(feature = "wave_trace")]
107+
return self.update_cycle_by_cycle(clock_count, vcd_writer);
108+
101109
debug_assert!(clock_count >= self.last_clock_count);
102110
if clock_count <= self.last_clock_count {
103111
self.next_interrupt = self.estimate_next_interrupt();
@@ -189,7 +197,11 @@ impl Timer {
189197
/// Return true if there is a interrupt
190198
///
191199
/// Reference implementation to [Self::update]. Slower, but less prone to bugs.
192-
pub fn update_cycle_by_cycle(&mut self, clock_count: u64) -> bool {
200+
pub fn update_cycle_by_cycle(
201+
&mut self,
202+
clock_count: u64,
203+
#[cfg(feature = "wave_trace")] vcd_writer: &crate::wave_trace::WaveTrace,
204+
) -> bool {
193205
let mut interrupt = false;
194206

195207
for _clock in self.last_clock_count..clock_count {
@@ -219,6 +231,9 @@ impl Timer {
219231
}
220232

221233
self.last_counter_bit = counter_bit;
234+
235+
#[cfg(feature = "wave_trace")]
236+
vcd_writer.trace_timer(_clock, self).unwrap();
222237
}
223238

224239
self.last_clock_count = clock_count;
@@ -305,7 +320,7 @@ impl Timer {
305320
}
306321
}
307322

308-
#[cfg(test)]
323+
#[cfg(all(test, not(feature = "wave_trace")))]
309324
mod tests {
310325
use super::*;
311326
use rand::Rng;

0 commit comments

Comments
 (0)