initial commit
This commit is contained in:
commit
546800f344
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# chip8_rust
|
||||
|
||||
Full chip8 implementation in Rust.
|
||||
For learning purposes only.
|
1
chip8_core/.gitignore
vendored
Normal file
1
chip8_core/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
157
chip8_core/Cargo.lock
generated
Normal file
157
chip8_core/Cargo.lock
generated
Normal file
@ -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",
|
||||
]
|
7
chip8_core/Cargo.toml
Normal file
7
chip8_core/Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "chip8_core"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rand = "0.9.1"
|
445
chip8_core/src/lib.rs
Normal file
445
chip8_core/src/lib.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
1
desktop/.gitignore
vendored
Normal file
1
desktop/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
206
desktop/Cargo.lock
generated
Normal file
206
desktop/Cargo.lock
generated
Normal file
@ -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",
|
||||
]
|
8
desktop/Cargo.toml
Normal file
8
desktop/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "desktop"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
chip8_core = { path = "../chip8_core" }
|
||||
sdl2 = "0.37.0"
|
BIN
desktop/c8games/15PUZZLE
Normal file
BIN
desktop/c8games/15PUZZLE
Normal file
Binary file not shown.
BIN
desktop/c8games/BLINKY
Normal file
BIN
desktop/c8games/BLINKY
Normal file
Binary file not shown.
BIN
desktop/c8games/BLITZ
Normal file
BIN
desktop/c8games/BLITZ
Normal file
Binary file not shown.
BIN
desktop/c8games/BRIX
Normal file
BIN
desktop/c8games/BRIX
Normal file
Binary file not shown.
BIN
desktop/c8games/CONNECT4
Normal file
BIN
desktop/c8games/CONNECT4
Normal file
Binary file not shown.
BIN
desktop/c8games/GUESS
Normal file
BIN
desktop/c8games/GUESS
Normal file
Binary file not shown.
BIN
desktop/c8games/HIDDEN
Normal file
BIN
desktop/c8games/HIDDEN
Normal file
Binary file not shown.
BIN
desktop/c8games/INVADERS
Normal file
BIN
desktop/c8games/INVADERS
Normal file
Binary file not shown.
BIN
desktop/c8games/KALEID
Normal file
BIN
desktop/c8games/KALEID
Normal file
Binary file not shown.
BIN
desktop/c8games/MAZE
Normal file
BIN
desktop/c8games/MAZE
Normal file
Binary file not shown.
BIN
desktop/c8games/MERLIN
Normal file
BIN
desktop/c8games/MERLIN
Normal file
Binary file not shown.
BIN
desktop/c8games/MISSILE
Normal file
BIN
desktop/c8games/MISSILE
Normal file
Binary file not shown.
BIN
desktop/c8games/PONG
Normal file
BIN
desktop/c8games/PONG
Normal file
Binary file not shown.
BIN
desktop/c8games/PONG2
Normal file
BIN
desktop/c8games/PONG2
Normal file
Binary file not shown.
BIN
desktop/c8games/PUZZLE
Normal file
BIN
desktop/c8games/PUZZLE
Normal file
Binary file not shown.
BIN
desktop/c8games/SYZYGY
Normal file
BIN
desktop/c8games/SYZYGY
Normal file
Binary file not shown.
BIN
desktop/c8games/TANK
Normal file
BIN
desktop/c8games/TANK
Normal file
Binary file not shown.
BIN
desktop/c8games/TETRIS
Normal file
BIN
desktop/c8games/TETRIS
Normal file
Binary file not shown.
BIN
desktop/c8games/TICTAC
Normal file
BIN
desktop/c8games/TICTAC
Normal file
Binary file not shown.
BIN
desktop/c8games/UFO
Normal file
BIN
desktop/c8games/UFO
Normal file
Binary file not shown.
BIN
desktop/c8games/VBRIX
Normal file
BIN
desktop/c8games/VBRIX
Normal file
Binary file not shown.
BIN
desktop/c8games/VERS
Normal file
BIN
desktop/c8games/VERS
Normal file
Binary file not shown.
BIN
desktop/c8games/WIPEOFF
Normal file
BIN
desktop/c8games/WIPEOFF
Normal file
Binary file not shown.
4
desktop/macos.sh
Executable file
4
desktop/macos.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
export LIBRARY_PATH="$LIBRARY_PATH:/opt/homebrew/lib"
|
||||
|
||||
cargo run $1
|
133
desktop/src/main.rs
Normal file
133
desktop/src/main.rs
Normal file
@ -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<Window>) {
|
||||
// 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<usize> {
|
||||
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,
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user