initial commit
This commit is contained in:
commit
e8417279bb
7 changed files with 602 additions and 0 deletions
74
.gitignore
vendored
Normal file
74
.gitignore
vendored
Normal 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
13
Cargo.toml
Normal 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
3
README.md
Normal 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
349
src/chip8/chip8.rs
Normal 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
18
src/chip8/font.rs
Normal 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
2
src/chip8/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod chip8;
|
||||||
|
mod font;
|
143
src/main.rs
Normal file
143
src/main.rs
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue