commit e8417279bb860f38ad1d07782464ab1fa3ded367 Author: Jake Walker Date: Wed Apr 19 17:41:24 2023 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3baae79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# Created by https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=rust,visualstudiocode,macos + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/rust,visualstudiocode,macos +roms +.fleet \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..db113ad --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "chip8" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pixels = "0.12.1" +pretty-hex = "0.3.0" +rand = "0.8.5" +winit = "0.28.3" +winit_input_helper = "0.14.1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3da1ba --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# CHIP-8 Emulator + +A kinda janky CHIP-8 emulator I've written using Rust. This should theoretically be cross platform with the libraries I have used. \ No newline at end of file diff --git a/src/chip8/chip8.rs b/src/chip8/chip8.rs new file mode 100644 index 0000000..ccf6d73 --- /dev/null +++ b/src/chip8/chip8.rs @@ -0,0 +1,349 @@ +use pretty_hex::*; +use std::time::{Instant, Duration}; + +const FONT_OFFSET: u16 = 0x050; + +struct Options { + modern_shift: bool, + modern_jump_with_offset: bool, + modern_store_and_load: bool, + modern_add_to_index: bool +} + +pub struct Emulator<'a> { + // 4k RAM + memory: [u8; 4096], + + // 16-bit program counter + pc: u16, + + // 16-bit index register + i: u16, + + // stack of 16-bit addresses + stack: Vec, + + // 8-bit delay timer + delay_timer: u8, + + // 8-bit sound timer + sound_timer: u8, + + // 16 x 8-bit variable registers V0-VF + vr: [u8; 16], + + cpu_speed: Duration, + timer_speed: Duration, + last_cpu_cycle: Instant, + last_timer_decrement: Instant, + options: Options, + peripherals: &'a dyn Peripherals, +} + +impl<'a> Emulator<'a> { + pub fn new<'b>(peripherals: &'b dyn Peripherals, cpu_clock_speed: u32) -> Emulator<'b> { + let cpu_duration = Duration::from_secs(1) / cpu_clock_speed; + let timer_duration = Duration::from_secs(1) / 60; + + let mut emulator = Emulator { + memory: [0; 4096], + pc: 0x200, + i: 0, + stack: Vec::new(), + delay_timer: 0, + sound_timer: 0, + vr: [0; 16], + cpu_speed: cpu_duration, + timer_speed: timer_duration, + last_cpu_cycle: Instant::now(), + last_timer_decrement: Instant::now(), + options: Options { modern_shift: true, modern_jump_with_offset: false, modern_store_and_load: true, modern_add_to_index: true }, + peripherals + }; + emulator.load_memory(super::font::FONT.to_vec(), FONT_OFFSET); + + emulator + } + + pub fn load_memory(&mut self, input: Vec, offset: u16) { + for i in 0..input.len() { + self.memory[offset as usize + i] = *input.get(i).expect("Input should have address"); + } + } + + pub fn cpu_cycle_due(&self) -> bool { + let delta = Instant::now() - self.last_cpu_cycle; + delta >= self.cpu_speed + } + + pub fn timer_decrement_due(&self) -> bool { + let delta = Instant::now() - self.last_timer_decrement; + delta >= self.timer_speed + } + + pub fn decrement_timers(&mut self) { + if let Some(new_delay_timer) = self.delay_timer.checked_sub(1) { + self.delay_timer = new_delay_timer; + } + if let Some(new_sound_timer) = self.sound_timer.checked_sub(1) { + self.sound_timer = new_sound_timer; + } + self.last_timer_decrement = Instant::now(); + } + + pub fn cycle(&mut self, screen: &mut [u8], keys: [bool; 16]) { + // fetch + let instruction: u16 = { + let instruction1 = self + .memory + .get(self.pc as usize) + .expect("Should be able to read memory instruction1"); + let instruction2 = self + .memory + .get((self.pc + 1) as usize) + .expect("Should be able to read memory instruction2"); + ((*instruction1 as u16) << 8) | *instruction2 as u16 + }; + + // increment program counter + self.pc += 2; + + // decode & execute + let o: u8 = (instruction >> 12) as u8; + let x: u8 = ((instruction & 0x0F00) >> 8) as u8; // u4 + let y: u8 = ((instruction & 0x00F0) >> 4) as u8; // u4 + let n: u8 = (instruction & 0x000F) as u8; // u4 + let nn: u8 = (instruction & 0x00FF) as u8; // u8 + let nnn: u16 = instruction & 0x0FFF; // u12 + + let vx = self.vr[x as usize]; + let vy = self.vr[y as usize]; + +// println!("Execute: 0x{:X}", instruction); + + match (o, x, y, n) { + // 00E0 - clear display + (0x0, 0x0, 0xE, 0x0) => self.peripherals.clear_display(screen), + // 00EE - subroutine + (0x0, 0x0, 0xE, 0xE) => { + self.pc = self.stack.pop().expect("Should have address on stack to return from"); + }, + // 0NNN - execute machine language routine + (0x0, _, _, _) => (), + // 1NNN - jump + (0x1, _, _, _) => self.pc = nnn, + // 2NNN - subroutine + (0x2, _, _, _) => { + // put pc in stack + self.stack.push(self.pc); + // set pc to address + self.pc = nnn; + }, + // 3XNN - skip if vx == nn + (0x3, _, _, _) => { + if vx == nn { + self.pc += 2; + } + }, + // 4XNN - skip if vx != nn + (0x4, _, _, _) => { + if vx != nn { + self.pc += 2; + } + }, + // 5XY0 - skip if vx == vy + (0x5, _, _, 0x0) => { + if vx == vy { + self.pc += 2; + } + }, + // 6XNN - set register vx + (0x6, _, _, _) => self.vr[x as usize] = nn, + // 7XNN - add value to register vx + (0x7, _, _, _) => self.vr[x as usize] = vx.wrapping_add(nn), + // 8XY0 - set (vx = vy) + (0x8, _, _, 0x0) => self.vr[x as usize] = vy, + // 8XY1 - or (vx or vy) + (0x8, _, _, 0x1) => self.vr[x as usize] = vx | vy, + // 8XY2 - and (vx and vy) + (0x8, _, _, 0x2) => self.vr[x as usize] = vx & vy, + // 8XY3 - xor (vx xor vy) + (0x8, _, _, 0x3) => self.vr[x as usize] = vx ^ vy, + // 8XY4 - add (vx + vy) + (0x8, _, _, 0x4) => { + let (value, of) = vx.overflowing_add(vy); + self.vr[x as usize] = value; + self.vr[0xF] = if of { 1 } else { 0 }; + }, + // 8XY5 - subtract (vx - vy) + (0x8, _, _, 0x5) => { + let (value, uf) = vx.overflowing_sub(vy); + self.vr[x as usize] = value; + if vx > vy { + self.vr[0xF] = 1; + } else if vy > vx && uf { + self.vr[0xF] = 0; + } + }, + // 8XY7 - subtract (vy - vx) + (0x8, _, _, 0x7) => { + let (value, uf) = vy.overflowing_sub(vx); + self.vr[x as usize] = value; + if vy > vx { + self.vr[0xF] = 1; + } else if vx > vy && uf { + self.vr[0xF] = 0; + } + }, + // 8XY6 - shift right + (0x8, _, _, 0x6) => { + if !self.options.modern_shift { + self.vr[x as usize] = vy; + } + let vf = vx & 1; + self.vr[x as usize] = vx >> 1; + self.vr[0xF] = vf; + }, + // 8XYE - shift left + (0x8, _, _, 0xE) => { + if !self.options.modern_shift { + self.vr[x as usize] = vy; + } + let vf = vx >> 7; + self.vr[x as usize] = vx << 1; + self.vr[0xF] = vf; + }, + // 9XY0 - skip if vx != vy + (0x9, _, _, 0) => { + if vx != vy { + self.pc += 2; + } + } + // ANNN - set i register + (0xA, _, _, _) => self.i = nnn, + // BNNN - jump with offset + (0xB, _, _, _) => { + let offset = { + if self.options.modern_jump_with_offset { + vx + } else { + self.vr[0] + } + }; + self.pc = nnn + offset as u16; + }, + // CXNN - random + (0xC, _, _, _) => { + let random: u8 = rand::random(); + self.vr[x as usize] = random & nn; + }, + // DXYN - display + (0xD, _, _, _) => { + let x_coordinate: usize = (vx % (self.peripherals.display_width() as u8)).into(); + let y_coordinate: usize = (vy % (self.peripherals.display_height() as u8)).into(); + + // set vf to 0 + self.vr[0xF] = 0; + + for height in 0..(n as usize) { + if y_coordinate + height >= self.peripherals.display_height() { + break; + } + + let sprite_line = self.memory[self.i as usize + height]; + + for i in 0..8 { + if x_coordinate + i >= self.peripherals.display_width() { + break; + } + + let state: bool = (sprite_line >> (7-i)) % 2 == 1; + if state { + let turned_off = self.peripherals.draw_pixel(screen, x_coordinate + i, y_coordinate + height); + if turned_off { + self.vr[0xF] = 1; + } + } + } + } + }, + // EX9E - skip if key + (0xE, _, 0x9, 0xE) => { + if keys[vx as usize] { + self.pc += 2; + } + }, + // EXA1 - skip if key + (0xE, _, 0xA, 0x1) => { + if !keys[vx as usize] { + self.pc += 2; + } + }, + // FX07 - timers (set vx to delay timer) + (0xF, _, 0x0, 0x7) => self.vr[x as usize] = self.delay_timer, + // FX15 - timers (set delay timer to vx) + (0xF, _, 0x1, 0x5) => self.delay_timer = vx, + // FX18 - timers (set sound timer to vx) + (0xF, _, 0x1, 0x8) => self.sound_timer = vx, + // FX1E - add to index + (0xF, _, 0x1, 0xE) => { + self.i += vx as u16; + if self.options.modern_add_to_index && self.i > 0x1000 { + self.vr[0xF] = 1; + } + }, + // FX0A - get key + (0xF, _, 0x0, 0xA) => { + if !keys.iter().any(|k| *k) { + self.pc -= 2; + } + }, + // FX29 - font character + (0xF, _, 0x2, 0x9) => self.i = FONT_OFFSET + ((vx & 0xF) as u16 * 20), + // FX33 - binary-coded decimal conversion + (0xF, _, 0x3, 0x3) => { + let digits: Vec<_> = vx.to_string().chars().map(|d| d.to_digit(10).unwrap() as u8).collect(); + self.memory[self.i as usize] = digits.get(0).unwrap_or(&0).to_owned(); + self.memory[self.i as usize + 1] = digits.get(1).unwrap_or(&0).to_owned(); + self.memory[self.i as usize + 2] = digits.get(2).unwrap_or(&0).to_owned(); + }, + // FX55 - store memory + (0xF, _, 0x5, 0x5) => { + for i in 0..=x as usize { + self.memory[self.i as usize + i] = self.vr[i]; + } + if !self.options.modern_store_and_load { + self.i += x as u16; + } + }, + // FX65 - load memory + (0xF, _, 0x6, 0x5) => { + for i in 0..=x as usize { + self.vr[i] = self.memory[self.i as usize + i]; + } + if !self.options.modern_store_and_load { + self.i += x as u16; + } + }, + _ => panic!("Unimplemented instruction: 0x{:X}", instruction) + } + + self.last_cpu_cycle = Instant::now(); + } +} + +impl std::fmt::Display for Emulator<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let registers: String = self.vr.iter().enumerate().map(|(i, r)| format!("V{:X}: 0x{:x}", i, r)).collect::>().join(", "); + let stack: String = self.stack.iter().map(|a| format!("0x{:x}", a)).collect::>().join(", "); + let dump = pretty_hex(&self.memory); + write!(f, "PC: 0x{:x}, I: 0x{:x}, DT: 0x{:x}, ST: 0x{:x}\n{}\nStack: {}\n\n{}", self.pc, self.i, self.delay_timer, self.sound_timer, registers, stack, dump) + } +} + +pub trait Peripherals { + fn display_width(&self) -> usize; + fn display_height(&self) -> usize; + fn draw_pixel(&self, screen: &mut [u8], x: usize, y: usize) -> bool; + fn clear_display(&self, screen: &mut[u8]); +} diff --git a/src/chip8/font.rs b/src/chip8/font.rs new file mode 100644 index 0000000..be0b016 --- /dev/null +++ b/src/chip8/font.rs @@ -0,0 +1,18 @@ +pub const FONT: [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 +]; \ No newline at end of file diff --git a/src/chip8/mod.rs b/src/chip8/mod.rs new file mode 100644 index 0000000..8c50848 --- /dev/null +++ b/src/chip8/mod.rs @@ -0,0 +1,2 @@ +pub mod chip8; +mod font; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..20f5881 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,143 @@ +mod chip8; + +use std::error::Error; +use pixels::{Pixels, SurfaceTexture}; +use winit::{ + dpi::LogicalSize, + event::{Event, VirtualKeyCode}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use winit_input_helper::WinitInputHelper; +use chip8::chip8::{Emulator, Peripherals}; +use std::fs; +use std::env; + +const WIDTH: u32 = 64; +const HEIGHT: u32 = 32; +const DEFAULT_SCALE: f64 = 6.0; +const COLOR_WHITE: [u8; 3] = [0xFF, 0xFF, 0xFF]; +const COLOR_BLACK: [u8; 3] = [0x00, 0x00, 0x00]; +const KEY_MAP: [VirtualKeyCode; 16] = [ + VirtualKeyCode::X, VirtualKeyCode::Key1, VirtualKeyCode::Key2, VirtualKeyCode::Key3, + VirtualKeyCode::Q, VirtualKeyCode::W, VirtualKeyCode::E, VirtualKeyCode::A, + VirtualKeyCode::S, VirtualKeyCode::D, VirtualKeyCode::Z, VirtualKeyCode::C, + VirtualKeyCode::Key4, VirtualKeyCode::R, VirtualKeyCode::F, VirtualKeyCode::V +]; + +struct EmulatorPeripherals { + width: usize, + height: usize +} + +impl EmulatorPeripherals { + fn set_pixel(&self, screen: &mut [u8], x: usize, y: usize, state: bool) { + let i = ((y * self.width) + x) * 4; + let color = { + match state { + true => COLOR_WHITE, + false => COLOR_BLACK + } + }; + screen[i] = color[0]; + screen[i+1] = color[1]; + screen[i+2] = color[2]; + screen[i+3] = 0xFF; + } + + fn get_pixel(&self, screen: &[u8], x: usize, y: usize) -> bool { + let i = ((y * self.width) + x) * 4; + return screen[i] == 255; + } +} + +impl Peripherals for EmulatorPeripherals { + fn display_width(&self) -> usize { + self.width + } + + fn display_height(&self) -> usize { + self.height + } + + fn draw_pixel(&self, screen: &mut [u8], x: usize, y: usize) -> bool { + let current = self.get_pixel(screen, x, y); + self.set_pixel(screen, x, y, !current); + current + } + + fn clear_display(&self, screen: &mut [u8]) { + for y in 0..self.height { + for x in 0..self.width { + self.set_pixel(screen, x, y, false); + } + } + } +} + +fn main() -> Result<(), Box> { + let event_loop = EventLoop::new(); + let mut input = WinitInputHelper::new(); + + let window = { + let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64); + let scaled_size = LogicalSize::new(WIDTH as f64 * DEFAULT_SCALE, HEIGHT as f64 * DEFAULT_SCALE); + WindowBuilder::new() + .with_title("CHIP-8") + .with_inner_size(scaled_size) + .with_min_inner_size(size) + .build(&event_loop)? + }; + + let mut pixels = { + let window_size = window.inner_size(); + let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window); + Pixels::new(WIDTH, HEIGHT, surface_texture)? + }; + + let mut emulator = Emulator::new(&EmulatorPeripherals{width: WIDTH as usize, height: HEIGHT as usize}, 600); + + let args: Vec = env::args().collect(); + + let rom = fs::read(args.get(1).expect("ROM path should be passed as argument")).expect("Should be able to read ROM"); + emulator.load_memory(rom, 0x200); + + let mut keys = [false; 16]; + + event_loop.run(move |event, _, control_flow| { + if emulator.cpu_cycle_due() { + emulator.cycle(pixels.frame_mut(), keys); + } + + if emulator.timer_decrement_due() { + emulator.decrement_timers(); + } + + if let Event::RedrawRequested(_) = event { + pixels.render().expect("Should be able to render"); + } + + if input.update(&event) { + if input.key_pressed(VirtualKeyCode::Escape) || input.close_requested() || input.destroyed() { + *control_flow = ControlFlow::Exit; + return; + } + + keys = [false; 16]; + for (i, key) in KEY_MAP.iter().enumerate() { + if input.key_held(*key) { + keys[i] = true; + } + } + + if let Some(size) = input.window_resized() { + if let Err(err) = pixels.resize_surface(size.width, size.height) { + *control_flow = ControlFlow::Exit; + return; + } + } + + window.request_redraw(); + } + }); +}