#![feature(exclusive_range_pattern, let_chains, slice_flatten, async_closure)] mod processor; mod util; use clap::{ArgGroup, Parser}; use gilrs::Gilrs; use minifb::{Window, WindowOptions}; use once_cell::sync::OnceCell; use processor::{ memory::{Memory, Rom}, Cpu, }; use std::{ fs, io::{self, stdout, Write}, }; use crate::processor::gpu; #[macro_export] macro_rules! verbose_println { ($($tts:tt)*) => { if $crate::is_verbose() { println!($($tts)*); } }; } #[macro_export] macro_rules! verbose_print { ($($tts:tt)*) => { if $crate::is_verbose() { print!($($tts)*); } }; } /// Gameboy (DMG-A/B/C) emulator #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] #[command(group(ArgGroup::new("prints").args(["verbose","cycle_count"])))] struct Args { /// ROM path #[arg(short, long)] rom: String, /// BootROM path #[arg(short, long)] bootrom: String, /// Just run BootROM #[arg(long)] run_bootrom: bool, /// Verbose print #[arg(short, long)] verbose: bool, /// Show cycle count #[arg(short, long)] cycle_count: bool, /// Show tile window #[arg(short, long)] tile_window: bool, /// Step emulation by... #[arg(long)] step_by: Option, /// Step emulation by... #[arg(short, long)] scale_factor: Option, } static mut PAUSE_ENABLED: bool = false; static mut PAUSE_QUEUED: bool = false; static VERBOSE: OnceCell = OnceCell::new(); const WIDTH: usize = 160; const HEIGHT: usize = 144; static FACTOR: OnceCell = OnceCell::new(); fn main() { let args = Args::parse(); VERBOSE.set(args.verbose).unwrap(); FACTOR .set(if let Some(factor) = args.scale_factor { factor } else { 3 }) .unwrap(); gpu::init_statics(); let rom: Rom = match fs::read(args.rom) { Ok(data) => Rom::load(data), Err(e) => { println!("Error reading ROM: {e}"); return; } }; let bootrom: Vec = match fs::read(args.bootrom) { Ok(data) => data, Err(e) => { println!("Error reading bootROM: {e}"); return; } }; let mut window = Window::new( format!("{} on {}", rom.get_title(), rom.mbc_type()).as_str(), WIDTH * FACTOR.get().unwrap(), HEIGHT * FACTOR.get().unwrap(), WindowOptions::default(), ) .unwrap_or_else(|e| { panic!("{e}"); }); window.set_position(500, 50); window.topmost(true); let mut cpu = Cpu::new( Memory::init(bootrom, args.run_bootrom, rom), window, args.tile_window, args.run_bootrom, Gilrs::new().unwrap(), ); let mut cycle_num = 0; verbose_println!("\n\n Begin execution...\n"); match args.step_by { Some(step_size) => loop { for _ in 0..step_size { cycle_num += 1; if args.cycle_count { print_cycles(&cycle_num); } run_cycle(&mut cpu); } print!(" ...{cycle_num} cycles - press enter to continue\r"); stdout().flush().unwrap(); pause(); }, None => loop { cycle_num += 1; if args.cycle_count { print_cycles(&cycle_num); } run_cycle(&mut cpu); }, } } fn run_cycle(cpu: &mut Cpu) { let will_pause = unsafe { PAUSE_QUEUED }; let pause_enabled = unsafe { PAUSE_ENABLED }; cpu.exec_next(); if !pause_enabled && cpu.reg.pc >= 0x100 { unsafe { PAUSE_ENABLED = true }; } if will_pause { pause_then_step(); } } fn pause_then_step() { unsafe { if PAUSE_ENABLED { let line = pause(); PAUSE_QUEUED = !line.contains("continue"); } } } #[allow(dead_code)] fn pause_once() { println!("paused..."); pause(); } fn pause() -> String { let mut line = String::new(); match io::stdin().read_line(&mut line) { Ok(_) => line, Err(_) => String::from(""), } } fn print_cycles(cycles: &i32) { if *cycles % 45678 != 0 { return; } let instructions_per_second = 400000; print!( "cycle {} - approx {} seconds on real hardware\r", cycles, cycles / instructions_per_second ); stdout().flush().unwrap(); } fn is_verbose() -> bool { match VERBOSE.get() { Some(v) => *v, None => false, } }