Skip to content

Instantly share code, notes, and snippets.

@thevar1able
Created January 22, 2023 10:19
Show Gist options
  • Save thevar1able/b4dba91eb9bf4643d203b7168f6936e3 to your computer and use it in GitHub Desktop.
Save thevar1able/b4dba91eb9bf4643d203b7168f6936e3 to your computer and use it in GitHub Desktop.
CHIP-8 emulator
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