use rand::{Rng, rng, rngs::ThreadRng};
use std::{fmt, io, time::Duration};
pub const CPU_TICK: Duration = Duration::from_micros(1430);
pub const TIMER_TICK: Duration = Duration::from_micros(16667);
pub const SCREEN_WIDTH: usize = 64;
pub const SCREEN_HEIGHT: usize = 32;
pub const SCREEN_AREA: usize = SCREEN_WIDTH * SCREEN_HEIGHT;
const FONTSET_SIZE: usize = 80;
const FONT_ADDR: u16 = 0x050;
const FONTSET: [u8; FONTSET_SIZE] = [
0xF0, 0x90, 0x90, 0x90, 0xF0, 0x20, 0x60, 0x20, 0x20, 0x70, 0xF0, 0x10, 0xF0, 0x80, 0xF0, 0xF0, 0x10, 0xF0, 0x10, 0xF0, 0x90, 0x90, 0xF0, 0x10, 0x10, 0xF0, 0x80, 0xF0, 0x10, 0xF0, 0xF0, 0x80, 0xF0, 0x90, 0xF0, 0xF0, 0x10, 0x20, 0x40, 0x40, 0xF0, 0x90, 0xF0, 0x90, 0xF0, 0xF0, 0x90, 0xF0, 0x10, 0xF0, 0xF0, 0x90, 0xF0, 0x90, 0x90, 0xE0, 0x90, 0xE0, 0x90, 0xE0, 0xF0, 0x80, 0x80, 0x80, 0xF0, 0xE0, 0x90, 0x90, 0x90, 0xE0, 0xF0, 0x80, 0xF0, 0x80, 0xF0, 0xF0, 0x80, 0xF0, 0x80, 0x80, ];
const RAM_SIZE: usize = 4096;
const NUM_REGS: usize = 16;
const STACK_SIZE: usize = 16;
const NUM_KEYS: usize = 16;
const VF: usize = 15;
const START_ADDR: u16 = 0x200;
#[derive(Debug)]
struct Opcode(u8, u8, u8, u8);
#[derive(Debug)]
pub struct Oxid8 {
pc: u16, ram: [u8; RAM_SIZE], screen: [bool; SCREEN_AREA], v_reg: [u8; NUM_REGS], i_reg: u16, sp: u16, stack: [u16; STACK_SIZE], keys: [bool; NUM_KEYS], stored_key: Option<usize>, dt: u8, st: u8, rng: ThreadRng, }
impl Opcode {
fn new(byte1: u8, byte2: u8) -> Self {
Self(
(byte1 & 0xF0) >> 4,
byte1 & 0x0F,
(byte2 & 0xF0) >> 4,
byte2 & 0x0F,
)
}
fn full(&self) -> u16 {
(self.0 as u16) << 12 | (self.1 as u16) << 8 | (self.2 as u16) << 4 | (self.3 as u16)
}
fn nnn(&self) -> u16 {
(self.1 as u16) << 8 | (self.2 as u16) << 4 | (self.3 as u16)
}
fn n(&self) -> u8 {
self.3
}
fn x(&self) -> u8 {
self.1
}
fn y(&self) -> u8 {
self.2
}
fn kk(&self) -> u8 {
self.2 << 4 | self.3
}
}
impl fmt::Display for Opcode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {}, {}, {})", self.0, self.1, self.2, self.3)
}
}
impl Oxid8 {
pub fn new() -> Self {
Oxid8::default()
}
pub fn reset(&mut self) {
*self = Oxid8::default();
}
pub fn next_frame(&mut self) -> Result<(), String> {
for _ in 0..10 {
self.run_cycle()?;
}
self.dec_timers();
Ok(())
}
pub fn run_cycle(&mut self) -> Result<(), String> {
let opcode = Opcode::new(
self.ram[self.pc as usize], self.ram[self.pc as usize + 1], );
let pc_at_err = self.pc;
self.pc += 2;
let invalid = || -> Result<(), String> {
Err(format!(
"Invalid Instruction: {:04X} at {}",
opcode.full(),
pc_at_err,
))
};
match opcode.0 {
0x0 => match opcode.kk() {
0xE0 => self.cls(),
0xEE => self.ret(),
_ => invalid()?,
},
0x1 => self.jp_nnn(opcode.nnn()),
0x2 => self.call(opcode.nnn()),
0x3 => self.se_xkk(opcode.x() as usize, opcode.kk()),
0x4 => self.sne_xkk(opcode.x() as usize, opcode.kk()),
0x5 => self.se_xy(opcode.x() as usize, opcode.y() as usize),
0x6 => self.ld_xkk(opcode.x() as usize, opcode.kk()),
0x7 => self.add_xkk(opcode.x() as usize, opcode.kk()),
0x8 => match opcode.n() {
0x0 => self.ld_xy(opcode.x() as usize, opcode.y() as usize),
0x1 => self.or(opcode.x() as usize, opcode.y() as usize),
0x2 => self.and(opcode.x() as usize, opcode.y() as usize),
0x3 => self.xor(opcode.x() as usize, opcode.y() as usize),
0x4 => self.add_xy(opcode.x() as usize, opcode.y() as usize),
0x5 => self.sub_xy(opcode.x() as usize, opcode.y() as usize),
0x6 => self.shr(opcode.x() as usize, opcode.y() as usize),
0x7 => self.subn_xy(opcode.x() as usize, opcode.y() as usize),
0xE => self.shl(opcode.x() as usize, opcode.y() as usize),
_ => invalid()?,
},
0x9 => self.sne_xy(opcode.x() as usize, opcode.y() as usize),
0xA => self.ld_innn(opcode.nnn()),
0xB => self.jp_0nnn(opcode.nnn()),
0xC => self.rnd(opcode.x() as usize, opcode.kk()),
0xD => {
self.drw(
opcode.x() as usize, opcode.y() as usize, opcode.n(), );
}
0xE => match opcode.kk() {
0x9E => self.skp(opcode.x() as usize),
0xA1 => self.sknp(opcode.x() as usize),
_ => invalid()?,
},
0xF => match opcode.kk() {
0x07 => self.ld_xdt(opcode.x() as usize),
0x0A => self.ld_xk(opcode.x() as usize),
0x15 => self.ld_dtx(opcode.x() as usize),
0x18 => self.ld_stx(opcode.x() as usize),
0x1E => self.add_ix(opcode.x() as usize),
0x29 => self.ld_fx(opcode.x() as usize),
0x33 => self.ld_bx(opcode.x() as usize),
0x55 => self.ld_ix(opcode.x() as usize),
0x65 => self.ld_xi(opcode.x() as usize),
_ => invalid()?,
},
_ => invalid()?,
}
Ok(())
}
pub fn dec_timers(&mut self) {
if self.dt > 0 {
self.dt -= 1;
}
if self.st > 0 {
self.st -= 1;
}
}
#[must_use]
pub fn sound(&self) -> bool {
self.st != 0
}
pub fn set_key(&mut self, k: usize, val: bool) {
self.keys[k] = val;
}
pub fn clear_keys(&mut self) {
self.keys = [false; NUM_KEYS];
}
#[must_use]
pub fn screen_ref(&self) -> &[bool; SCREEN_AREA] {
&self.screen
}
pub fn load_font(&mut self) {
self.ram[FONT_ADDR as usize..(FONT_ADDR as usize + FONTSET_SIZE)] .copy_from_slice(&FONTSET);
}
pub fn load_rom(&mut self, path: impl AsRef<std::path::Path>) -> io::Result<()> {
use std::fs;
let rom_data: Vec<u8> = fs::read(path)?;
self.load_rom_bytes(rom_data.as_slice())
}
pub fn load_rom_bytes(&mut self, rom_data: &[u8]) -> io::Result<()> {
let len = rom_data.len();
if len > (RAM_SIZE - START_ADDR as usize) {
return Err(io::Error::new(
io::ErrorKind::FileTooLarge,
format!("ROM too large: {}", len),
));
}
self.ram[START_ADDR as usize..(START_ADDR as usize + len)] .copy_from_slice(rom_data);
Ok(())
}
fn push(&mut self, val: u16) {
match self.sp as usize {
0..STACK_SIZE => {
self.stack[self.sp as usize] = val;
self.sp += 1;
}
_ => panic!("ERROR::Emulator Stack Overflow"),
};
}
fn pop(&mut self) -> u16 {
match self.sp as usize {
1..=STACK_SIZE => {
self.sp -= 1;
self.stack[self.sp as usize]
}
_ => panic!("ERROR::Emulator Stack Underflow"),
}
}
}
impl Default for Oxid8 {
fn default() -> Self {
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],
stored_key: None,
dt: 0,
st: 0,
rng: rng(),
}
}
}
impl Oxid8 {
fn cls(&mut self) {
self.screen = [false; SCREEN_WIDTH * SCREEN_HEIGHT];
}
fn ret(&mut self) {
self.pc = self.pop();
}
fn jp_nnn(&mut self, nnn: u16) {
self.pc = nnn;
}
fn call(&mut self, nnn: u16) {
self.push(self.pc);
self.pc = nnn;
}
fn se_xkk(&mut self, x: usize, kk: u8) {
if self.v_reg[x] == kk {
self.pc += 2;
}
}
fn sne_xkk(&mut self, x: usize, kk: u8) {
if self.v_reg[x] != kk {
self.pc += 2;
}
}
fn se_xy(&mut self, x: usize, y: usize) {
if self.v_reg[x] == self.v_reg[y] {
self.pc += 2;
}
}
fn ld_xkk(&mut self, x: usize, kk: u8) {
self.v_reg[x] = kk;
}
fn add_xkk(&mut self, x: usize, kk: u8) {
self.v_reg[x] = self.v_reg[x].wrapping_add(kk);
}
fn ld_xy(&mut self, x: usize, y: usize) {
self.v_reg[x] = self.v_reg[y];
}
fn or(&mut self, x: usize, y: usize) {
self.v_reg[x] |= self.v_reg[y];
}
fn and(&mut self, x: usize, y: usize) {
self.v_reg[x] &= self.v_reg[y];
}
fn xor(&mut self, x: usize, y: usize) {
self.v_reg[x] ^= self.v_reg[y];
}
fn add_xy(&mut self, x: usize, y: usize) {
let (vx, carry) = self.v_reg[x].overflowing_add(self.v_reg[y]);
self.v_reg[x] = vx;
self.v_reg[VF] = carry as u8;
}
fn sub_xy(&mut self, x: usize, y: usize) {
let (vx, borrow) = self.v_reg[x].overflowing_sub(self.v_reg[y]);
self.v_reg[x] = vx;
self.v_reg[VF] = !borrow as u8;
}
fn shr(&mut self, x: usize, _y: usize) {
let vx = self.v_reg[x];
self.v_reg[x] = vx >> 1;
self.v_reg[VF] = vx & 1;
}
fn subn_xy(&mut self, x: usize, y: usize) {
let (vx, borrow) = self.v_reg[y].overflowing_sub(self.v_reg[x]);
self.v_reg[x] = vx;
self.v_reg[VF] = !borrow as u8;
}
fn shl(&mut self, x: usize, _y: usize) {
let vx = self.v_reg[x];
self.v_reg[x] = vx << 1;
self.v_reg[VF] = (vx >> 7) & 1;
}
fn sne_xy(&mut self, x: usize, y: usize) {
if self.v_reg[x] != self.v_reg[y] {
self.pc += 2;
}
}
fn ld_innn(&mut self, nnn: u16) {
self.i_reg = nnn;
}
fn jp_0nnn(&mut self, nnn: u16) {
self.pc = nnn + self.v_reg[0] as u16;
}
fn rnd(&mut self, x: usize, kk: u8) {
self.v_reg[x] = self.rng.random_range(0..=0xFF) as u8 & kk;
}
fn drw(&mut self, x: usize, y: usize, n: u8) {
let (x, y) = (
self.v_reg[x] as usize % SCREEN_WIDTH, self.v_reg[y] as usize % SCREEN_HEIGHT, );
self.v_reg[VF] = 0; let start_pixel: usize = (y * SCREEN_WIDTH) + x;
let start_addr: usize = self.i_reg as usize;
for i in 0..n as usize {
if y + i >= SCREEN_HEIGHT {
break; }
let pixel_posn: usize = start_pixel + (SCREEN_WIDTH * i);
let sprite_row: u8 = self.ram[start_addr + i];
for j in 0..8 {
if x + j >= SCREEN_WIDTH {
break; }
let ref mut pixel_ref = self.screen[pixel_posn + j];
let old_pixel = *pixel_ref;
let sprite_pixel = (sprite_row >> (0x7 - j)) & 0x1;
*pixel_ref ^= sprite_pixel != 0;
if !(*pixel_ref) && old_pixel {
self.v_reg[VF] = 1; }
}
}
}
fn skp(&mut self, x: usize) {
if self.keys[self.v_reg[x] as usize] {
self.pc += 2;
}
}
fn sknp(&mut self, x: usize) {
if !self.keys[self.v_reg[x] as usize] {
self.pc += 2;
}
}
fn ld_xdt(&mut self, x: usize) {
self.v_reg[x] = self.dt;
}
fn ld_xk(&mut self, x: usize) {
match self.stored_key {
Some(k) => {
if !self.keys[k] {
self.v_reg[x] = k as u8;
self.stored_key = None;
return;
}
}
None => {
for (k, &pressed) in self.keys.iter().enumerate() {
if pressed {
self.stored_key = Some(k);
break;
}
}
}
}
self.pc -= 2;
}
fn ld_dtx(&mut self, x: usize) {
self.dt = self.v_reg[x];
}
fn ld_stx(&mut self, x: usize) {
self.st = self.v_reg[x];
}
fn add_ix(&mut self, x: usize) {
self.i_reg = self.i_reg.wrapping_add(self.v_reg[x] as u16);
}
fn ld_fx(&mut self, x: usize) {
self.i_reg = FONT_ADDR + (self.v_reg[x] as u16 * 5);
}
fn ld_bx(&mut self, x: usize) {
let i = self.i_reg as usize;
let v = self.v_reg[x];
self.ram[i] = (v / 100) % 10;
self.ram[i + 1] = (v / 10) % 10;
self.ram[i + 2] = v % 10;
}
fn ld_ix(&mut self, x: usize) {
let i = self.i_reg as usize;
self.ram[i..=(i + x)].copy_from_slice(&self.v_reg[0..=x]);
}
fn ld_xi(&mut self, x: usize) {
let i = self.i_reg as usize;
self.v_reg[0..=x].copy_from_slice(&self.ram[i..=(i + x)]);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test() {
let a: [u8; 5] = [255, 155, 100, 55, 5];
let i: u16 = 0;
assert_eq!(255, a[i as usize]);
assert_eq!(155, a[i as usize + 1]);
}
#[test]
fn opcode_new() {
let opcode = Opcode::new(0x12, 0x34);
assert_eq!(opcode.0, 0x1);
assert_eq!(opcode.1, 0x2);
assert_eq!(opcode.2, 0x3);
assert_eq!(opcode.3, 0x4);
}
#[test]
fn opcode_decode() {
let opcode = Opcode::new(0x12, 0x34);
assert_eq!(opcode.full(), 0x1234);
assert_eq!(opcode.nnn(), 0x234);
assert_eq!(opcode.n(), 0x4);
assert_eq!(opcode.x(), 0x2);
assert_eq!(opcode.y(), 0x3);
assert_eq!(opcode.kk(), 0x34);
}
#[test]
fn invalid_opcode() {
let mut emu = Oxid8::new();
emu.ram[START_ADDR as usize] = 0xFF;
emu.ram[START_ADDR as usize + 1] = 0xFF;
assert!(emu.run_cycle().is_err_and(|msg| msg
== format!(
"Invalid Instruction: FFFF at {}", START_ADDR )))
}
#[test]
fn push_pop() {
let mut emu = Oxid8::new();
assert_eq!(emu.sp, 0); emu.push(1); assert_eq!(emu.sp, 1); assert_eq!(emu.stack[0], 1); assert_eq!(emu.pop(), 1); assert_eq!(emu.sp, 0); }
#[test]
#[should_panic(expected = "Stack Overflow")]
fn push_panic() {
let mut emu = Oxid8::new();
for _ in 0..=STACK_SIZE {
emu.push(1);
}
}
#[test]
#[should_panic(expected = "Stack Underflow")]
fn pop_panic() {
let mut emu = Oxid8::new();
emu.pop();
}
#[test]
fn load_font() {
let mut emu = Oxid8::new();
emu.load_font();
assert_eq!(
emu.ram[FONT_ADDR as usize..(FONT_ADDR as usize + FONTSET_SIZE)],
FONTSET
);
}
#[test]
fn draw_basic() {
let sprite = [
0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x81, ];
let screen = [
true, false, false, false, false, false, false, true, false, true, false, false, false, false, true, false, false, false, true, false, false, true, false, false, false, false, false, true, true, false, false, false, false, false, false, true, true, false, false, false, false, false, true, false, false, true, false, false, false, true, false, false, false, false, true, false, true, false, false, false, false, false, false, true, false, true, false, false, false, false, true, false, false, false, true, false, false, true, false, false, false, false, false, true, true, false, false, false, false, false, false, true, true, false, false, false, false, false, true, false, false, true, false, false, false, true, false, false, false, false, true, false, true, false, false, false, false, false, false, true, ];
let mut emu = Oxid8::new();
emu.i_reg = START_ADDR;
let start = START_ADDR as usize;
emu.ram[start..start + sprite.len()].copy_from_slice(&sprite);
emu.drw(0, 0, sprite.len() as u8);
for i in 0..15 {
let offset1: usize = i * SCREEN_WIDTH;
let offset2: usize = i * 8;
assert_eq!(
emu.screen[offset1 + 0..offset1 + 8],
screen[offset2 + 0..offset2 + 8]
);
}
}
}