diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..067d31c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +*.ch8 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..edf01c5 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,133 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + +[[package]] +name = "crunch" +version = "0.1.0" +dependencies = [ + "rand", + "sdl2", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "sdl2" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" +dependencies = [ + "bitflags", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.35.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" +dependencies = [ + "cfg-if", + "cmake", + "libc", + "version-compare", +] + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cbbf823 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "crunch" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.5" +sdl2 = { version = "0.35.2", features = ["bundled"] } + +[lib] +name = "crunch" +path = "src/lib.rs" + +[[bin]] +name = "crunch" +path = "src/main.rs" diff --git a/README.md b/README.md new file mode 100644 index 0000000..2fd7b03 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Crunch + +A Rust-based emulator for CHIP-8 + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ddbdd47 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,351 @@ +const SCREEN_WIDTH: u8 = 64; +const SCREEN_HEIGHT: u8 = 32; + +pub struct Chip8 { + cpu: CPU, + display: Display, + memory: Memory, +} + +const ROM_START: usize = 512; + +impl Chip8 { + pub fn new(rom: &[u8]) -> Chip8 { + let mut memory = Memory::new(); + memory.0[ROM_START..(ROM_START + rom.len())].copy_from_slice(rom); + Chip8 { + cpu: CPU { + delay_timer: 0, + sound_timer: 0, + stack: Vec::new(), + program_counter: ROM_START as u16, + index_register: 0, + registers: [0; 16], + }, + display: Display::new(), + memory, + } + } + + pub fn advance(&mut self) { + let Chip8 { + cpu, + display, + memory, + } = self; + advance(cpu, display, memory); + } + + pub fn display(&self) -> &Display { + &self.display + } +} + +pub fn advance(cpu: &mut CPU, display: &mut Display, memory: &mut Memory) { + let instr = [ + memory.get(cpu.program_counter), + memory.get(cpu.program_counter + 1), + ]; + let instr = ((instr[0] as u16) << 8) | instr[1] as u16; + cpu.program_counter += 2; + + if cpu.delay_timer > 0 { + cpu.delay_timer -= 1; + } + if cpu.sound_timer > 0 { + cpu.sound_timer -= 1; + } + + // Match on first nibble + match instr >> 12 { + 0 => match instr & 0x00FF { + 0xE0 => { + display.clear(); + } + 0xEE => { + // return + cpu.program_counter = cpu.stack.pop().expect("Tried to return but stack empty"); + } + _ => panic!("instruction unknown: {}", instr), + }, + 1 => { + let target = instr & 0x0FFF; + cpu.program_counter = target; + } + 2 => { + // "subroutine" + let target = instr & 0x0FFF; + cpu.stack.push(cpu.program_counter); + cpu.program_counter = target; + } + 3 => { + // skip if val equal + let register_value = cpu.register((instr & 0x0F00) >> 8); + let value = (instr & 0x00FF) as u8; + if register_value == value { + cpu.program_counter += 2; + } + } + 4 => { + // skip if val not equal + let register_value = cpu.register((instr & 0x0F00) >> 8); + let value = (instr & 0x00FF) as u8; + if register_value != value { + cpu.program_counter += 2; + } + } + 5 => { + // skip if register equal + let a = cpu.register((instr & 0x0F00) >> 8); + let b = cpu.register((instr & 0x00F0) >> 4); + if a == b { + cpu.program_counter += 2; + } + } + 9 => { + // skip if register not equal + let a = cpu.register((instr & 0x0F00) >> 8); + let b = cpu.register((instr & 0x00F0) >> 4); + if a != b { + cpu.program_counter += 2; + } + } + 6 => { + cpu.set_register((instr & 0x0F00) >> 8, (instr & 0x00FF) as u8); + } + 7 => { + // add to register + let index = (instr & 0x0F00) >> 8; + let register_value = cpu.register(index); + let constant = instr & 0x00FF; + let sum = (register_value as u16) + constant; + cpu.set_register(index, sum as u8); + } + 8 => { + // logical and arithmetic operators + let r = (instr & 0x0F00) >> 8; + let a = cpu.register(r); + let b = cpu.register((instr & 0x00F0) >> 4); + match instr & 0x000F { + 0 => { + cpu.set_register(r, b); + } + 1 => { + cpu.set_register(r, a | b); + } + 2 => { + cpu.set_register(r, a & b); + } + 3 => { + cpu.set_register(r, a ^ b); + } + 4 => { + let result = (a as u16) + (b as u16); + cpu.set_register(r, result as u8); + cpu.set_register(0xF, if result > 255 { 1 } else { 0 }); + } + 5 | 7 => { + let result = if (instr & 0x000F) == 5 { + (a as i16) - (b as i16) + } else { + (b as i16) - (a as i16) + }; + if dbg!(result) >= 0 { + cpu.set_register(r, result as u8); + cpu.set_register(0xF, 1); + } else { + cpu.set_register(r, (256 + result) as u8); + cpu.set_register(0xF, 0); + } + } + 6 => { + cpu.set_register(r, a >> 1); + } + 0xE => { + cpu.set_register(r, a << 1); + } + _ => panic!("instruction unknown: {}", instr), + } + } + 0xA => { + cpu.index_register = instr & 0x0FFF; + } + 0xB => { + // jump with offset + // quirk? + let pointer = cpu.register(0) as u16; + let offset = instr & 0x0FFF; + cpu.program_counter = pointer + offset; + } + 0xC => { + // random + let mask = (instr & 0x00FF) as u8; + let rand_value = rand::random::(); + cpu.set_register((instr & 0x0F00) >> 8, mask & rand_value); + } + 0xD => { + // display + let x = cpu.register((instr & 0x0F00) >> 8) % SCREEN_WIDTH; + let mut y = cpu.register((instr & 0x00F0) >> 4) % SCREEN_HEIGHT; + let n = instr & 0x00F; + cpu.set_register(0xF, 0); + for i in 0..n { + let byte = memory.get(i + cpu.index_register); + let mut x = x; + for bit in 0..7 { + let is_on = (byte & (0b10000000u8 >> bit)) != 0; + if is_on { + let pixel = display.get_pixel_mut(x, y); + if *pixel { + cpu.set_register(0xF, 1); + } + *pixel = !*pixel; + } + x += 1; + if x >= SCREEN_WIDTH { + continue; + } + } + y += 1; + if y >= SCREEN_HEIGHT { + continue; + } + } + } + 0xE => { + // skip based on input + // TODO: keyboard input + } + 0xF => { + let op = (instr & 0x0F00) >> 8; + match instr & 0x00FF { + 0x07 => { + cpu.set_register(op, cpu.delay_timer); + } + 0x15 => { + cpu.delay_timer = cpu.register(op); + } + 0x18 => { + cpu.sound_timer = cpu.register(op); + } + 0x1E => { + let result = (cpu.register(op) as u16) + cpu.index_register; + if result > 0x0FFF { + cpu.set_register(0xF, 1); + } + cpu.index_register = result; + } + 0x0A => { + // TODO: block and wait for key input + } + 0x29 => { + cpu.index_register = (FONT_START as u16) + (cpu.register(op) as u16) & 0x0F * 5; + } + 0x33 => { + let value = cpu.register(op); + memory.set(cpu.index_register + 2, value % 10); + memory.set(cpu.index_register + 1, (value / 10) % 10); + memory.set(cpu.index_register, (value / 100) % 10); + } + 0x55 => { + for i in 0..=op { + memory.set(cpu.index_register + i, cpu.register(i)); + } + // quirk? + } + 0x65 => { + for i in 0..=op { + cpu.set_register(i, memory.get(cpu.index_register + i)); + } + // quirk? + } + _ => panic!("unknown instruction: {}", instr), + } + } + _ => unreachable!("Cannot have a nibble higher than 0xF"), + } +} + +pub struct CPU { + pub delay_timer: u8, + pub sound_timer: u8, + pub stack: Vec, + pub program_counter: u16, + pub index_register: u16, + pub registers: [u8; 16], +} + +impl CPU { + pub fn register(&self, index: u16) -> u8 { + self.registers[index as usize] + } + + pub fn set_register(&mut self, index: u16, value: u8) { + self.registers[index as usize] = value; + } +} + +pub struct Display(Box<[[bool; 64]; 32]>); + +impl Display { + pub fn new() -> Display { + Display(Box::new([[false; 64]; 32])) + } + + pub fn clear(&mut self) { + for row in self.0.iter_mut() { + for cell in row.iter_mut() { + *cell = false; + } + } + } + + pub fn get_pixel(&self, x: u8, y: u8) -> bool { + self.0[y as usize][x as usize] + } + + pub fn get_pixel_mut(&mut self, x: u8, y: u8) -> &mut bool { + &mut self.0[y as usize][x as usize] + } +} + +const FONT_START: usize = 0x50; + +pub struct Memory(Box<[u8; 4096]>); + +impl Memory { + pub fn new() -> Memory { + let mut buffer = Box::new([0u8; 4096]); + + let font = [ + 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 + ]; + + buffer[FONT_START..(font.len() + FONT_START)].copy_from_slice(&font); + + Memory(buffer) + } + + pub fn get(&self, idx: u16) -> u8 { + self.0[idx as usize] + } + + pub fn set(&mut self, idx: u16, value: u8) { + self.0[idx as usize] = value; + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ce3a4ec --- /dev/null +++ b/src/main.rs @@ -0,0 +1,62 @@ +use crunch::Chip8; + +use sdl2::event::Event; +use sdl2::keyboard::Keycode; +use sdl2::pixels::Color; +use sdl2::rect::Rect; +use std::time::Duration; + +pub fn main() -> Result<(), String> { + let rom = std::fs::read("test_opcode.ch8").expect("no rom found at rom.ch8"); + let mut chip = Chip8::new(&rom); + + let sdl_context = sdl2::init()?; + let video_subsystem = sdl_context.video()?; + + let window = video_subsystem + .window("rust-sdl2 demo: Video", 768, 384) + .position_centered() + .opengl() + .build() + .map_err(|e| e.to_string())?; + + let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?; + + canvas.clear(); + canvas.present(); + let mut event_pump = sdl_context.event_pump()?; + + 'running: loop { + for event in event_pump.poll_iter() { + match event { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => break 'running, + _ => {} + } + } + + canvas.set_draw_color(Color::RGB(0, 0, 0)); + canvas.clear(); + canvas.set_draw_color(Color::RGB(255, 255, 255)); + let display = chip.display(); + for x in 0..64 { + for y in 0..32 { + if display.get_pixel(x, y) { + canvas + .fill_rect(Rect::new((x as i32) * 12, (y as i32) * 12, 12, 12)) + .expect("failed to draw rect"); + } + } + } + canvas.present(); + std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60)); + chip.advance(); + // TODO: beep if beep > 0 + // The rest of the game loop goes here... + } + + Ok(()) +}