1
0
Fork 0

initial commit

This commit is contained in:
Jake Walker 2023-04-19 17:41:24 +01:00
commit e8417279bb
7 changed files with 602 additions and 0 deletions

74
.gitignore vendored Normal file
View file

@ -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

13
Cargo.toml Normal file
View file

@ -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"

3
README.md Normal file
View file

@ -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.

349
src/chip8/chip8.rs Normal file
View file

@ -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<u16>,
// 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<u8>, 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::<Vec<String>>().join(", ");
let stack: String = self.stack.iter().map(|a| format!("0x{:x}", a)).collect::<Vec<String>>().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]);
}

18
src/chip8/font.rs Normal file
View file

@ -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
];

2
src/chip8/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod chip8;
mod font;

143
src/main.rs Normal file
View file

@ -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<dyn Error>> {
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<String> = 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();
}
});
}