Initial Commit

This commit adds most of the logic for the processor

Things that are missing:
+ Most sdl2 interactions
+ Cartridge loading
+ Main loop for the game
This commit is contained in:
Marco Thomas
2021-03-10 14:38:13 +01:00
commit f61b3ed74d
8 changed files with 671 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

138
Cargo.lock generated Normal file
View File

@@ -0,0 +1,138 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "c_vec"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a318911dce53b5f1ca6539c44f5342c632269f0fa7ea3e35f32458c27a7c30"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chip8-rs"
version = "0.1.0"
dependencies = [
"rand",
"sdl2",
]
[[package]]
name = "getrandom"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a"
[[package]]
name = "ppv-lite86"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "rand"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
dependencies = [
"rand_core",
]
[[package]]
name = "sdl2"
version = "0.34.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcbb85f4211627a7291c83434d6bbfa723e28dcaa53c7606087e3c61929e4b9c"
dependencies = [
"bitflags",
"c_vec",
"lazy_static",
"libc",
"sdl2-sys",
]
[[package]]
name = "sdl2-sys"
version = "0.34.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d81feded049b9c14eceb4a4f6d596a98cebbd59abdba949c5552a015466d33"
dependencies = [
"cfg-if 0.1.10",
"libc",
"version-compare",
]
[[package]]
name = "version-compare"
version = "0.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"

