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