use crate::display::Display; use crate::display::{DISPLAY_HEIGHT, DISPLAY_WIDTH}; use crate::input::InputModule; use crate::instruction::{Instruction, ProcessorInstruction}; use crate::sound::SoundModule; use crate::stack::Stack; use anyhow::anyhow; use log::{debug, info, trace, warn}; use rand::Rng; use std::fs::File; use std::io::Read; use std::path::Path; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; use std::thread; use std::thread::sleep; use std::time::{Duration, Instant}; const MEMORY_SIZE: usize = 4096; const NUMBER_OF_REGISTERS: usize = 16; const FONT_SPRITES: [u8; 80] = [ 0xF0, 0x90, 0x90, 0x90, 0xF0, // 0 0x20, 0x60, 0x20, 0x20, 0x70, // 1 0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2 0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3 0x90, 0x90, 0xF0, 0x10, 0x10, // 4 0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5 0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6 0xF0, 0x10, 0x20, 0x40, 0x40, // 7 0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8 0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9 0xF0, 0x90, 0xF0, 0x90, 0x90, // A 0xE0, 0x90, 0xE0, 0x90, 0xE0, // B 0xF0, 0x80, 0x80, 0x80, 0xF0, // C 0xE0, 0x90, 0x90, 0x90, 0xE0, // D 0xF0, 0x80, 0xF0, 0x80, 0xF0, // E 0xF0, 0x80, 0xF0, 0x80, 0x80, // F ]; /// Emulator emulates the Chip8 CPU. pub struct Emulator where D: Display, S: SoundModule, I: InputModule, { /// Memory represents the emulator's memory. memory: [u8; MEMORY_SIZE], /// Registers holds the general purpose registers. registers: [u8; NUMBER_OF_REGISTERS], /// The index register store memory addresses. index_register: u16, /// The program counter register tracks the currently executing instruction. program_counter: u16, /// The delay timer register. It is decremented at a rate of 60 Hz until it reaches 0. delay_timer: u8, /// The sound timer register. It is decremented at a rate of 60 Hz until it reaches 0. /// It plays a beeping sound when it's value is different from 0. sound_timer: u8, /// The stack pointer register. stack_pointer: u8, /// The display_data holds all the data associated with the display display: D, /// The sound module for making sounds. sound_module: S, /// The module responsible for receiving user input. input_module: I, /// The stack of the emulator. stack: Stack, /// Holds the display data, each bit corresponds to a pixel. display_data: [bool; DISPLAY_WIDTH * DISPLAY_HEIGHT], /// Tracks the last key pressed by the user. last_key_pressed: Option, } impl Emulator where D: Display + 'static, S: SoundModule + 'static, I: InputModule + Clone + Send + 'static, { /// Creates a new `Emulator` instance. /// pub fn new(display: D, sound_module: S, input_module: I) -> Emulator { let mut emulator = Emulator { memory: [0; MEMORY_SIZE], registers: [0; NUMBER_OF_REGISTERS], index_register: 0, program_counter: 0, delay_timer: 0, sound_timer: 0, stack_pointer: 0, stack: Stack::new(), display_data: [false; DISPLAY_WIDTH * DISPLAY_HEIGHT], display, sound_module, input_module, last_key_pressed: None, }; emulator.load_font_data(); emulator } fn load_font_data(&mut self) { info!("Loading font data..."); FONT_SPRITES .iter() .enumerate() .for_each(|i| self.memory[0xf0 + i.0] = *i.1); info!("Loaded font data into memory at 0xf0."); } /// Emulates the ROM specified at `path`. pub fn emulate(&mut self, path: T) -> Result<(), anyhow::Error> where T: AsRef + std::fmt::Display, { self.load_rom(path)?; self.emulation_loop::()?; Ok(()) } /// Emulation loop executes the fetch -> decode -> execute pipeline fn emulation_loop(&mut self) -> Result<(), anyhow::Error> { let mut tick_timer = Instant::now(); let target_fps: u128 = 60; let (tx, rx): (Sender, Receiver) = mpsc::channel(); let mut input_module_clone = self.input_module.clone(); thread::spawn(move || loop { let key = input_module_clone.get_key_pressed(); if let Some(some_key) = key { let _ = tx.send(some_key); } }); // clear display self.display.clear(); loop { let now = Instant::now(); let elapsed_time = now.duration_since(tick_timer); let elapsed_ms = elapsed_time.as_millis(); if elapsed_ms >= (1000 / target_fps) { self.handle_input(&rx); // Handle sound and delay timer. self.handle_timers(); for _ in 0..=7 { // fetch instruction & decode it let instruction = self.fetch_instruction()?; self.program_counter += 2; // execute self.execute_instruction(instruction)?; } tick_timer = Instant::now(); } else { sleep(Duration::from_millis(1)); } } } /// Handles the timers logic. fn handle_timers(&mut self) { // Handle timers if self.delay_timer > 0 { self.delay_timer -= 1 } if self.delay_timer > 0 { self.delay_timer -= 1 } else { self.do_beep() } } /// Handle the input fn handle_input(&mut self, receiver: &Receiver) { let received_input = receiver.try_recv(); if let Ok(key_pressed) = received_input { if key_pressed == 0xFF { // Exit requested self.display.clear(); println!("Thank you for playing! See you next time! :-)"); std::process::exit(0); } else { self.last_key_pressed = Option::from((key_pressed & 0xF) as u8) } } else { self.last_key_pressed = None; } } /// Should make an audible beep. fn do_beep(&mut self) { self.sound_module.beep(); } /// Executes the instruction fn execute_instruction(&mut self, instruction: Instruction) -> Result<(), anyhow::Error> { match instruction.processor_instruction() { ProcessorInstruction::ClearScreen => { trace!("Clear display"); self.display_data = [false; DISPLAY_WIDTH * DISPLAY_HEIGHT]; self.display.clear() } ProcessorInstruction::Jump { address } => { trace!("Jump to address {:04x}", address); self.program_counter = address } ProcessorInstruction::SetRegister { register, data } => { trace!("Set register {} to data {:04x}", register, data); self.registers[register as usize] = data } ProcessorInstruction::AddValueToRegister { register, value } => { trace!("Add to register {} data {:04x}", register, value); let (result, _) = self.registers[register as usize].overflowing_add(value); self.registers[register as usize] = result; } ProcessorInstruction::SetIndexRegister { data } => { trace!("Set index register to data {:04x}", data); self.index_register = data; } ProcessorInstruction::Draw { vx, vy, rows } => { trace!("Draw vx_register={vx} vy_register={vy} pixels={rows}"); let x_coordinate = self.registers[vx as usize]; let y_coordinate = self.registers[vy as usize]; // Keep track if any pixels were flipped let mut flipped = false; // Iterate over each row of our sprite for y_line in 0..rows { // Determine which memory address our row's data is stored let addr = self.index_register + y_line as u16; let pixels = self.memory[addr as usize]; // Iterate over each column in our row for x_line in 0..8 { // Use a mask to fetch current pixel's bit. Only flip if a 1 if (pixels & (0b1000_0000 >> x_line)) != 0 { // Sprites should wrap around screen, so apply modulo let x = (x_coordinate + x_line) as usize % DISPLAY_WIDTH; let y = (y_coordinate + y_line) as usize % DISPLAY_HEIGHT; // Get our pixel's index for our 1D screen array let index = x + DISPLAY_WIDTH * y; // Check if we're about to flip the pixel and set flipped |= self.display_data[index]; self.display_data[index] ^= true; } } } if flipped { self.registers[0xF] = 1; } else { self.registers[0xF] = 0; } self.display.render(&self.display_data); } ProcessorInstruction::Return => { let value = self.stack.pop().unwrap(); trace!("Return to {value:04x}"); self.program_counter = value; } ProcessorInstruction::Call { address } => { trace!("Call {address:04x}"); // Save PC to the stack self.stack.push(self.program_counter); // Set PC to subroutine address self.program_counter = address; } ProcessorInstruction::Set { vx, vy } => { trace!("Set VX={vx:04x} VY={vy:04x}"); self.registers[vx as usize] = self.registers[vy as usize]; } ProcessorInstruction::BinaryOr { vx, vy } => { trace!("BinaryOr VX={vx:04x} VY={vy:04x}"); self.registers[vx as usize] |= self.registers[vy as usize] } ProcessorInstruction::BinaryAnd { vx, vy } => { trace!("BinaryAnd VX={vx:04x} VY={vy:04x}"); self.registers[vx as usize] &= self.registers[vy as usize] } ProcessorInstruction::BinaryXor { vx, vy } => { trace!("BinaryXor VX={vx:04x} VY={vy:04x}"); self.registers[vx as usize] ^= self.registers[vy as usize] } ProcessorInstruction::Add { vx, vy } => { trace!("Add VX={vx:04x} VY={vy:04x}"); let (result, overflow) = self.registers[vx as usize].overflowing_add(self.registers[vy as usize]); self.registers[vx as usize] = result; if overflow { self.registers[0xF] = 1; } else { self.registers[0xF] = 0; } } ProcessorInstruction::SubtractVX { vx, vy } => { trace!("SubtractVX VX={vx:04x} VY={vy:04x}"); if self.registers[vx as usize] > self.registers[vy as usize] { self.registers[0xF] = 1 } else { // The register 0xF will be 0 if there's an underflow. self.registers[0xF] = 0 } let (result, _) = self.registers[vx as usize].overflowing_sub(self.registers[vy as usize]); self.registers[vx as usize] = result; } ProcessorInstruction::SubtractVY { vx, vy } => { trace!("SubtractVY VX={vx:04x} VY={vy:04x}"); if self.registers[vy as usize] > self.registers[vx as usize] { self.registers[0xF] = 1 } else { // The register 0xF will be 0 if there's an underflow. self.registers[0xF] = 0 } let (result, _) = self.registers[vy as usize].overflowing_sub(self.registers[vx as usize]); self.registers[vx as usize] = result; } ProcessorInstruction::ShiftLeft { vx, vy } => { trace!("ShiftLeft VX={vx:04x} VY={vy:04x}"); self.registers[0xF] = (self.registers[vx as usize] >> 7) & 1; self.registers[vx as usize] <<= 1; } ProcessorInstruction::ShiftRight { vx, vy } => { trace!("ShiftRight VX={vx:04x} VY={vy:04x}"); self.registers[0xF] = self.registers[vx as usize] & 0x1; self.registers[vx as usize] >>= 1; } ProcessorInstruction::JumpWithOffset(address) => { let offset = self.registers[0x0]; trace!("Jump With offset Address={address:04x} Offset={offset:04x}"); self.program_counter = address + offset as u16 } ProcessorInstruction::GenerateRandomNumber(register, data) => { trace!("Generate random number"); self.registers[register as usize] = rand::thread_rng().gen_range(0x00..0xFF) & data } ProcessorInstruction::SkipEqualVXData(vx, data) => { trace!("SkipEqualVXData"); let vx_data = self.registers[vx as usize]; if vx_data == data { self.program_counter += 2 } } ProcessorInstruction::SkipNotEqualVXData(vx, data) => { trace!("SkipNotEqualVXData"); let vx_data = self.registers[vx as usize]; if vx_data != data { self.program_counter += 2 } } ProcessorInstruction::SkipEqualVXVY(vx, vy) => { trace!("SkipNotEqualVXData"); let vx_data = self.registers[vx as usize]; let vy_data = self.registers[vy as usize]; if vx_data == vy_data { self.program_counter += 2 } } ProcessorInstruction::SkipNotEqualVXVY(vx, vy) => { trace!("SkipNotEqualVXVY"); let vx_data = self.registers[vx as usize]; let vy_data = self.registers[vy as usize]; if vx_data != vy_data { self.program_counter += 2 } } ProcessorInstruction::SetVXToDelayTimer(vx) => { trace!("SetVXToDelayTimer"); self.registers[vx as usize] = self.delay_timer } ProcessorInstruction::SetDelayTimer(vx) => { trace!("SetDelayTimer"); self.delay_timer = self.registers[vx as usize] } ProcessorInstruction::SetSoundTimer(vx) => { trace!("SetSoundTimer"); self.sound_timer = self.registers[vx as usize] } ProcessorInstruction::AddToIndex(vx) => { trace!("AddToIndex"); let (result, overflow) = self .index_register .overflowing_add(self.registers[vx as usize] as u16); self.index_register = result; if overflow { self.registers[0xF] = 1 } else { self.registers[0xF] = 0 } } ProcessorInstruction::FontCharacter(vx) => { self.index_register = 0xF0 + (self.registers[vx as usize] as u16 & 0xF) * 5u16; } ProcessorInstruction::BinaryCodedDecimalConversion(vx) => { let number = self.registers[vx as usize]; self.memory[self.index_register as usize] = number / 100; self.memory[self.index_register as usize + 1] = (number / 10) % 10; self.memory[self.index_register as usize + 2] = ((number) % 100) % 10; } ProcessorInstruction::LoadMemory(vx) => { for i in 0..=vx { let memory_index = (self.index_register + (i as u16)) as usize; self.registers[i as usize] = self.memory[memory_index]; } } ProcessorInstruction::StoreMemory(vx) => { for i in 0..=vx { let memory_index = (self.index_register + (i as u16)) as usize; self.memory[memory_index] = self.registers[i as usize]; } } ProcessorInstruction::GetKeyBlocking(vx) => { if let Some(key) = self.last_key_pressed { self.registers[vx as usize] = key; } else { self.program_counter -= 2; } } ProcessorInstruction::SkipIfKeyIsPressed(vx) => { if let Some(key) = self.last_key_pressed { if self.registers[vx as usize] == key { self.program_counter += 2; } } } ProcessorInstruction::SkipIfKeyIsNotPressed(vx) => { if let Some(key) = self.last_key_pressed { if self.registers[vx as usize] != key { self.program_counter += 2; } } else { self.program_counter += 2; } } _ => { warn!("Unknown instruction: {:04x}, skipping.", instruction); } } Ok(()) } /// Fetches the current instruction from the memory without incrementing the program counter. fn fetch_instruction(&self) -> Result { if self.program_counter as usize >= self.memory.len() { return Err(anyhow!("program_counter is out of range")); } Ok(Instruction::new([ self.memory[self.program_counter as usize], self.memory[self.program_counter as usize + 1], ])) } /// Loads the ROM found at the rom path in the emulator's RAM memory. fn load_rom(&mut self, rom_file: T) -> Result<(), anyhow::Error> where T: AsRef + std::fmt::Display, { let mut file = File::open(&rom_file)?; // Check ROM length if it overflows max RAM size. let rom_size = file.metadata()?.len(); debug!("Open ROM {} of size {} bytes.", &rom_file, rom_size); if rom_size > MEMORY_SIZE as u64 - 0x200 { return Err(anyhow!( "ROM at {} overflows emulator's RAM size of 4kB.", &rom_file )); } file.read(&mut self.memory[0x200..])?; // Set program counter to start of memory self.program_counter = 0x200; Ok(()) } } #[cfg(test)] mod tests { use super::*; use crate::display::TerminalDisplay; use crate::input::NoInput; use crate::sound::TerminalSound; use pretty_assertions::assert_eq; #[test] fn test_load_font_data() { let emulator = Emulator::new(TerminalDisplay::new(), TerminalSound, NoInput); assert_eq!(emulator.memory[0xf0..0xf0 + 80], FONT_SPRITES) } #[test] fn test_load_rom_ibm_logo() { // Setup let mut file = File::open("roms/ibm-logo.ch8").expect("Failed to test open ROM"); let mut rom_file_data: [u8; 132] = [0; 132]; file.read(&mut rom_file_data) .expect("Failed to read test ROM"); // Test let mut emulator = Emulator::new(TerminalDisplay::new(), TerminalSound, NoInput); emulator .load_rom("roms/ibm-logo.ch8") .expect("failed to load ROM"); // Assert assert_eq!(emulator.memory[0x200..0x200 + 132], rom_file_data) } }