15
Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "chip8-rs"
version = "0.1.0"
authors = ["Marco Thomas <mail@marco-thomas.net>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.8.3"
[dependencies.sdl2]
version = "0.34"
default-features = false
features = ["ttf","image","gfx","mixer"]

21
README.md Normal file
View File

@@ -0,0 +1,21 @@
chip8-rs
========
Yet another chip8 emulator written in rust \o/
## TODO
Expand on this README
## System Dependencies
`sdl2` together with its dependencies are required to be installed on your hostmachine.
On arch-based distros you can use
`sudo pacman -Syu sdl2 sdl2_gfx sdl2_image sdl2_mixer sdl2_ttf`
to install them.
## Credits
For this project I used the help from the following sites:
+ [Chip-8 Technical Reference](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#3.0)
+ [How to write an emulator (CHIP-8 interpreter)](http://www.multigesture.net/articles/how-to-write-an-emulator-chip-8-interpreter/)

18
src/fontset.rs Normal file
View File

@@ -0,0 +1,18 @@
pub (crate) 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
];

0
src/handle_input.rs Normal file
View File

27
src/main.rs Normal file
View File

@@ -0,0 +1,27 @@
extern crate rand;
extern crate sdl2;
use std::env;
mod processor;
mod fontset;
use crate::processor::Processor;
fn main() {
let cartridge = env::args().nth(1);
match cartridge {
Some(_) => println!("Found a cartridge file! Trying to load..."),
None => {
println!("No cartridge file found! Exiting!");
return;
}
};
let mut processor = Processor::new();
// load cartridge file
processor.start();
}

451
src/processor.rs Normal file
View File

@@ -0,0 +1,451 @@
extern crate sdl2;
use rand::Rng;
use crate::fontset::FONT;
const MEMORY_SIZE: usize = 4096;
const GAME_ENTRY: usize = 0x200; // most games load into 0x200
const SCREEN_WIDTH: usize = 64;
const SCREEN_HEIGHT: usize = 32;
const OPCODE_SIZE: usize = 2;
pub struct Processor {
memory: [u8; MEMORY_SIZE],
register: [u8; 16],
index: usize, // used to store memory addresses
pc: usize, // programm counter
screen: [[u8; SCREEN_WIDTH]; SCREEN_HEIGHT],
draw_flag: bool, // redraw screen if true
delay_timer: usize,
sound_timer: usize,
stack: [usize; 16],
sp: usize, // stack pointer
key: [bool; 16],
waiting_for_key: bool,
waiting_key_location: usize,
}
impl Processor {
pub fn new() -> Self {
let mut mem = [0; MEMORY_SIZE];
// load font
for (pos, &val) in FONT.iter().enumerate() {
mem[pos] = val;
}
Processor {
memory: mem,
register: [0; 16],
index: 0,
pc: GAME_ENTRY,
screen: [[0; SCREEN_WIDTH]; SCREEN_HEIGHT],
draw_flag: false,
delay_timer: 0,
sound_timer: 0,
stack: [0; 16],
sp: 0,
key: [false; 16],
waiting_for_key: false,
waiting_key_location: 0,
}
}
// TODO: cartridge needed
pub fn start(&mut self) {
let sdl_ctx = sdl2::init().unwrap();
loop {
// get keyinput using sdl2
// emulate one cycle
self.cycle();
// draw to screen using sdl2
if self.draw_flag {
}
// play sound using sdl2
// add delay
}
}
pub fn cycle(&mut self) {
// reset
self.draw_flag = false;
// opcode FX0A holds the program, until a key is pressed
if self.waiting_for_key {
for (pos, &val) in self.key.iter().enumerate() {
if self.key[pos] {
self.waiting_for_key = false;
self.register[self.waiting_key_location] = val as u8;
break;
}
}
}
else {
// decr both timers every cycle
if self.delay_timer > 0 {
self.delay_timer -= 1;
}
if self.sound_timer > 0 {
self.sound_timer -= 1;
}
// execute current opcode
let opcode = self.fetch_opcode();
self.decode_opcode(opcode);
}
}
pub fn load_game(&mut self, game: &[u8]) {
// load game
for (pos, &val) in game.iter().enumerate() {
let position = GAME_ENTRY + pos;
if position < MEMORY_SIZE { // don't go above mem limit
self.memory[position] = val;
}
else {
break;
}
}
}
pub fn fetch_opcode(&self) -> u16 {
// final opcode consists of 2 bytes
(self.memory[self.pc] as u16) << 8 | (self.memory[self.pc + 1] as u16)
}
pub fn decode_opcode(&mut self, opcode: u16) {
// values from http://devernay.free.fr/hacks/chip8/C8TECH10.HTM#3.0
// n or nibble - A 4-bit value, the lowest 4 bits of the instruction
let nibbles = (
(opcode & 0xF000 >> 12) as usize,
(opcode & 0x0F00 >> 8) as usize,
(opcode & 0x00F0 >> 4) as usize,
(opcode & 0x000F) as usize
);
// nnn or addr - A 12-bit value, the lowest 12 bits of the instruction
let nnn = (opcode & 0x0FFF) as usize;
// n or nibble - A 4-bit value, the lowest 4 bits of the instruction
let n = nibbles.3 as usize;
// x - A 4-bit value, the lower 4 bits of the high byte of the instruction
let x = nibbles.1 as usize;
// y - A 4-bit value, the upper 4 bits of the low byte of the instruction
let y = nibbles.2 as usize;
//kk or byte - An 8-bit value, the lowest 8 bits of the instruction
let kk = (opcode & 0x00FF) as u8;
// match nibbles to opcodes => run funtion
match nibbles {
(0x00, 0x00, 0x0e, 0x00) => self.code_00e0(),
(0x00, 0x00, 0x0e, 0x0e) => self.code_00ee(),
(0x01, _, _, _) => self.code_1nnn(nnn),
(0x02, _, _, _) => self.code_2nnn(nnn),
(0x03, _, _, _) => self.code_3xkk(x, kk),
(0x04, _, _, _) => self.code_4xkk(x, kk),
(0x05, _, _, 0x00) => self.code_5xy0(x, y),
(0x06, _, _, _) => self.code_6xkk(x, kk),
(0x07, _, _, _) => self.code_7xkk(x, kk),
(0x08, _, _, 0x00) => self.code_8xy0(x, y),
(0x08, _, _, 0x01) => self.code_8xy1(x, y),
(0x08, _, _, 0x02) => self.code_8xy2(x, y),
(0x08, _, _, 0x03) => self.code_8xy3(x, y),
(0x08, _, _, 0x04) => self.code_8xy4(x, y),
(0x08, _, _, 0x05) => self.code_8xy5(x, y),
(0x08, _, _, 0x06) => self.code_8xy6(x, y),
(0x08, _, _, 0x07) => self.code_8xy7(x, y),
(0x08, _, _, 0x0e) => self.code_8xye(x, y),
(0x09, _, _, 0x00) => self.code_9xy0(x, y),
(0x0a, _, _, _) => self.code_annn(nnn),
(0x0b, _, _, _) => self.code_bnnn(nnn),
(0x0c, _, _, _) => self.code_cxkk(x, kk),
(0x0d, _, _, _) => self.code_dxyn(x, y, n),
(0x0e, _, 0x09, 0x0e) => self.code_ex9e(x),
(0x0e, _, 0x0a, 0x01) => self.code_exa1(x),
(0x0f, _, 0x00, 0x07) => self.code_fx07(x),
(0x0f, _, 0x00, 0x0a) => self.code_fx0a(x),
(0x0f, _, 0x01, 0x05) => self.code_fx15(x),
(0x0f, _, 0x01, 0x08) => self.code_fx18(x),
(0x0f, _, 0x01, 0x0e) => self.code_fx1e(x),
(0x0f, _, 0x02, 0x09) => self.code_fx29(x),
(0x0f, _, 0x03, 0x03) => self.code_fx33(x),
(0x0f, _, 0x05, 0x05) => self.code_fx55(x),
(0x0f, _, 0x06, 0x05) => self.code_fx65(x),
_ => self.pc += OPCODE_SIZE
};
}
// Clear screen
fn code_00e0(&mut self) {
self.screen = [[0; SCREEN_WIDTH]; SCREEN_HEIGHT];
self.pc += OPCODE_SIZE;
}
// Return from subroutine
fn code_00ee(&mut self) {
self.sp -= 1;
self.pc = self.stack[self.sp];
}
// Jump to location nnn
fn code_1nnn(&mut self, nnn: usize) {
self.pc = nnn;
}
// Call subroutine at nnn
fn code_2nnn(&mut self, nnn: usize) {
self.sp += 1;
self.stack[self.sp] = self.pc;
self.pc = nnn;
}
// Skip next instruction if Vx = kk
fn code_3xkk(&mut self, x: usize, kk: u8) {
if self.register[x] == kk {
self.pc += 2 * OPCODE_SIZE;
}
else {
self.pc += OPCODE_SIZE;
}
}
// Skip next instruction if Vx != kk
fn code_4xkk(&mut self, x: usize, kk: u8) {
if self.register[x] != kk {
self.pc += 2 * OPCODE_SIZE;
}
else {
self.pc += OPCODE_SIZE;
}
}
// Skip next instruction if Vx = Vy
fn code_5xy0(&mut self, x: usize, y: usize) {
if self.register[x] == self.register[y] {
self.pc += 2 * OPCODE_SIZE;
}
else {
self.pc += OPCODE_SIZE;
}
}
// Set Vx = kk
fn code_6xkk(&mut self, x: usize, kk: u8) {
self.register[x] = kk;
self.pc += OPCODE_SIZE;
}
// Set Vx = Vx + kk
fn code_7xkk(&mut self, x: usize, kk: u8) {
self.register[x] = self.register[x] + kk;
self.pc += OPCODE_SIZE;
}
// Set Vx = Vy
fn code_8xy0(&mut self, x: usize, y: usize) {
self.register[x] = self.register[y];
self.pc += OPCODE_SIZE;
}
// Set Vx = Vx OR Vy
fn code_8xy1(&mut self, x: usize, y: usize) {
self.register[x] = self.register[x] | self.register[y];
self.pc += OPCODE_SIZE;
}
// Set Vx = Vx AND Vy
fn code_8xy2(&mut self, x: usize, y: usize) {
self.register[x] = self.register[x] & self.register[y];
self.pc += OPCODE_SIZE;
}
// Set Vx = Vx XOR Vy
fn code_8xy3(&mut self, x: usize, y: usize) {
self.register[x] = self.register[x] ^ self.register[y];
self.pc += OPCODE_SIZE;
}
// Set Vx = Vx + Vy, set VF = carry
fn code_8xy4(&mut self, x: usize, y: usize) {
let result = (self.register[x]) as usize + (self.register[y]) as usize;
self.register[x] = result as u8; // write back lowest 8bit
self.register[0x0f] = (result > 255) as u8; // set carry flag
self.pc += OPCODE_SIZE;
}
// Set Vx = Vx - Vy, set VF = NOT borrow
fn code_8xy5(&mut self, x: usize, y: usize) {
let x_val = self.register[x];
let y_val = self.register[y];
self.register[0x0f] = (x_val > y_val) as u8;
self.register[x] = (x_val - y_val) as u8;
self.pc += OPCODE_SIZE;
}
// Set Vx = Vx SHR 1
fn code_8xy6(&mut self, x: usize, _y: usize) {
self.register[0x0f] = self.register[x] & 1; // set if least significant bit == 1
self.register[x] = self.register[x] / 2;
self.pc += OPCODE_SIZE;
}
// Set Vx = Vy - Vx, set VF = NOT borrow
fn code_8xy7(&mut self, x: usize, y: usize) {
self.register[0x0f] = (self.register[y] > self.register[x]) as u8;
self.register[x] = self.register[y] - self.register[x];
self.pc += OPCODE_SIZE;
}
// Set Vx = Vx SHL 1
fn code_8xye(&mut self, x: usize, _y: usize) {
self.register[0x0f] = (self.register[x] & 0b10000000) >> 7;
self.register[x] = self.register[x] * 2;
self.pc += OPCODE_SIZE;
}
// Skip next instruction if Vx != Vy
fn code_9xy0(&mut self, x: usize, y: usize) {
if self.register[x] != self.register[y] {
self.pc += 2 * OPCODE_SIZE;
}
else {
self.pc += OPCODE_SIZE;
}
}
// Set I = nnn
fn code_annn(&mut self, nnn: usize) {
self.index = nnn;
self.pc += OPCODE_SIZE;
}
// Jump to location nnn + V0
fn code_bnnn(&mut self, nnn: usize) {
self.pc = nnn + self.register[0x00] as usize;
}
// Set Vx = random byte AND kk
fn code_cxkk(&mut self, x: usize, kk: u8) {
let rng = rand::thread_rng().gen_range(0..255);
self.register[x] = kk & rng;
self.pc += OPCODE_SIZE;
}
// Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision.
fn code_dxyn(&mut self, x: usize, y: usize, n: usize) {
// TODO
self.register[0x0f] = 0;
for byte in 0..n {
// get y coord, which we want to draw -> use modulo, so we don't overlap
let y = (self.register[y] as usize + byte) % SCREEN_HEIGHT;
for bit in 0..8 {
// get x coord, just as above
let x = (self.register[x] as usize + bit) % SCREEN_WIDTH;
// bit hack to get every bit in a row
let pixel_to_draw = (self.memory[self.index + byte] >> (7 - bit)) & 1;
// check if we will overwrite an existing pixel
self.register[0x0f] = self.register[0x0f] | (pixel_to_draw & self.screen[x][y]);
self.screen[x][y] = self.screen[x][y] ^ pixel_to_draw;
}
}
self.draw_flag = true;
self.pc += OPCODE_SIZE;
}
// Skip next instruction if key with the value of Vx is pressed
fn code_ex9e(&mut self, x: usize) {
if self.key[self.register[x] as usize] {
self.pc += 2 * OPCODE_SIZE;
}
else {
self.pc += OPCODE_SIZE;
}
}
// Skip next instruction if key with the value of Vx is not pressed
fn code_exa1(&mut self, x: usize) {
if !(self.key[self.register[x] as usize]) {
self.pc += 2 * OPCODE_SIZE;
}
else {
self.pc += OPCODE_SIZE;
}
}
// Set Vx = delay timer value
fn code_fx07(&mut self, x: usize) {
self.register[x] = self.delay_timer as u8;
self.pc = OPCODE_SIZE;
}
// Wait for a key press, store the value of the key in Vx
fn code_fx0a(&mut self, x: usize) {
self.waiting_for_key = true;
self.waiting_key_location = x; // safe for later
self.pc += OPCODE_SIZE;
}
// Set delay timer = Vx
fn code_fx15(&mut self, x: usize) {
self.delay_timer = self.register[x] as usize;
self.pc += OPCODE_SIZE;
}
// Set sound timer = Vx
fn code_fx18(&mut self, x: usize) {
self.sound_timer = self.register[x] as usize;
self.pc += OPCODE_SIZE;
}
// Set I = I + Vx
fn code_fx1e(&mut self, x: usize) {
self.index += self.register[x] as usize;
self.pc += OPCODE_SIZE;
}
// Set I = location of sprite for digit Vx
fn code_fx29(&mut self, x: usize) {
let sprite_name = self.register[x] as usize;
let mem_position = sprite_name * 5; // single sprite is 5byte
self.index = mem_position;
self.pc += OPCODE_SIZE;
}
// Store BCD representation of Vx in memory locations I, I+1, and I+2
fn code_fx33(&mut self, x: usize) {
let val = self.register[x];
self.memory[self.index] = (val / 100) as u8;
self.memory[self.index + 1] = ((val % 100) / 10) as u8;
self.memory[self.index + 2] = (val % 10) as u8;
self.pc += OPCODE_SIZE;
}
// Store registers V0 through Vx in memory starting at location I
fn code_fx55(&mut self, x: usize) {
// TODO offset correct?
for reg_i in 0..x {
self.memory[self.index + reg_i] = self.register[reg_i];
}
self.pc += OPCODE_SIZE;
}
// Read registers V0 through Vx from memory starting at location I
fn code_fx65(&mut self, x: usize) {
for reg_i in 0..x {
self.register[reg_i] = self.memory[self.index + reg_i];
}
self.pc += OPCODE_SIZE;
}
}