Restructured opcodes and reorganized code in src/main.rs

This commit is contained in:
Rekai Musuka 2020-06-25 19:06:27 -05:00
parent 4acffdfb6b
commit 9f990159b6
2 changed files with 221 additions and 144 deletions

View File

@ -69,9 +69,7 @@ impl Chip8 {
}
pub fn load_font_set(&mut self) {
for (i, byte) in Self::FONT_SET.iter().enumerate() {
self.memory[i] = *byte;
}
self.memory[0..0x50].copy_from_slice(&Self::FONT_SET);
}
pub fn load_rom<P: AsRef<Path>>(&mut self, path: P) -> Result<(), io::Error> {
@ -79,9 +77,7 @@ impl Chip8 {
let mut rom_buf: Vec<u8> = vec![];
file.read_to_end(&mut rom_buf)?;
for (i, byte) in rom_buf.iter().enumerate() {
self.memory[i + 0x200] = *byte;
}
self.memory[0x200..(0x200 + rom_buf.len())].copy_from_slice(&rom_buf);
Ok(())
}
@ -92,88 +88,98 @@ impl Chip8 {
((self.memory[pc] as u16) << 8) | self.memory[pc + 1] as u16
}
fn get_nibs(opcode: u16) -> (u8, u8, u8, u8) {
// Given 0xA2F0
(
((opcode & 0xF000) >> 12) as u8, // 0xA
((opcode & 0x0F00) >> 8) as u8, // 0x2
((opcode & 0x00F0) >> 4) as u8, // 0xF
(opcode & 0x000F) as u8, // 0x0
)
}
fn handle_opcode(&mut self) {
// Given: 0xA2F0
let nib_1 = (self.opcode & 0xF000) >> 12; //0xA
let nib_2 = (self.opcode & 0x0F00) >> 8; // 0x2
let nib_3 = (self.opcode & 0x00F0) >> 4; // 0xF
let nib_4 = self.opcode & 0x000F; // 0x0
let (nib_1, nib_2, nib_3, nib_4) = Self::get_nibs(self.opcode);
let nnn: u16 = self.opcode & 0x0FFF;
let kk: u8 = (self.opcode & 0x00FF) as u8;
let x = nib_2;
let y = nib_3;
let n = nib_4;
self.pc += 2;
// nib_ns are u16s so we waste 4 bytes here.
match (nib_1, nib_2, nib_3, nib_4) {
// CLS
(0x0, 0x0, 0xE, 0x0) => self.cls(),
// 00EE
(0x0, 0x0, 0xE, 0xE) => self.ret(),
// 1NNN
(0x1, _, _, _) => self.jmp_addr(nib_2, nib_3, nib_4),
(0x1, _, _, _) => self.jmp_addr(nnn),
// 2NNN
(0x2, _, _, _) => self.call_addr(nib_2, nib_3, nib_4),
(0x2, _, _, _) => self.call_addr(nnn),
// 3XKK
(0x3, _, _, _) => self.se_vx_byte(nib_2, nib_3, nib_4),
(0x3, _, _, _) => self.se_vx_byte(x, kk),
// 4xkk
(0x4, _, _, _) => self.sne_vx_byte(nib_2, nib_3, nib_4),
(0x4, _, _, _) => self.sne_vx_byte(x, kk),
// 5xy0
(0x5, _, _, 0x0) => self.se_vx_vy(nib_2, nib_3),
(0x5, _, _, 0x0) => self.se_vx_vy(x, y),
// 6xkk
(0x6, _, _, _) => self.ld_vx_byte(nib_2, nib_3, nib_4),
(0x6, _, _, _) => self.ld_vx_byte(x, kk),
// 7xkk
(0x7, _, _, _) => self.add_vx_byte(nib_2, nib_3, nib_4),
(0x7, _, _, _) => self.add_vx_byte(x, kk),
// 8xy0
(0x8, _, _, 0x0) => self.ld_vx_vy(nib_2, nib_3),
(0x8, _, _, 0x0) => self.ld_vx_vy(x, y),
// 8xy1
(0x8, _, _, 0x1) => self.or_vx_vy(nib_2, nib_3),
(0x8, _, _, 0x1) => self.or(x, y),
// 8xy2
(0x8, _, _, 0x2) => self.and_vx_vy(nib_2, nib_3),
(0x8, _, _, 0x2) => self.and_vx_vy(x, y),
// 8xy3
(0x8, _, _, 0x3) => self.xor_vx_vy(nib_2, nib_3),
(0x8, _, _, 0x3) => self.xor(x, y),
// 8xy4
(0x8, _, _, 0x4) => self.add_vx_vy(nib_2, nib_3),
(0x8, _, _, 0x4) => self.add_vx_vy(x, y),
// 8xy5
(0x8, _, _, 0x5) => self.sub_vx_vy(nib_2, nib_3),
(0x8, _, _, 0x5) => self.sub_vx_vy(x, y),
// 8xy6
(0x8, _, _, 0x6) => self.shr_vx(nib_2),
(0x8, _, _, 0x6) => self.shr(x),
// 8xy7
(0x8, _, _, 0x7) => self.subn_vx_vy(nib_2, nib_3),
(0x8, _, _, 0x7) => self.subn_vx_vy(x, y),
// 8xyE
(0x8, _, _, 0xE) => self.shl_vx(nib_2),
(0x8, _, _, 0xE) => self.shl(x),
// 9xy0
(0x9, _, _, 0x0) => self.sne_vx_vy(nib_2, nib_3),
(0x9, _, _, 0x0) => self.sne_vx_vy(x, y),
// Annn
(0xA, _, _, _) => self.ld_i_addr(nib_2, nib_3, nib_4),
(0xA, _, _, _) => self.load_addr_to_i(nnn),
// Bnnn
(0xB, _, _, _) => self.jmp_v0_addr(nib_2, nib_3, nib_4),
(0xB, _, _, _) => self.jmp_addr_with_offset(nnn),
// Cxkk
(0xC, _, _, _) => self.rnd_vx_byte(nib_2, nib_3, nib_4),
(0xC, _, _, _) => self.rand(x, kk),
//Dxyn
(0xD, _, _, _) => self.drw_vx_vy_nib(nib_2, nib_3, nib_4),
(0xD, _, _, _) => self.draw(x, y, n),
// Ex9E
(0xE, _, 0x9, 0xE) => self.skp_vx(nib_2),
(0xE, _, 0x9, 0xE) => self.skip_on_press(x),
// ExA1
(0xE, _, 0xA, 0x1) => self.sknp_vx(nib_2),
(0xE, _, 0xA, 0x1) => self.skip_not_pressed(x),
// Fx07
(0xF, _, 0x0, 0x7) => self.ld_vx_dt(nib_2),
(0xF, _, 0x0, 0x7) => self.copy_delay_timer_val(x),
// Fx0A
(0xF, _, 0x0, 0xA) => self.ld_vx_k(nib_2),
(0xF, _, 0x0, 0xA) => self.loop_until_key_vx(x),
// Fx15
(0xF, _, 0x1, 0x15) => self.ld_dt_vx(nib_2),
(0xF, _, 0x1, 0x15) => self.set_delay_to_vx(x),
// Fx18
(0xF, _, 0x1, 0x8) => self.ld_st_vx(nib_2),
(0xF, _, 0x1, 0x8) => self.set_sound_to_vx(x),
// Fx1E
(0xF, _, 0x1, 0xE) => self.add_i_vx(nib_2),
(0xF, _, 0x1, 0xE) => self.add_vx_to_i(x),
// Fx29
(0xF, _, 0x2, 0x9) => self.ld_f_vx(nib_2),
(0xF, _, 0x2, 0x9) => self.set_i_to_hex_sprite_loc(x),
// Fx33
(0xF, _, 0x3, 0x3) => self.ld_b_vx(nib_2),
(0xF, _, 0x3, 0x3) => self.copy_digits_to_i(x),
// Fx55
(0xF, _, 0x5, 0x5) => self.ld_i_vx(nib_2),
(0xF, _, 0x5, 0x5) => self.copy_from_vx_to_memory(x),
// Fx65
(0xF, _, 0x6, 0x5) => self.ld_vx_i(nib_2),
// General Case
_ => println!("UNIMPLEMENTED OPCODE: {:#x}", self.opcode),
(0xF, _, 0x6, 0x5) => self.load_into_vx_from_i(x),
// Otherwise...
_ => panic!("UNIMPLEMENTED OPCODE: {:#x}", self.opcode),
}
}
@ -182,171 +188,168 @@ impl Chip8 {
self.display.clear();
}
fn jmp_addr(&mut self, n_1: u16, n_2: u16, n_3: u16) {
// sets the program counter to addr (nnn)
self.pc = Self::convert_to_addr(n_1, n_2, n_3);
println!("{:#x}", self.pc);
}
fn ret(&mut self) {
// sets the program counter to the addres at the top of the stack,
// sets the program counter to the address at the top of the stack,
// then subtracts one (1) from the stack pointer
self.pc = self.stack[self.sp as usize];
self.sp -= 1;
}
fn call_addr(&mut self, n_1: u16, n_2: u16, n_3: u16) {
fn jmp_addr(&mut self, nnn: u16) {
// sets the program counter to addr (nnn)
self.pc = nnn;
}
fn call_addr(&mut self, nnn: u16) {
// increments the stack pointer, then puts current pc on top of stack
// pc is then set to addr
self.sp += 1;
self.stack[self.sp as usize] = self.pc;
self.pc = Self::convert_to_addr(n_1, n_2, n_3);
self.pc = nnn;
}
fn se_vx_byte(&mut self, x: u16, k_1: u16, k_2: u16) {
fn se_vx_byte(&mut self, x: u8, kk: u8) {
// compares Vx to kk. If they are equal, pc is incremented by 2
if self.v[x as usize] == Self::convert_to_byte(k_1, k_2) {
if self.v[x as usize] == kk {
self.pc += 2;
}
}
fn sne_vx_byte(&mut self, x: u16, k_1: u16, k_2: u16) {
fn sne_vx_byte(&mut self, x: u8, kk: u8) {
// compares Vx to kk. If they are **not** equal, pc is incremented by 2
if self.v[x as usize] != Self::convert_to_byte(k_1, k_2) {
if self.v[x as usize] != kk {
self.pc += 2;
}
}
fn se_vx_vy(&mut self, x: u16, y: u16) {
fn se_vx_vy(&mut self, x: u8, y: u8) {
// compares Vx to Vy. If they are equal, pc is incremented by 2
if self.v[x as usize] == self.v[y as usize] {
self.pc += 2;
}
}
fn ld_vx_byte(&mut self, x: u16, k_1: u16, k_2: u16) {
fn ld_vx_byte(&mut self, x: u8, kk: u8) {
// put value kk into Vx
self.v[x as usize] = Self::convert_to_byte(k_1, k_2);
self.v[x as usize] = kk;
}
fn add_vx_byte(&mut self, x: u16, k_1: u16, k_2: u16) {
fn add_vx_byte(&mut self, x: u8, kk: u8) {
// calculate Vx + kk, then store it in Vx
let x = x as usize;
self.v[x] = self.v[x].wrapping_add(Self::convert_to_byte(k_1, k_2));
self.v[x] = self.v[x] + kk;
}
fn ld_vx_vy(&mut self, x: u16, y: u16) {
fn ld_vx_vy(&mut self, x: u8, y: u8) {
// store Vy in Vx
self.v[x as usize] = self.v[y as usize];
}
fn or_vx_vy(&mut self, x: u16, y: u16) {
fn or(&mut self, x: u8, y: u8) {
// calc bitwise OR on Vx and Vy, then store in Vx
let x = x as usize;
self.v[x] = self.v[x] | self.v[y as usize];
}
fn and_vx_vy(&mut self, x: u16, y: u16) {
fn and_vx_vy(&mut self, x: u8, y: u8) {
// calc bitwise AND on Vx and Vy, then store in Vx
let x = x as usize;
self.v[x] = self.v[x] & self.v[y as usize];
}
fn xor_vx_vy(&mut self, x: u16, y: u16) {
fn xor(&mut self, x: u8, y: u8) {
// calc bitwise XOR on Vx and Vy, then store in Vx
let x = x as usize;
self.v[x] = self.v[x] ^ self.v[y as usize];
}
fn add_vx_vy(&mut self, x: u16, y: u16) {
fn add_vx_vy(&mut self, x: u8, y: u8) {
// add Vx and Vy, if result is greater than 8 bits
// set VF to 1, otherwise 0
// only the lowest 8 bits of result are stored in Vx
let x = x as usize;
let (res, did_overflow) = self.v[x].overflowing_add(self.v[y as usize]);
self.v[0xF as usize] = if did_overflow { 1 } else { 0 };
self.v[0xF] = if did_overflow { 1 } else { 0 };
self.v[x] = res;
}
fn sub_vx_vy(&mut self, x: u16, y: u16) {
fn sub_vx_vy(&mut self, x: u8, y: u8) {
// subtract Vx and Vy, if Vx > Vy VF is set to 1, otherwise 0
// then set Vx to Vx - Vy
let vx = self.v[x as usize];
let vy = self.v[y as usize];
self.v[0xF as usize] = if vx > vy { 1 } else { 0 };
self.v[x as usize] = vx.wrapping_sub(vy);
self.v[0xF] = if vx > vy { 1 } else { 0 };
self.v[x as usize] = vx - vy;
}
fn shr_vx(&mut self, x: u16) {
fn shr(&mut self, x: u8) {
// if LSB is set to 1, set VF to 1, otherwise set VF to 0
// then shift Vx one to the right
let x = x as usize;
self.v[0xF as usize] = if (self.v[x] & 1) == 1 { 1 } else { 0 };
self.v[x] = self.v[x] >> 1;
self.v[0xF] = if (self.v[x] & 1) == 1 { 1 } else { 0 };
self.v[x] >>= 1;
}
fn subn_vx_vy(&mut self, x: u16, y: u16) {
fn subn_vx_vy(&mut self, x: u8, y: u8) {
// subtract Vy and Vx, if Vy > Vx VF is set to 1, otherwise 0
// then set Vx = Vy - Vx
let vx = self.v[x as usize];
let vy = self.v[y as usize];
self.v[0xF as usize] = if vy > vx { 1 } else { 0 };
self.v[0xF] = if vy > vx { 1 } else { 0 };
self.v[x as usize] = vy - vx;
}
fn shl_vx(&mut self, x: u16) {
fn shl(&mut self, x: u8) {
// if MSB is set to 1, set VF to 1, otherwise set VF to 0
// then shift Vx one to the left;
let x = x as usize;
self.v[0xF as usize] = if (self.v[x] & 0x80) == 1 { 1 } else { 0 };
self.v[0xF] = if (self.v[x] & 0x80) == 1 { 1 } else { 0 };
self.v[x] = self.v[x] << 1;
}
fn sne_vx_vy(&mut self, x: u16, y: u16) {
fn sne_vx_vy(&mut self, x: u8, y: u8) {
// if Vx != vy program counter is increased by 2
if self.v[x as usize] != self.v[y as usize] {
self.pc += 2;
}
}
fn ld_i_addr(&mut self, n_1: u16, n_2: u16, n_3: u16) {
fn load_addr_to_i(&mut self, nnn: u16) {
// set i to addr
self.i = Self::convert_to_addr(n_1, n_2, n_3);
self.i = nnn;
}
fn jmp_v0_addr(&mut self, n_1: u16, n_2: u16, n_3: u16) {
fn jmp_addr_with_offset(&mut self, nnn: u16) {
// set program counter to addr + V0
self.pc = Self::convert_to_addr(n_1, n_2, n_3) + self.v[0 as usize] as u16;
self.pc = nnn + self.v[0] as u16;
}
fn rnd_vx_byte(&mut self, x: u16, k_1: u16, k_2: u16) {
fn rand(&mut self, x: u8, kk: u8) {
// generate a random number from 0 to 255
// AND with the value of kk, then store in Vx
self.v[x as usize] = rand::random::<u8>() & Self::convert_to_byte(k_1, k_2);
self.v[x as usize] = rand::random::<u8>() & kk;
}
fn drw_vx_vy_nib(&mut self, x: u16, y: u16, nib: u16) {
fn draw(&mut self, x: u8, y: u8, n: u8) {
// read n bytes from memory starting from self.i
// then display them starting at (vx, vy)
let i = self.i as usize;
let disp_x = self.v[x as usize];
let disp_y = self.v[y as usize];
let mut display_buf = self.display.buf;
let mut pixel: u8;
self.v[0xF as usize] = 0;
self.v[0xF] = 0;
// TODO: Rewrite Your Solution to better fit
// the rest of your code base.
// http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/
for i in 0..(nib as usize) {
for i in 0..(n as usize) {
// height of sprite (y-axis)
pixel = self.memory[self.i as usize + i];
for j in 0..8 {
@ -355,7 +358,7 @@ impl Chip8 {
let index = (disp_x as usize) + j + (((disp_y as usize) + i) * 64);
if display_buf[index] == 1 {
self.v[0xF as usize] = 1;
self.v[0xF] = 1;
}
display_buf[index] ^= 1;
@ -364,7 +367,7 @@ impl Chip8 {
}
}
fn skp_vx(&mut self, x: u16) {
fn skip_on_press(&mut self, x: u8) {
// if current key is the same as the on in Vx
// program counter is increased by 2
@ -375,7 +378,7 @@ impl Chip8 {
}
}
fn sknp_vx(&mut self, x: u16) {
fn skip_not_pressed(&mut self, x: u8) {
// if current key is not the sameas the one in Vx
// increment the program counter by 2
@ -389,12 +392,12 @@ impl Chip8 {
}
}
fn ld_vx_dt(&mut self, x: u16) {
fn copy_delay_timer_val(&mut self, x: u8) {
// set Vx to be the value of the delay timer
self.v[x as usize] = self.delay.get();
}
fn ld_vx_k(&mut self, x: u16) {
fn loop_until_key_vx(&mut self, x: u8) {
// wait (blocking) until a key is pressed
// once pressed, store in Vx
@ -404,27 +407,27 @@ impl Chip8 {
}
}
fn ld_dt_vx(&mut self, x: u16) {
fn set_delay_to_vx(&mut self, x: u8) {
// set delay timer to be value of Vx
self.delay.set(self.v[x as usize]);
}
fn ld_st_vx(&mut self, x: u16) {
fn set_sound_to_vx(&mut self, x: u8) {
// set sound timer to be value of Vx
self.delay.set(self.v[x as usize]);
}
fn add_i_vx(&mut self, x: u16) {
fn add_vx_to_i(&mut self, x: u8) {
// set I to be I + Vx
self.i = self.i + self.v[x as usize] as u16;
}
fn ld_f_vx(&mut self, x: u16) {
fn set_i_to_hex_sprite_loc(&mut self, x: u8) {
// set I to location of hex sprite related to Vx
self.i = self.v[x as usize] as u16 * 5;
}
fn ld_b_vx(&mut self, x: u16) {
fn copy_digits_to_i(&mut self, x: u8) {
// take hundreds digit and place it at I
// take tens digit and place it at I + 1
// take ones digit and place it at I + 2
@ -440,7 +443,7 @@ impl Chip8 {
self.memory[i + 2] = ones as u8;
}
fn ld_i_vx(&mut self, x: u16) {
fn copy_from_vx_to_memory(&mut self, x: u8) {
// copy values v0 -> Vx to memory starting at i
for n in 0..=(x as usize) {
@ -448,7 +451,7 @@ impl Chip8 {
}
}
fn ld_vx_i(&mut self, x: u16) {
fn load_into_vx_from_i(&mut self, x: u8) {
// read what will be values of v0 -> vx from memory starting at i
let x = x as usize;
@ -457,14 +460,6 @@ impl Chip8 {
}
}
fn convert_to_byte(k_1: u16, k_2: u16) -> u8 {
((k_1 as u8) << 4) | (k_2 as u8)
}
fn convert_to_addr(n_1: u16, n_2: u16, n_3: u16) -> u16 {
(n_1 << 8) | (n_2 << 4) | n_3
}
// https://stackoverflow.com/questions/41536479/how-do-i-split-an-integer-into-individual-digits
fn digits(mut num: usize) -> impl Iterator<Item = usize> {
let mut divisor = 1;
@ -484,3 +479,77 @@ impl Chip8 {
})
}
}
#[cfg(test)]
mod test {
use super::Chip8;
#[test]
fn chip8_initializes_properly() {
let chip8: Chip8 = Default::default();
assert_eq!(chip8.pc, 0x200);
let slice: &[u8] = &chip8.memory[0..0x50];
let font_set: &[u8] = &Chip8::FONT_SET;
for (i, byte) in slice.iter().enumerate() {
assert_eq!(*byte, font_set[i]);
}
}
#[test]
fn get_opcode_works() {
let mut chip8: Chip8 = Default::default();
let expected = 0x00E0; // Clear Screen
chip8.memory[0x202] = 0x00;
chip8.memory[0x203] = 0xE0;
chip8.pc = 0x202;
assert_eq!(chip8.get_opcode(), expected);
}
#[test]
fn get_nibs_works() {
let opcode = 0xA2F0;
let (n1, n2, n3, n4) = Chip8::get_nibs(opcode);
assert_eq!(n1, 0xA);
assert_eq!(n2, 0x2);
assert_eq!(n3, 0xF);
assert_eq!(n4, 0x0);
}
#[test]
fn opcode_cls_works() {
let mut chip8: Chip8 = Default::default();
// Display is now noise
for byte in chip8.display.buf.iter_mut() {
*byte = rand::random();
}
chip8.cls();
// now check that everything in the display buffer is zeroed out
for byte in chip8.display.buf.iter() {
assert_eq!(*byte, 0);
}
}
#[test]
fn opcode_ret_works() {
let mut chip8: Chip8 = Default::default();
chip8.pc = 0x202;
chip8.stack[0] = 0xABF0;
chip8.stack[1] = 0x50D5; // 80
chip8.sp = 1;
chip8.ret();
// Ret should set the programcouner to the address at the top of the stack
// and then decrement the stack pointer by 1
assert_eq!(chip8.sp, 0);
assert_eq!(chip8.pc, 0x50D5);
}
}

View File

@ -3,13 +3,12 @@
// for information about how to implement a CHIP-8 Emulator
use chip8::emu::Chip8;
use std::path::Path;
use std::time::Duration;
use pixels::{wgpu::Surface, Error, Pixels, SurfaceTexture};
use pixels::{wgpu::Surface, Pixels, SurfaceTexture};
use winit::dpi::LogicalSize;
use winit::event::{Event, VirtualKeyCode};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
use winit::window::{Window, WindowBuilder};
use winit_input_helper::WinitInputHelper;
static WIDTH: u32 = 64;
@ -17,57 +16,66 @@ static HEIGHT: u32 = 32;
fn main() {
let event_loop = EventLoop::new();
let mut input = WinitInputHelper::new();
let window = {
let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64);
WindowBuilder::new()
.with_title("Chip8 Emulator")
.with_inner_size(size)
.with_min_inner_size(size)
.build(&event_loop)
.unwrap()
};
let window = init_window(&event_loop);
let mut pixels = init_pixels(&window);
let mut hidpi_factor = window.scale_factor();
let mut pixels = {
let surface = Surface::create(&window);
let surface_texture = SurfaceTexture::new(WIDTH, HEIGHT, surface);
Pixels::new(WIDTH, HEIGHT, surface_texture).unwrap()
};
let mut input = WinitInputHelper::new();
let mut chip8: Chip8 = Default::default();
chip8
.load_rom(Path::new("./games/test_opcode.ch8"))
.expect("Unable to load ROM");
event_loop.run(move |event, _, control_flow| {
chip8.execute_cycle();
draw(&chip8.display.buf, pixels.get_frame());
pixels.render().unwrap();
window.request_redraw();
if let Event::RedrawRequested(_) = event {
draw(&chip8.display.buf, pixels.get_frame());
if pixels
.render()
.map_err(|e| eprintln!("pixels.render() failed: {}", e))
.is_err()
{
*control_flow = ControlFlow::Exit;
return;
}
}
if input.update(&event) {
if let Some(factor) = input.scale_factor_changed() {
hidpi_factor = factor;
};
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
*control_flow = ControlFlow::Exit;
return;
}
if let Some(factor) = input.scale_factor_changed() {
hidpi_factor = factor;
};
if let Some(size) = input.window_resized() {
pixels.resize(size.width, size.height);
}
}
// std::thread::sleep(Duration::from_millis(1000));
chip8.execute_cycle();
window.request_redraw();
}
});
}
fn init_pixels(window: &Window) -> Pixels {
let surface = Surface::create(window);
let texture = SurfaceTexture::new(WIDTH, HEIGHT, surface);
Pixels::new(WIDTH, HEIGHT, texture).unwrap()
}
fn init_window(event_loop: &EventLoop<()>) -> Window {
let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64);
WindowBuilder::new()
.with_title("Chip8 Emulator")
.with_inner_size(size)
.with_min_inner_size(size)
.build(event_loop)
.unwrap()
}
fn draw(chip8_gfx: &[u8], frame: &mut [u8]) {
for (i, pixel) in frame.chunks_exact_mut(4).enumerate() {
let rgba = if chip8_gfx[i] != 0 {