diff --git a/src/args.rs b/src/args.rs index 1f50402..84a21ec 100644 --- a/src/args.rs +++ b/src/args.rs @@ -13,6 +13,7 @@ pub struct Porcel8ProgramArgs { help = "Emulate new behaviour of instructions (As seen in Chip-48 and SuperChip8)", default_value_t = true )] + /// Use updated CHIP-8 behaviours. pub new_chip8_behaviour: bool, #[arg( short='i', @@ -20,5 +21,12 @@ pub struct Porcel8ProgramArgs { help = "Halt on invalid instruction", default_value_t = false )] + /// Halt on finding invalid instruction pub halt_on_invalid: bool, + /// Enable Instruction Throttling + #[arg(short, default_value_t=true)] + pub do_instruction_throttling: bool, + /// Target Instructions per second, if throttling is enabled + #[arg(short='t',long,default_value_t=5_000u64)] + pub ips_throttling_rate: u64 } diff --git a/src/device/device.rs b/src/device/device.rs index b22da79..1e840c5 100644 --- a/src/device/device.rs +++ b/src/device/device.rs @@ -1,7 +1,7 @@ use crate::{device::instruction::Instruction, util::EmulatorError}; use crate::device::keyboard::Keyboard; use crate::device::timer::DeviceTimerManager; -use crate::util::EmulatorResult; +use crate::util::{DeviceConfig, EmulatorResult}; use rand::random; use rand::seq::IteratorRandom; use std::sync::{Arc, Mutex}; @@ -14,9 +14,8 @@ pub struct Device { pub timer: DeviceTimerManager, pub stack: Vec, pub frame_buffer: Arc>>, - pub new_chip8_mode: bool, - pub halt_on_invalid: bool, pub device_keyboard: Keyboard, + pub device_config: DeviceConfig } impl Device { @@ -28,8 +27,7 @@ impl Device { timer: DeviceTimerManager, fb: Arc>>, device_keyboard: Keyboard, - new_chip8_mode: bool, - halt_on_invalid: bool + device_config: DeviceConfig ) -> Device { let memory = vec![0u8; Self::DEVICE_MEMORY_SIZE] .into_boxed_slice() @@ -41,10 +39,9 @@ impl Device { memory, frame_buffer: fb, stack: Vec::with_capacity(16), - halt_on_invalid, timer, - new_chip8_mode, device_keyboard, + device_config } } } @@ -55,11 +52,6 @@ impl Device { const FONT_DEFAULT_MEM_LOCATION_END: usize = 0x9F; const ROM_START: usize = 0x200; - // Throttling configuration for cpu - const DO_CHIP_CPU_THROTTLING:bool = false; - const TARGET_CPU_SPEED_INSTRUCTIONS_PER_SECOND: u64 = 10000; - const TARGET_CPU_INSTRUCTION_TIME: Duration = Duration::from_micros(1_000_000/Self::TARGET_CPU_SPEED_INSTRUCTIONS_PER_SECOND); - pub fn cycle(&mut self) -> EmulatorResult<()> { let time_start = std::time::Instant::now(); self.device_keyboard.update_keyboard_registers()?; @@ -71,9 +63,9 @@ impl Device { let instruction = Instruction::decode_instruction(instr_slice); self.execute_instruction(instruction)?; - let instruction_time = time_start.elapsed(); - let time_left_to_sleep_for_instruction = Self::TARGET_CPU_INSTRUCTION_TIME.checked_sub(instruction_time).unwrap_or(Duration::ZERO); - if Self::DO_CHIP_CPU_THROTTLING { + if let Some(throttling_duration) = self.device_config.get_throttling_config() { + let instruction_time = time_start.elapsed(); + let time_left_to_sleep_for_instruction = throttling_duration.checked_sub(instruction_time).unwrap_or(Duration::ZERO); log::trace!("Instruction took {:?}, left with {:?}",instruction_time,time_left_to_sleep_for_instruction); sleep(time_left_to_sleep_for_instruction); } @@ -89,7 +81,7 @@ impl Device { match instruction { Instruction::InvalidInstruction => { log::info!("Executing passthrough"); - if self.halt_on_invalid { + if self.device_config.should_halt_on_invalid() { return Err(EmulatorError::IOError("Caught Invalid Instruction".to_string())); } }, @@ -152,7 +144,7 @@ impl Device { } } Instruction::JumpWithOffset(x, num) => { - let regnum = if self.new_chip8_mode { x } else { 0 }; + let regnum = if self.device_config.is_new_chip8() { x } else { 0 }; let new_pc = self.registers.v[regnum] as u16 + num; self.registers.pc = new_pc; } @@ -205,7 +197,7 @@ impl Device { self.set_flag_register(!is_overflow); } Instruction::RShift(x, y) => { - if !self.new_chip8_mode { + if !self.device_config.is_new_chip8() { self.registers.v[x] = self.registers.v[y]; } let val = self.registers.v[x]; @@ -214,7 +206,7 @@ impl Device { self.set_flag_register(bit_carry); } Instruction::LShift(x, y) => { - if !self.new_chip8_mode { + if !self.device_config.is_new_chip8() { self.registers.v[x] = self.registers.v[y]; } let left = self.registers.v[x]; @@ -239,7 +231,7 @@ impl Device { let reg_value = self.registers.v[x]; let index_original = self.registers.i; // newer instruction set requires wrapping on 12 bit overflow, and setting vf - let addn_res = if self.new_chip8_mode { + let addn_res = if self.device_config.is_new_chip8() { let overflowing = (reg_value as u16 + index_original) >= 0x1000; self.set_flag_register(overflowing); (reg_value as u16 + index_original) % 0x1000 @@ -290,7 +282,7 @@ impl Device { let index = self.registers.i as usize; self.memory[index..=(index + last_reg_to_store)].copy_from_slice(reg_slice); // Old Chip8 used to use i as a incrementing index - if !self.new_chip8_mode { + if !self.device_config.is_new_chip8() { self.registers.i += last_reg_to_store as u16 + 1; } } @@ -299,7 +291,7 @@ impl Device { let mem_slice = &self.memory[index..=(index + last_reg_to_load)]; self.registers.v[0..=last_reg_to_load].copy_from_slice(mem_slice); // Old Chip8 used to use i as a incrementing index - if !self.new_chip8_mode { + if !self.device_config.is_new_chip8() { self.registers.i += last_reg_to_load as u16 + 1; } } diff --git a/src/main.rs b/src/main.rs index 8f5b4ca..03abf89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use sdl2::render::BlendMode; use sdl2::render::WindowCanvas; use simple_logger::SimpleLogger; +use util::DeviceConfig; use crate::args::Porcel8ProgramArgs; use crate::device::Device; @@ -33,7 +34,7 @@ const WINDOW_TITLE: &str = "porcel8"; fn main() -> EmulatorResult<()> { SimpleLogger::new().with_level(LevelFilter::Info).env().init().unwrap(); - let Porcel8ProgramArgs { filename, new_chip8_behaviour, draw_scale, halt_on_invalid } = Porcel8ProgramArgs::parse(); + let Porcel8ProgramArgs { filename, new_chip8_behaviour, draw_scale, halt_on_invalid, do_instruction_throttling, ips_throttling_rate: ipms_throttling_rate } = Porcel8ProgramArgs::parse(); log::info!("Started emulator"); @@ -45,8 +46,8 @@ fn main() -> EmulatorResult<()> { let (sdl_kb_adapter, device_keyboard) = SdlKeyboardAdapter::new_keyboard(); timer.start(); - - let device = Device::new(timer, frame_buffer_for_device, device_keyboard, new_chip8_behaviour, halt_on_invalid); + let device_config = DeviceConfig::new(new_chip8_behaviour, halt_on_invalid, do_instruction_throttling, ipms_throttling_rate); + let device = Device::new(timer, frame_buffer_for_device, device_keyboard, device_config); let (device_termination_signal_sender, compute_handle) = start_compute_thread(filename, device)?; @@ -111,8 +112,6 @@ fn start_compute_thread(filename: String, mut device: Device) -> EmulatorResult< panic!("Disconnected"); } device.cycle().expect("Failed to execute"); - // Put a bit of delay to slow down execution - thread::sleep(Duration::from_millis(2)) } })?; Ok((device_termination_signal_sender, compute_handle)) diff --git a/src/util.rs b/src/util.rs index c826a93..27687c4 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,17 +1,54 @@ +use crate::device::keyboard::KeyboardEvent; +use sdl2::video::WindowBuildError; +use sdl2::IntegerOrSdlError; use std::sync::mpsc::SendError; use std::sync::PoisonError; -use sdl2::IntegerOrSdlError; -use sdl2::video::WindowBuildError; -use crate::device::keyboard::KeyboardEvent; +use std::time::Duration; pub type EmulatorResult = Result; +/// CHIP-8 Device configuration +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct DeviceConfig { + is_new_chip8: bool, + halt_on_invalid: bool, + /// None if disabled, target instruction time otherwise + throttling_time: Option, +} + +impl DeviceConfig { + pub fn new( + is_new_chip8: bool, + halt_on_invalid: bool, + do_instruction_throttling: bool, + ips_throttling_rate: u64, + ) -> DeviceConfig { + DeviceConfig { + is_new_chip8, + halt_on_invalid, + throttling_time: if do_instruction_throttling { + Some(Duration::from_micros(1_000_000 / ips_throttling_rate)) + } else { + None + }, + } + } + pub fn is_new_chip8(&self) -> bool { + self.is_new_chip8 + } + pub fn should_halt_on_invalid(&self) -> bool { + self.halt_on_invalid + } + pub fn get_throttling_config(&self) -> Option { + self.throttling_time + } +} #[derive(Clone, Debug)] pub enum EmulatorError { SdlError(String), IOError(String), - MutexInvalidState(String) + MutexInvalidState(String), } impl From for EmulatorError { @@ -29,24 +66,50 @@ impl From for EmulatorError { impl From for EmulatorError { fn from(value: IntegerOrSdlError) -> Self { match value { - IntegerOrSdlError::IntegerOverflows(x, y) => { Self::SdlError(format!("{} - {}", x, y)) } - IntegerOrSdlError::SdlError(str) => { Self::SdlError(str) } + IntegerOrSdlError::IntegerOverflows(x, y) => Self::SdlError(format!("{} - {}", x, y)), + IntegerOrSdlError::SdlError(str) => Self::SdlError(str), } } } -impl From for EmulatorError{ +impl From for EmulatorError { fn from(value: std::io::Error) -> Self { Self::IOError(value.to_string()) } } -impl From> for EmulatorError{ +impl From> for EmulatorError { fn from(value: PoisonError) -> Self { Self::MutexInvalidState(value.to_string()) } } -impl From> for EmulatorError{ +impl From> for EmulatorError { fn from(value: SendError) -> Self { - Self::IOError(format!("Failed to communicate keyboard event to main thread: {}",value)) + Self::IOError(format!( + "Failed to communicate keyboard event to main thread: {}", + value + )) + } +} + +#[cfg(test)] +mod tests{ + use std::time::Duration; + + use super::DeviceConfig; + + #[test] + fn test_device_config_all_false(){ + let device_config = DeviceConfig::new(false, false, false, 800); + assert_eq!(false,device_config.is_new_chip8()); + assert_eq!(false,device_config.should_halt_on_invalid()); + assert!(device_config.get_throttling_config().is_none()); + } + #[test] + fn test_device_config_throttling_enabled_and_new_chip8(){ + let device_config = DeviceConfig::new(true, false, true, 100); + const EXPECTED_INSTRUCTION_TIME_MS:u64 = 10; + assert_eq!(true,device_config.is_new_chip8()); + assert_eq!(false,device_config.should_halt_on_invalid()); + assert_eq!(Some(Duration::from_millis(EXPECTED_INSTRUCTION_TIME_MS)),device_config.get_throttling_config()); } } \ No newline at end of file