Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
78 changes: 78 additions & 0 deletions core/src/bin/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use gameroy::{
consts::CLOCK_SPEED,
gameboy::{cartridge::Cartridge, GameBoy},
interpreter::Interpreter,
};

fn parse_timeout(timeout: &str) -> Option<u64> {
Some(if let Some(value) = timeout.strip_suffix("s") {
value.parse::<u64>().ok()? * CLOCK_SPEED
} else if let Some(value) = timeout.strip_suffix("ms") {
value.parse::<u64>().ok()? * CLOCK_SPEED / 1000
} else {
timeout.parse::<u64>().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();
}
}
41 changes: 35 additions & 6 deletions core/src/gameboy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<(u8, u16, u8)>>,

/// VCD writer for the waveform tracing.
#[cfg(feature = "wave_trace")]
pub vcd_writer: crate::wave_trace::WaveTrace,
}

impl std::fmt::Debug for GameBoy {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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));
}
Expand Down
13 changes: 10 additions & 3 deletions core/src/gameboy/cartridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
};

Expand Down
4 changes: 4 additions & 0 deletions core/src/gameboy/cpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 21 additions & 6 deletions core/src/gameboy/ppu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,19 +320,19 @@ 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.
///
/// Mode 0 - Horizontal Blank
/// 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,
Expand Down Expand Up @@ -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<T: std::hash::Hash>(value: &T) -> impl std::fmt::Debug {
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
21 changes: 18 additions & 3 deletions core/src/gameboy/timer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -305,7 +320,7 @@ impl Timer {
}
}

#[cfg(test)]
#[cfg(all(test, not(feature = "wave_trace")))]
mod tests {
use super::*;
use rand::Rng;
Expand Down
Loading
Loading