Created
January 22, 2023 10:19
-
-
Save thevar1able/b4dba91eb9bf4643d203b7168f6936e3 to your computer and use it in GitHub Desktop.
CHIP-8 emulator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::time::Duration; | |
| use std::{env, io, fs, fmt}; | |
| use sdl2::Sdl; | |
| use sdl2::pixels::Color; | |
| use sdl2::render::Canvas; | |
| struct Instruction { | |
| bytes: [u8; 2], | |
| } | |
| impl Instruction { | |
| fn as_chars(&self) -> String { | |
| return format!("{:02x}{:02x}", self.bytes[0], self.bytes[1]); | |
| } | |
| } | |
| impl fmt::Display for Instruction { | |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
| write!(f, "0x{}", self.as_chars()) | |
| } | |
| } | |
| #[derive(Debug)] | |
| struct CPU { | |
| program_counter: u16, | |
| stack_pointer: u8, | |
| memory: [u8; 4096], | |
| stack: [u16; 16], | |
| display: [u8; 64 * 32], | |
| keypad: [bool; 16], | |
| v_registers: [u8; 16], | |
| i_register: u16, | |
| delay_register: u8, | |
| timer_register: u8, | |
| } | |
| impl CPU { | |
| fn new() -> Self { | |
| Self { | |
| program_counter: 0x200, | |
| stack_pointer: 0, | |
| memory: [0; 4096], | |
| stack: [0; 16], | |
| display: [0; 64 * 32], | |
| keypad: [false; 16], | |
| v_registers: [0; 16], | |
| i_register: 0, | |
| delay_register: 0, | |
| timer_register: 0, | |
| } | |
| } | |
| fn load_rom(&mut self, rom: &str) { | |
| let rom_bytes = fs::read(rom).unwrap(); | |
| for (i, byte) in rom_bytes.iter().enumerate() { | |
| self.memory[i + 0x200] = *byte; | |
| } | |
| } | |
| fn fetch(&self) -> Instruction { | |
| Instruction { | |
| bytes: [self.memory[self.program_counter as usize], self.memory[self.program_counter as usize + 1]], | |
| } | |
| } | |
| fn execute(& mut self, instruction: &Instruction) { | |
| let register_x = (instruction.bytes[0] & 0x0F) as usize; | |
| let register_y = (instruction.bytes[1] & 0xF0) as usize >> 4; | |
| match instruction.as_chars().as_bytes() { | |
| [b'0', b'0', b'e', b'0'] => { | |
| println!("CLS"); | |
| self.display = [0; 64 * 32]; | |
| self.program_counter += 2; | |
| }, | |
| [b'0', b'0', b'e', b'e'] => { | |
| println!("RET"); | |
| self.stack_pointer -= 1; | |
| self.program_counter = self.stack[self.stack_pointer as usize]; | |
| self.program_counter += 2; | |
| }, | |
| [b'0', b'0', b'0', b'0'] => { | |
| println!("NOP"); | |
| self.program_counter += 2; | |
| }, | |
| [b'0', ..] => { | |
| println!("SYS {}", instruction); | |
| // this is a noop | |
| self.program_counter += 2; | |
| }, | |
| [b'1', ..] => { | |
| println!("JP {}", instruction); | |
| self.program_counter = u16::from_be_bytes(instruction.bytes) & 0x0FFF; | |
| }, | |
| [b'2', ..] => { | |
| println!("CALL {}", instruction); | |
| self.stack[self.stack_pointer as usize] = self.program_counter; | |
| self.stack_pointer += 1; | |
| self.program_counter = u16::from_be_bytes(instruction.bytes) & 0x0FFF; | |
| }, | |
| [b'3', ..] => { | |
| println!("SE {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| let value = instruction.bytes[1]; | |
| if self.v_registers[register] == value { | |
| self.program_counter += 4; | |
| } else { | |
| self.program_counter += 2; | |
| } | |
| }, | |
| [b'4', ..] => { | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| let value = instruction.bytes[1]; | |
| if self.v_registers[register] != value { | |
| self.program_counter += 4; | |
| } else { | |
| self.program_counter += 2; | |
| } | |
| }, | |
| [b'5', .., b'0'] => { | |
| println!("SE {}", instruction); | |
| if self.v_registers[register_x] == self.v_registers[register_y] { | |
| self.program_counter += 4; | |
| } else { | |
| self.program_counter += 2; | |
| } | |
| }, | |
| [b'6', ..] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(0x0F & instruction.bytes[0]); | |
| let value = instruction.bytes[1]; | |
| self.v_registers[register] = value; | |
| self.program_counter += 2; | |
| }, | |
| [b'7', ..] => { | |
| println!("ADD {}", instruction); | |
| let register = usize::from(0x0F & instruction.bytes[0]); | |
| let value = instruction.bytes[1]; | |
| self.v_registers[register] = self.v_registers[register].wrapping_add(value); | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'0'] => { | |
| println!("LD {}", instruction); | |
| self.v_registers[register_x] = self.v_registers[register_y]; | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'1'] => { | |
| println!("OR {}", instruction); | |
| self.v_registers[register_x] |= self.v_registers[register_y]; | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'2'] => { | |
| println!("AND {}", instruction); | |
| self.v_registers[register_x] &= self.v_registers[register_y]; | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'3'] => { | |
| println!("XOR {}", instruction); | |
| self.v_registers[register_x] ^= self.v_registers[register_y]; | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'4'] => { | |
| println!("ADD {}", instruction); | |
| let (result, overflow) = self.v_registers[register_x].overflowing_add(self.v_registers[register_y]); | |
| self.v_registers[register_x] = result; | |
| self.v_registers[0xF] = if overflow { 1 } else { 0 }; | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'5'] => { | |
| println!("SUB {}", instruction); | |
| let (result, overflow) = self.v_registers[register_x].overflowing_sub(self.v_registers[register_y]); | |
| self.v_registers[register_x] = result; | |
| self.v_registers[0xF] = if overflow { 0 } else { 1 }; | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'6'] => { | |
| println!("SHR {}", instruction); | |
| let overflow = self.v_registers[register_x] & 0x1; | |
| self.v_registers[register_x] >>= 1; | |
| self.v_registers[0xF] = overflow; | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'7'] => { | |
| println!("SUBN {}", instruction); | |
| let (result, overflow) = self.v_registers[register_y].overflowing_sub(self.v_registers[register_x]); | |
| self.v_registers[register_x] = result; | |
| self.v_registers[0xF] = if overflow { 0 } else { 1 }; | |
| self.program_counter += 2; | |
| }, | |
| [b'8', .., b'e'] => { | |
| println!("SHL {}", instruction); | |
| let overflow = (self.v_registers[register_x] & 0x80) >> 7; | |
| self.v_registers[register_x] <<= 1; | |
| self.v_registers[0xF] = overflow; | |
| self.program_counter += 2; | |
| }, | |
| [b'9', .., b'0'] => { | |
| println!("SNE {}", instruction); | |
| if self.v_registers[register_x] != self.v_registers[register_y] { | |
| self.program_counter += 4; | |
| } else { | |
| self.program_counter += 2; | |
| } | |
| }, | |
| [b'a', ..] => { | |
| println!("LD {}", instruction); | |
| let value = u16::from_be_bytes(instruction.bytes) & 0x0FFF; | |
| self.i_register = value; | |
| self.program_counter += 2; | |
| }, | |
| [b'b', ..] => { | |
| println!("JP {}", instruction); | |
| let value = u16::from_be_bytes(instruction.bytes) & 0x0FFF; | |
| self.program_counter = value + u16::from(self.v_registers[0]); | |
| }, | |
| [b'c', ..] => { | |
| println!("RND {}", instruction); | |
| let register = usize::from(0x0F & instruction.bytes[0]); | |
| let value = instruction.bytes[1]; | |
| self.v_registers[register] = rand::random::<u8>() & value; | |
| self.program_counter += 2; | |
| }, | |
| [b'd', ..] => { | |
| println!("DRW {}", instruction); | |
| let x = usize::from(instruction.bytes[0] & 0x0F); | |
| let y = usize::from((instruction.bytes[1] & 0xF0) >> 4); | |
| let height = usize::from(instruction.bytes[1] & 0x0F); | |
| let x = self.v_registers[x] as usize; | |
| let y = self.v_registers[y] as usize; | |
| self.v_registers[0xF] = 0; | |
| for row in 0..height { | |
| let pixel = self.memory[(self.i_register + row as u16) as usize]; | |
| for col in 0..8 { | |
| let real_x = usize::from((x + col) % 64); | |
| let real_y = usize::from((y + row) % 32); | |
| if pixel & (0x80 >> col) != 0 { | |
| self.v_registers[0xF] |= self.display[real_y * 64 + real_x]; | |
| self.display[real_y * 64 + real_x] ^= 0xFF; | |
| } | |
| } | |
| } | |
| self.program_counter += 2; | |
| }, | |
| [b'e', .., b'9', b'e'] => { | |
| println!("SKP {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| if self.keypad[self.v_registers[register] as usize] { | |
| self.program_counter += 4; | |
| } else { | |
| self.program_counter += 2; | |
| } | |
| }, | |
| [b'e', .., b'a', b'1'] => { | |
| println!("SKNP {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| if !self.keypad[self.v_registers[register] as usize] { | |
| self.program_counter += 4; | |
| } else { | |
| self.program_counter += 2; | |
| } | |
| }, | |
| [b'f', .., b'0', b'7'] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| self.v_registers[register] = self.delay_register; | |
| self.program_counter += 2; | |
| }, | |
| [b'f', .., b'0', b'a'] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| if self.keypad.into_iter().any(|x| x) { | |
| self.program_counter += 2; | |
| } | |
| for (idx, value) in self.keypad.into_iter().enumerate() { | |
| if !value { | |
| continue | |
| } | |
| self.v_registers[register] = idx as u8; | |
| self.keypad[idx as usize] = false; | |
| } | |
| }, | |
| [b'f', .., b'1', b'5'] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| self.delay_register = self.v_registers[register]; | |
| self.program_counter += 2; | |
| }, | |
| [b'f', .., b'1', b'8'] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| self.timer_register = self.v_registers[register]; | |
| self.program_counter += 2; | |
| }, | |
| [b'f', .., b'1', b'e'] => { | |
| println!("ADD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| self.i_register += u16::from(self.v_registers[register]); | |
| self.program_counter += 2; | |
| }, | |
| [b'f', .., b'2', b'9'] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| self.i_register = u16::from(self.v_registers[register]) * 5; | |
| self.program_counter += 2; | |
| }, | |
| [b'f', .., b'3', b'3'] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| let value = self.v_registers[register]; | |
| self.memory[self.i_register as usize] = value / 100; | |
| self.memory[(self.i_register + 1) as usize] = (value / 10) % 10; | |
| self.memory[(self.i_register + 2) as usize] = (value % 100) % 10; | |
| self.program_counter += 2; | |
| }, | |
| [b'f', .., b'5', b'5'] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| for i in 0..=register { | |
| self.memory[(self.i_register + i as u16) as usize] = self.v_registers[i]; | |
| } | |
| self.program_counter += 2; | |
| }, | |
| [b'f', .., b'6', b'5'] => { | |
| println!("LD {}", instruction); | |
| let register = usize::from(instruction.bytes[0] & 0x0F); | |
| for i in 0..=register { | |
| self.v_registers[i] = self.memory[(self.i_register + i as u16) as usize]; | |
| } | |
| self.program_counter += 2; | |
| }, | |
| _ => panic!("Unknown instruction {}", instruction) | |
| } | |
| self.delay_register = self.delay_register.checked_sub(1).unwrap_or(0); | |
| } | |
| fn cycle(&mut self) { | |
| let instruction = self.fetch(); | |
| // println!("PC: {:x?}", self.program_counter); | |
| // println!("Registers V0-VF: {:?}", self.v_registers); | |
| // println!("Register I: {:x?}", self.i_register); | |
| // println!("Stack: {:?}", self.stack); | |
| // println!("Stack pointer: {:x?}", self.stack_pointer); | |
| // println!("Executing {}", instruction); | |
| self.execute(&instruction); | |
| // println!("PC: {:x?}", self.program_counter); | |
| // println!("Registers V0-VF: {:?}", self.v_registers); | |
| // println!("Register I: {:x?}", self.i_register); | |
| // println!("Stack: {:?}", self.stack); | |
| // println!("Stack pointer: {:x?}", self.stack_pointer); | |
| println!("Keypad: {:?}", self.keypad); | |
| // println!("===") | |
| } | |
| } | |
| fn get_display(sdl_context: &Sdl) -> Canvas<sdl2::video::Window> { | |
| let video = sdl_context.video().expect("SDL2 failed"); | |
| let window = video | |
| .window("Chip-8", 1280, 640) | |
| .position_centered() | |
| .vulkan() | |
| .build() | |
| .expect("Window init failed"); | |
| let mut canvas = window.into_canvas().build().expect("canvas fail"); | |
| canvas.set_draw_color(Color::RGB(0,0,0)); | |
| canvas.clear(); | |
| canvas.present(); | |
| canvas | |
| } | |
| fn main() -> io::Result<()> { | |
| if env::args().len() < 2 { | |
| eprintln!("missing args: filename"); | |
| return Ok(()); | |
| } | |
| let filename = env::args().nth(1).unwrap(); | |
| println!("filename: {}", filename); | |
| let mut cpu = CPU::new(); | |
| cpu.load_rom(&filename); | |
| let sdl_context = sdl2::init().expect("SDL2 init failed"); | |
| let mut canvas = get_display(&sdl_context); | |
| let mut event_pump = sdl_context.event_pump().expect("event pump init fail"); | |
| 'mainloop: loop { | |
| for event in event_pump.poll_iter() { | |
| match event { | |
| sdl2::event::Event::Quit { .. } => break 'mainloop, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::Escape), .. } => break 'mainloop, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::U), .. } => { | |
| cpu.keypad[12] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::I), .. } => { | |
| cpu.keypad[13] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::O), .. } => { | |
| cpu.keypad[14] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::P), .. } => { | |
| cpu.keypad[15] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::H), .. } => { | |
| cpu.keypad[8] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::J), .. } => { | |
| cpu.keypad[9] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::K), .. } => { | |
| cpu.keypad[10] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::L), .. } => { | |
| cpu.keypad[11] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::N), .. } => { | |
| cpu.keypad[7] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::M), .. } => { | |
| cpu.keypad[6] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::Comma), .. } => { | |
| cpu.keypad[5] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::Period), .. } => { | |
| cpu.keypad[4] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::Slash), .. } => { | |
| cpu.keypad[0] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::A), .. } => { | |
| cpu.keypad[1] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::S), .. } => { | |
| cpu.keypad[2] = true; | |
| }, | |
| sdl2::event::Event::KeyDown { keycode: Some(sdl2::keyboard::Keycode::D), .. } => { | |
| cpu.keypad[3] = true; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::U), .. } => { | |
| cpu.keypad[12] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::I), .. } => { | |
| cpu.keypad[13] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::O), .. } => { | |
| cpu.keypad[14] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::P), .. } => { | |
| cpu.keypad[15] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::H), .. } => { | |
| cpu.keypad[8] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::J), .. } => { | |
| cpu.keypad[9] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::K), .. } => { | |
| cpu.keypad[10] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::L), .. } => { | |
| cpu.keypad[11] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::N), .. } => { | |
| cpu.keypad[7] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::M), .. } => { | |
| cpu.keypad[6] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::Comma), .. } => { | |
| cpu.keypad[5] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::Period), .. } => { | |
| cpu.keypad[4] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::Slash), .. } => { | |
| cpu.keypad[0] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::A), .. } => { | |
| cpu.keypad[1] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::S), .. } => { | |
| cpu.keypad[2] = false; | |
| }, | |
| sdl2::event::Event::KeyUp { keycode: Some(sdl2::keyboard::Keycode::D), .. } => { | |
| cpu.keypad[3] = false; | |
| }, | |
| _ => {} | |
| } | |
| } | |
| cpu.cycle(); | |
| // cpu.keypad = [false; 16]; | |
| canvas.clear(); | |
| for (idx, pixel) in cpu.display.iter().enumerate() { | |
| match pixel { | |
| 255 => canvas.set_draw_color(sdl2::pixels::Color::RGB(0x9B, 0x80, 0xB6)), | |
| _ => canvas.set_draw_color(sdl2::pixels::Color::RGB(0x12, 0x09, 0x20)), | |
| } | |
| let x = (idx % 64) * 20; | |
| let y = (idx / 64) * 20; | |
| canvas.fill_rect(sdl2::rect::Rect::new( | |
| x as i32, y as i32, 20, 20 | |
| )).unwrap(); | |
| } | |
| canvas.present(); | |
| ::std::thread::sleep(Duration::new(0, 1_000_000_000 / 480)); | |
| } | |
| Ok(()) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment