commit 546800f34426e1c61dd4c9aa53b47b1d25e9b778 Author: baldeau Date: Mon Apr 28 19:10:40 2025 +0200 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..706fc34 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# chip8_rust + +Full chip8 implementation in Rust. +For learning purposes only. diff --git a/chip8_core/.gitignore b/chip8_core/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/chip8_core/.gitignore @@ -0,0 +1 @@ +/target diff --git a/chip8_core/Cargo.lock b/chip8_core/Cargo.lock new file mode 100644 index 0000000..635e5a6 --- /dev/null +++ b/chip8_core/Cargo.lock @@ -0,0 +1,157 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chip8_core" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/chip8_core/Cargo.toml b/chip8_core/Cargo.toml new file mode 100644 index 0000000..114c724 --- /dev/null +++ b/chip8_core/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "chip8_core" +version = "0.1.0" +edition = "2024" + +[dependencies] +rand = "0.9.1" diff --git a/chip8_core/src/lib.rs b/chip8_core/src/lib.rs new file mode 100644 index 0000000..43eaf61 --- /dev/null +++ b/chip8_core/src/lib.rs @@ -0,0 +1,445 @@ +use rand::random; + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} + +const FONTSET_SIZE: usize = 80; +const FONTSET: [u8; FONTSET_SIZE] = [ + 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 +]; + +pub const SCREEN_WIDTH: usize = 64; +pub const SCREEN_HEIGHT: usize = 32; +const RAM_SIZE: usize = 4096; +const NUM_REGS: usize = 16; +const STACK_SIZE: usize = 16; +const NUM_KEYS: usize = 16; +pub struct Emu { + // Program Counter + pc: u16, + ram: [u8; RAM_SIZE], + screen: [bool; SCREEN_WIDTH * SCREEN_HEIGHT], + // V 8-bit registers + v_reg: [u8; NUM_REGS], + // I 16-bit register + i_reg: u16, + // stack pointer + sp: u16, + // 16-bit stack + stack: [u16; STACK_SIZE], + keys: [bool; NUM_KEYS], + // delay timer + dt: u8, + // sound timer + st: u8, +} + +// Chip-8 programs have to start at 0x200 +const START_ADDR: u16 = 0x200; + +impl Emu { + pub fn new() -> Self { + let mut new_emu = Self { + pc: START_ADDR, + ram: [0; RAM_SIZE], + screen: [false; SCREEN_WIDTH * SCREEN_HEIGHT], + v_reg: [0; NUM_REGS], + i_reg: 0, + sp: 0, + stack: [0; STACK_SIZE], + keys: [false; NUM_KEYS], + dt: 0, + st: 0, + }; + new_emu.ram[..FONTSET_SIZE].copy_from_slice(&FONTSET); + new_emu + } + + pub fn reset(&mut self) { + self.pc = START_ADDR; + self.ram = [0; RAM_SIZE]; + self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT]; + self.v_reg = [0; NUM_REGS]; + self.i_reg = 0; + self.sp = 0; + self.stack = [0; STACK_SIZE]; + self.keys = [false; NUM_KEYS]; + self.dt = 0; + self.st = 0; + self.ram[..FONTSET_SIZE].copy_from_slice(&FONTSET); + } + + // push to the stack + fn push(&mut self, val: u16) { + self.stack[self.sp as usize] = val; + self.sp += 1; + } + + // pop from the stack + fn pop(&mut self) -> u16 { + self.sp -= 1; + self.stack[self.sp as usize] + } + + // CPU tick + pub fn tick(&mut self) { + // Fetch + let op = self.fetch(); + // Decode & Execute + self.execute(op); + } + fn execute(&mut self, op: u16) { + let digit1 = (op & 0xF000) >> 12; + let digit2 = (op & 0x0F00) >> 8; + let digit3 = (op & 0x00F0) >> 4; + let digit4 = op & 0x000F; + match (digit1, digit2, digit3, digit4) { + // Nop + (0, 0, 0, 0) => return, + // clear screen + (0, 0, 0xE, 0) => self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT], + // RET: return from subroutine + (0, 0, 0xE, 0xE) => { + let ret_addr = self.pop(); + self.pc = ret_addr + } + // JMP NNN: jump instruction + (1, _, _, _) => { + let nnn = op & 0xFFF; + self.pc = nnn; + } + // CALL NNN: call subroutine + (2, _, _, _) => { + let nnn = op & 0xFFF; + self.push(self.pc); + self.pc = nnn; + } + // 3XNN: skip next if VX == NN + (3, _, _, _) => { + let x = digit2 as usize; + let nn = (op & 0xFF) as u8; + if self.v_reg[x] == nn { + self.pc += 2; + } + } + // 4XNN: skip next if VX != NN + (4, _, _, _) => { + let x = digit2 as usize; + let nn = (op & 0xFF) as u8; + if self.v_reg[x] != nn { + self.pc += 2; + } + } + // 5XY0: skip next if VX == VY + (5, _, _, 0) => { + let x = digit2 as usize; + let y = digit3 as usize; + if self.v_reg[x] == self.v_reg[y] { + self.pc += 2; + } + } + // 6XNN: VX = NN + // set the V register specified by the second digit + // to the value given + (6, _, _, _) => { + let x = digit2 as usize; + let nn = (op & 0xFF) as u8; + self.v_reg[x] = nn; + } + // 7XNN: VX += NN + // adds the given number to the specified V register + (7, _, _, _) => { + let x = digit2 as usize; + let nn = (op & 0xFF) as u8; + self.v_reg[x] = self.v_reg[x].wrapping_add(nn); + } + // 8XY0: VX = VY + (8, _, _, 0) => { + let x = digit2 as usize; + let y = digit3 as usize; + self.v_reg[x] = self.v_reg[y]; + } + // 8XY1, 8XY2, 8XY3: bitwise operations + // VX |= VY + (8, _, _, 1) => { + let x = digit2 as usize; + let y = digit3 as usize; + self.v_reg[x] |= self.v_reg[y]; + } + // VX &= VY + (8, _, _, 2) => { + let x = digit2 as usize; + let y = digit3 as usize; + self.v_reg[x] &= self.v_reg[y]; + } + // VX ^= VY + (8, _, _, 3) => { + let x = digit2 as usize; + let y = digit3 as usize; + self.v_reg[x] ^= self.v_reg[y]; + } + // 8XY4: VX += VY + (8, _, _, 4) => { + let x = digit2 as usize; + let y = digit3 as usize; + let (new_vx, carry) = self.v_reg[x].overflowing_add(self.v_reg[y]); + // set 16th V register to 1 if overflow + // the 16th V register handles over- and underflows + let new_vf = if carry { 1 } else { 0 }; + self.v_reg[x] = new_vx; + self.v_reg[0xF] = new_vf; + } + // 8XY5: VX -= VY + (8, _, _, 5) => { + let x = digit2 as usize; + let y = digit3 as usize; + let (new_vx, borrow) = self.v_reg[x].overflowing_sub(self.v_reg[y]); + let new_vf = if borrow { 0 } else { 1 }; + self.v_reg[x] = new_vx; + self.v_reg[0xF] = new_vf; + } + // 8XY6: VX >>= 1 + // single right shift on value VX + (8, _, _, 6) => { + let x = digit2 as usize; + let lsb = self.v_reg[x] & 1; + self.v_reg[x] >>= 1; + self.v_reg[0xF] = lsb; + } + // 8XY7: VX = VY - VX + (8, _, _, 7) => { + let x = digit2 as usize; + let y = digit3 as usize; + let (new_vx, borrow) = self.v_reg[y].overflowing_sub(self.v_reg[x]); + let new_vf = if borrow { 0 } else { 1 }; + self.v_reg[x] = new_vx; + self.v_reg[0xF] = new_vf; + } + // 8XYE: VX <<= 1 + // single left shift on value VX + (8, _, _, 0xE) => { + let x = digit2 as usize; + let msb = (self.v_reg[x] >> 7) & 1; + self.v_reg[x] <<= 1; + self.v_reg[0xF] = msb; + } + // 9XY0: skip if VX != VY + (9, _, _, 0) => { + let x = digit2 as usize; + let y = digit3 as usize; + if self.v_reg[x] != self.v_reg[y] { + self.pc += 2; + } + } + // ANNN: I = NNN + // set I register to the 0xNNN value encoded in this opcode + (0xA, _, _, _) => { + let nnn = op & 0xFFF; + self.i_reg = nnn; + } + // BNNN: jump to V0 + NNN + (0xB, _, _, _) => { + let nnn = op & 0xFFF; + self.pc = (self.v_reg[0] as u16) + nnn; + } + // CXNN: VX = rand() & NN + (0xC, _, _, _) => { + let x = digit2 as usize; + let nn = (op & 0xFF) as u8; + let rng: u8 = random(); + self.v_reg[x] = rng & nn; + } + // DRAW + (0xD, _, _, _) => { + // Get the (x, y) coords for our sprite + let x_coord = self.v_reg[digit2 as usize] as u16; + let y_coord = self.v_reg[digit3 as usize] as u16; + // The last digit determines how many rows high our sprite is + let num_rows = digit4; + // Keep track if any pixels were flipped + let mut flipped = false; + // Iterate over each row of our sprite + for y_line in 0..num_rows { + // Determine which memory address our row's data is stored + let addr = self.i_reg + y_line as u16; + let pixels = self.ram[addr as usize]; + // Iterate over each column in our row + for x_line in 0..8 { + if (pixels & (0b1000_0000 >> x_line)) != 0 { + // Sprites should wrap around screen, so apply modulo + let x = (x_coord + x_line) as usize % SCREEN_WIDTH; + let y = (y_coord + y_line) as usize % SCREEN_HEIGHT; + // Get our pixel's index for our 1D screen array + let idx = x + SCREEN_WIDTH * y; + // Check if we're about to flip the pixel and set + flipped |= self.screen[idx]; + self.screen[idx] ^= true; + } + } + } + // Populate VF register + if flipped { + self.v_reg[0xF] = 1; + } else { + self.v_reg[0xF] = 0; + } + } + // EX9E: skip if key pressed + (0xE, _, 9, 0xE) => { + let x = digit2 as usize; + let vx = self.v_reg[x]; + let key = self.keys[vx as usize]; + if key { + self.pc += 2; + } + } + // EXA1: skip if key not pressed + (0xE, _, 0xA, 1) => { + let x = digit2 as usize; + let vx = self.v_reg[x]; + let key = self.keys[vx as usize]; + if !key { + self.pc += 2; + } + } + // VX = DT + (0xF, _, 0, 7) => { + let x = digit2 as usize; + self.v_reg[x] = self.dt; + } + // FX0A: wait for key press + (0xF, _, 0, 0xA) => { + let x = digit2 as usize; + let mut pressed = false; + for i in 0..self.keys.len() { + if self.keys[i] { + self.v_reg[x] = i as u8; + pressed = true; + break; + } + } + if !pressed { + // Redo opcode + self.pc -= 2; + } + } + // FX15: DT = VX + (0xF, _, 1, 5) => { + let x = digit2 as usize; + self.dt = self.v_reg[x]; + } + // FX18: ST = VX + (0xF, _, 1, 8) => { + let x = digit2 as usize; + self.st = self.v_reg[x]; + } + // FX1E: I += VX + (0xF, _, 1, 0xE) => { + let x = digit2 as usize; + let vx = self.v_reg[x] as u16; + self.i_reg = self.i_reg.wrapping_add(vx); + } + // FX29: set I to font address + (0xF, _, 2, 9) => { + let x = digit2 as usize; + let c = self.v_reg[x] as u16; + self.i_reg = c * 5; + } + // FX33: | = BCD of VX + // storing the binary-coded decimal of + // a number stored in the V registers into the I register + (0xF, _, 3, 3) => { + let x = digit2 as usize; + let vx = self.v_reg[x] as f32; + // Fetch the hundreds digit by dividing by 100 and tossing the decimal + let hundreds = (vx / 100.0).floor() as u8; + // Fetch the tens digit by dividing by 10, tossing the ones digit and the decimal + let tens = ((vx / 10.0) % 10.0).floor() as u8; + // Fetch the ones digit by tossing the hundreds and the tens + let ones = (vx % 10.0) as u8; + self.ram[self.i_reg as usize] = hundreds; + self.ram[(self.i_reg + 1) as usize] = tens; + self.ram[(self.i_reg + 2) as usize] = ones; + } + // FX55: Store V0 - VX into I + (0xF, _, 5, 5) => { + let x = digit2 as usize; + let i = self.i_reg as usize; + for idx in 0..=x { + self.ram[i + idx] = self.v_reg[idx]; + } + } + // FX65: load I into V0 - VX + (0xF, _, 6, 5) => { + let x = digit2 as usize; + let i = self.i_reg as usize; + for idx in 0..=x { + self.v_reg[idx] = self.ram[i + idx]; + } + } + (_, _, _, _) => unimplemented!("Unimplemented opcode: {}", op), + } + } + + // grab the instruction we are about to execute (opcode) + pub fn fetch(&mut self) -> u16 { + let higher_byte = self.ram[self.pc as usize] as u16; + let lower_byte = self.ram[(self.pc + 1) as usize] as u16; + let op = (higher_byte << 8) | lower_byte; + self.pc += 2; + op + } + + pub fn tick_timers(&mut self) { + if self.dt > 0 { + self.dt -= 1; + } + if self.st > 0 { + if self.st == 1 { + // BEEP + } + self.st -= 1; + } + } + + pub fn get_display(&self) -> &[bool] { + &self.screen + } + + pub fn keypress(&mut self, idx: usize, pressed: bool) { + self.keys[idx] = pressed; + } + + pub fn load(&mut self, data: &[u8]) { + let start = START_ADDR as usize; + let end = (START_ADDR as usize) + data.len(); + self.ram[start..end].copy_from_slice(data); + } +} diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1 @@ +/target diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock new file mode 100644 index 0000000..2ee8a09 --- /dev/null +++ b/desktop/Cargo.lock @@ -0,0 +1,206 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chip8_core" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "desktop" +version = "0.1.0" +dependencies = [ + "chip8_core", + "sdl2", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "sdl2" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" +dependencies = [ + "cfg-if", + "libc", + "version-compare", +] + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml new file mode 100644 index 0000000..c49da0d --- /dev/null +++ b/desktop/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "desktop" +version = "0.1.0" +edition = "2024" + +[dependencies] +chip8_core = { path = "../chip8_core" } +sdl2 = "0.37.0" diff --git a/desktop/c8games/15PUZZLE b/desktop/c8games/15PUZZLE new file mode 100644 index 0000000..3ef0bc8 Binary files /dev/null and b/desktop/c8games/15PUZZLE differ diff --git a/desktop/c8games/BLINKY b/desktop/c8games/BLINKY new file mode 100644 index 0000000..235cf98 Binary files /dev/null and b/desktop/c8games/BLINKY differ diff --git a/desktop/c8games/BLITZ b/desktop/c8games/BLITZ new file mode 100644 index 0000000..0d2effa Binary files /dev/null and b/desktop/c8games/BLITZ differ diff --git a/desktop/c8games/BRIX b/desktop/c8games/BRIX new file mode 100644 index 0000000..ad639d9 Binary files /dev/null and b/desktop/c8games/BRIX differ diff --git a/desktop/c8games/CONNECT4 b/desktop/c8games/CONNECT4 new file mode 100644 index 0000000..200a67a Binary files /dev/null and b/desktop/c8games/CONNECT4 differ diff --git a/desktop/c8games/GUESS b/desktop/c8games/GUESS new file mode 100644 index 0000000..36f783d Binary files /dev/null and b/desktop/c8games/GUESS differ diff --git a/desktop/c8games/HIDDEN b/desktop/c8games/HIDDEN new file mode 100644 index 0000000..bd6b18d Binary files /dev/null and b/desktop/c8games/HIDDEN differ diff --git a/desktop/c8games/INVADERS b/desktop/c8games/INVADERS new file mode 100644 index 0000000..f7db5f5 Binary files /dev/null and b/desktop/c8games/INVADERS differ diff --git a/desktop/c8games/KALEID b/desktop/c8games/KALEID new file mode 100644 index 0000000..a1bc7cc Binary files /dev/null and b/desktop/c8games/KALEID differ diff --git a/desktop/c8games/MAZE b/desktop/c8games/MAZE new file mode 100644 index 0000000..152ae7d Binary files /dev/null and b/desktop/c8games/MAZE differ diff --git a/desktop/c8games/MERLIN b/desktop/c8games/MERLIN new file mode 100644 index 0000000..747843a Binary files /dev/null and b/desktop/c8games/MERLIN differ diff --git a/desktop/c8games/MISSILE b/desktop/c8games/MISSILE new file mode 100644 index 0000000..310e2de Binary files /dev/null and b/desktop/c8games/MISSILE differ diff --git a/desktop/c8games/PONG b/desktop/c8games/PONG new file mode 100644 index 0000000..e371e91 Binary files /dev/null and b/desktop/c8games/PONG differ diff --git a/desktop/c8games/PONG2 b/desktop/c8games/PONG2 new file mode 100644 index 0000000..295ce91 Binary files /dev/null and b/desktop/c8games/PONG2 differ diff --git a/desktop/c8games/PUZZLE b/desktop/c8games/PUZZLE new file mode 100644 index 0000000..bec7af0 Binary files /dev/null and b/desktop/c8games/PUZZLE differ diff --git a/desktop/c8games/SYZYGY b/desktop/c8games/SYZYGY new file mode 100644 index 0000000..b0653ef Binary files /dev/null and b/desktop/c8games/SYZYGY differ diff --git a/desktop/c8games/TANK b/desktop/c8games/TANK new file mode 100644 index 0000000..ca4bbab Binary files /dev/null and b/desktop/c8games/TANK differ diff --git a/desktop/c8games/TETRIS b/desktop/c8games/TETRIS new file mode 100644 index 0000000..9f5e087 Binary files /dev/null and b/desktop/c8games/TETRIS differ diff --git a/desktop/c8games/TICTAC b/desktop/c8games/TICTAC new file mode 100644 index 0000000..4d4bc99 Binary files /dev/null and b/desktop/c8games/TICTAC differ diff --git a/desktop/c8games/UFO b/desktop/c8games/UFO new file mode 100644 index 0000000..7fa5a15 Binary files /dev/null and b/desktop/c8games/UFO differ diff --git a/desktop/c8games/VBRIX b/desktop/c8games/VBRIX new file mode 100644 index 0000000..07f4006 Binary files /dev/null and b/desktop/c8games/VBRIX differ diff --git a/desktop/c8games/VERS b/desktop/c8games/VERS new file mode 100644 index 0000000..b0fe240 Binary files /dev/null and b/desktop/c8games/VERS differ diff --git a/desktop/c8games/WIPEOFF b/desktop/c8games/WIPEOFF new file mode 100644 index 0000000..2d5e513 Binary files /dev/null and b/desktop/c8games/WIPEOFF differ diff --git a/desktop/macos.sh b/desktop/macos.sh new file mode 100755 index 0000000..2fe6505 --- /dev/null +++ b/desktop/macos.sh @@ -0,0 +1,4 @@ +#!/bin/sh +export LIBRARY_PATH="$LIBRARY_PATH:/opt/homebrew/lib" + +cargo run $1 \ No newline at end of file diff --git a/desktop/src/main.rs b/desktop/src/main.rs new file mode 100644 index 0000000..7c12b4f --- /dev/null +++ b/desktop/src/main.rs @@ -0,0 +1,133 @@ +use chip8_core::*; + +use std::env; +use std::fs::File; +use std::io::Read; + +use sdl2::event::Event; +use sdl2::keyboard::Keycode; +use sdl2::pixels::Color; +use sdl2::rect::Rect; +use sdl2::render::Canvas; +use sdl2::video::Window; + +const SCALE: u32 = 15; +const WINDOW_WIDTH: u32 = (SCREEN_WIDTH as u32) * SCALE; +const WINDOW_HEIGHT: u32 = (SCREEN_HEIGHT as u32) * SCALE; + +const TICKS_PER_FRAME: usize = 10; + +fn main() { + let args: Vec<_> = env::args().collect(); + if args.len() != 2 { + println!("Usage: cargo run path/to/game"); + return; + } + + // Setup SDL + let sdl_context = sdl2::init().unwrap(); + let video_subsystem = sdl_context.video().unwrap(); + let window = video_subsystem + .window("Chip-8 Emulator", WINDOW_WIDTH, WINDOW_HEIGHT) + .position_centered() + .opengl() + .build() + .unwrap(); + let mut canvas = window.into_canvas().present_vsync().build().unwrap(); + canvas.clear(); + canvas.present(); + let mut event_pump = sdl_context.event_pump().unwrap(); + let mut chip8 = Emu::new(); + let mut rom = File::open(&args[1]).expect("Unable to open file"); + let mut buffer = Vec::new(); + rom.read_to_end(&mut buffer).unwrap(); + chip8.load(&buffer); + 'gameloop: loop { + for evt in event_pump.poll_iter() { + match evt { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => { + break 'gameloop; + } + Event::KeyDown { + keycode: Some(key), .. + } => { + if let Some(k) = key2btn(key) { + chip8.keypress(k, true); + } + } + Event::KeyUp { + keycode: Some(key), .. + } => { + if let Some(k) = key2btn(key) { + chip8.keypress(k, false); + } + } + _ => (), + } + } + for _ in 0..TICKS_PER_FRAME { + chip8.tick(); + } + chip8.tick_timers(); + draw_screen(&chip8, &mut canvas); + } +} + +fn draw_screen(emu: &Emu, canvas: &mut Canvas) { + // Clear canvas as black + canvas.set_draw_color(Color::RGB(0, 0, 0)); + canvas.clear(); + let screen_buf = emu.get_display(); + // Now set draw color to white, iterate through each point and see if it should be drawn + canvas.set_draw_color(Color::RGB(255, 255, 255)); + for (i, pixel) in screen_buf.iter().enumerate() { + if *pixel { + // Convert our 1D array's index into a 2D (x,y) position + let x = (i % SCREEN_WIDTH) as u32; + let y = (i / SCREEN_WIDTH) as u32; + // Draw rectangle at (x,y), scaled up by our SCALE value + let rect = Rect::new((x * SCALE) as i32, (y * SCALE) as i32, SCALE, SCALE); + canvas.fill_rect(rect).unwrap(); + } + } + canvas.present(); +} + +/* + Keyboard Chip-8 + +---+---+---+---+ +---+---+---+---+ + | 1 | 2 | 3 | 4 | | 1 | 2 | 3 | C | + +---+---+---+---+ +---+---+---+---+ + | Q | W | E | R | | 4 | 5 | 6 | D | + +---+---+---+---+ => +---+---+---+---+ + | A | S | D | F | | 7 | 8 | 9 | E | + +---+---+---+---+ +---+---+---+---+ + | Z | X | C | V | | A | 0 | B | F | + +---+---+---+---+ +---+---+---+---+ +*/ + +fn key2btn(key: Keycode) -> Option { + match key { + Keycode::Num1 => Some(0x1), + Keycode::Num2 => Some(0x2), + Keycode::Num3 => Some(0x3), + Keycode::Num4 => Some(0xC), + Keycode::Q => Some(0x4), + Keycode::W => Some(0x5), + Keycode::E => Some(0x6), + Keycode::R => Some(0xD), + Keycode::A => Some(0x7), + Keycode::S => Some(0x8), + Keycode::D => Some(0x9), + Keycode::F => Some(0xE), + Keycode::Z => Some(0xA), + Keycode::X => Some(0x0), + Keycode::C => Some(0xB), + Keycode::V => Some(0xF), + _ => None, + } +}