cpu.rs: Make quirks individually configurable

This commit is contained in:
John 2023-03-27 17:27:55 -05:00
parent 85956504d7
commit 784845b6f5
4 changed files with 335 additions and 239 deletions

View File

@ -1,54 +0,0 @@
//! Connects a BusConnectible to the Bus
use super::BusConnectible;
use std::{
fmt::{Display, Formatter, Result},
ops::Range,
};
/// BusDevice performs address translation for BusConnectibles.
/// It is an implementation detail of Bus.connect()
#[derive(Debug)]
pub struct BusDevice {
pub name: String,
pub range: Range<u16>,
device: Box<dyn BusConnectible>,
}
impl BusDevice {
pub fn new(name: &str, range: Range<u16>, device: Box<dyn BusConnectible>) -> Self {
BusDevice {
name: name.to_string(),
range,
device,
}
}
fn translate_address(&self, addr: u16) -> Option<u16> {
let addr = addr.wrapping_sub(self.range.start);
if addr < self.range.end {
Some(addr)
} else {
None
}
}
}
impl BusConnectible for BusDevice {
fn read_at(&self, addr: u16) -> Option<u8> {
self.device.read_at(self.translate_address(addr)?)
}
fn write_to(&mut self, addr: u16, data: u8) {
if let Some(addr) = self.translate_address(addr) {
self.device.write_to(addr, data);
}
}
fn get_mut(&mut self, addr: u16) -> Option<&mut u8> {
return self.device.get_mut(addr);
}
}
impl Display for BusDevice {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
writeln!(f, "{} [{:04x?}]:\n{}", self.name, self.range, self.device)
}
}

View File

@ -1,52 +0,0 @@
//! Iterators for working with Busses
use super::{Bus, Read};
use std::ops::Range;
pub trait IterMut<'a> {
type Item;
fn next(&'a mut self) -> Option<&'a mut Self::Item>;
}
pub trait IntoIterMut<'a> {
type Item;
type IntoIter;
fn into_iter(self) -> Self::IntoIter;
}
#[derive(Debug)]
pub struct BusIterator<'a> {
range: Range<u16>,
addr: u16,
bus: &'a Bus,
}
impl<'a> BusIterator<'a> {
/// Creates a new BusIterator with a specified range
pub fn new(range: Range<u16>, bus: &'a Bus) -> BusIterator<'a> {
BusIterator {
addr: range.start,
range,
bus,
}
}
pub fn range(mut self, range: Range<u16>) -> Self {
self.range = range;
self
}
}
impl<'a> Iterator for BusIterator<'a> {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
let mut res = None;
if self.range.contains(&self.addr) {
res = Some(self.bus.read(self.addr));
self.addr += 1;
}
res
}
}

View File

@ -15,12 +15,58 @@ type Reg = usize;
type Adr = u16; type Adr = u16;
type Nib = u8; type Nib = u8;
/// Controls the authenticity behavior of the CPU on a granular level.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Quirks {
/// Binary ops in `8xy`(`1`, `2`, `3`) should set vF to 0
pub bin_ops: bool,
/// Shift ops in `8xy`(`6`, `E`) should source from vY instead of vX
pub shift: bool,
/// Draw operations should pause execution until the next timer tick
pub draw_wait: bool,
/// DMA instructions `Fx55`/`Fx65` should change I to I + x + 1
pub dma_inc: bool,
/// Indexed jump instructions should go to ADR + v[N] where N is high nibble of adr
pub stupid_jumps: bool,
}
impl From<bool> for Quirks {
fn from(value: bool) -> Self {
if value {
Quirks {
bin_ops: true,
shift: true,
draw_wait: true,
dma_inc: true,
stupid_jumps: false,
}
} else {
Quirks {
bin_ops: false,
shift: false,
draw_wait: false,
dma_inc: false,
stupid_jumps: false,
}
}
}
}
impl Default for Quirks {
fn default() -> Self {
Self::from(false)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ControlFlags { pub struct ControlFlags {
pub debug: bool, pub debug: bool,
pub pause: bool, pub pause: bool,
pub keypause: bool, pub keypause: bool,
pub authentic: bool, pub vbi_wait: bool,
pub lastkey: Option<usize>,
pub quirks: Quirks,
pub monotonic: Option<usize>,
} }
impl ControlFlags { impl ControlFlags {
@ -37,8 +83,10 @@ pub struct Keys {
keys: [bool; 16], keys: [bool; 16],
} }
/// Represents the internal state of the CPU interpreter
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct CPU { pub struct CPU {
pub flags: ControlFlags,
// memory map info // memory map info
screen: Adr, screen: Adr,
font: Adr, font: Adr,
@ -47,12 +95,12 @@ pub struct CPU {
sp: Adr, sp: Adr,
i: Adr, i: Adr,
v: [u8; 16], v: [u8; 16],
delay: u8, delay: f64,
sound: u8, sound: f64,
// I/O // I/O
pub keys: [bool; 16], keys: [bool; 16],
pub flags: ControlFlags,
// Execution data // Execution data
timer: Instant,
cycle: usize, cycle: usize,
breakpoints: Vec<Adr>, breakpoints: Vec<Adr>,
disassembler: Disassemble, disassembler: Disassemble,
@ -60,36 +108,6 @@ pub struct CPU {
// public interface // public interface
impl CPU { impl CPU {
/// Press keys (where `keys` is a bitmap of the keys [F-0])
pub fn press(&mut self, key: usize) {
if (0..16).contains(&key) {
self.keys[key] = true;
self.flags.keypause = false;
}
}
/// Release all keys
pub fn release(&mut self) {
for key in &mut self.keys {
*key = false;
}
}
/// Set a general purpose register in the CPU
/// # Examples
/// ```rust
/// # use chirp::prelude::*;
/// // Create a new CPU, and set v4 to 0x41
/// let mut cpu = CPU::default();
/// cpu.set_gpr(0x4, 0x41);
/// // Dump the CPU registers
/// cpu.dump();
/// ```
pub fn set_gpr(&mut self, gpr: Reg, value: u8) {
if let Some(gpr) = self.v.get_mut(gpr) {
*gpr = value;
}
}
/// Constructs a new CPU, taking all configurable parameters /// Constructs a new CPU, taking all configurable parameters
/// # Examples /// # Examples
/// ```rust /// ```rust
@ -111,14 +129,42 @@ impl CPU {
font, font,
pc, pc,
sp, sp,
i: 0,
v: [0; 16],
delay: 0,
sound: 0,
cycle: 0,
keys: [false; 16],
breakpoints, breakpoints,
flags, flags,
..Default::default()
}
}
/// Press a key
pub fn press(&mut self, key: usize) {
if (0..16).contains(&key) {
self.keys[key] = true;
}
}
/// Release a key
pub fn release(&mut self, key: usize) {
if (0..16).contains(&key) {
self.keys[key] = false;
if self.flags.keypause {
self.flags.lastkey = Some(key);
}
self.flags.keypause = false;
}
}
/// Set a general purpose register in the CPU
/// # Examples
/// ```rust
/// # use chirp::prelude::*;
/// // Create a new CPU, and set v4 to 0x41
/// let mut cpu = CPU::default();
/// cpu.set_gpr(0x4, 0x41);
/// // Dump the CPU registers
/// cpu.dump();
/// ```
pub fn set_gpr(&mut self, gpr: Reg, value: u8) {
if let Some(gpr) = self.v.get_mut(gpr) {
*gpr = value;
} }
} }
@ -127,10 +173,15 @@ impl CPU {
self.pc self.pc
} }
pub fn cycle(&self) -> usize {
self.cycle
}
/// Soft resets the CPU, releasing keypause and reinitializing the program counter to 0x200 /// Soft resets the CPU, releasing keypause and reinitializing the program counter to 0x200
pub fn soft_reset(&mut self) { pub fn soft_reset(&mut self) {
self.pc = 0x200; self.pc = 0x200;
self.flags.keypause = false; self.flags.keypause = false;
self.flags.vbi_wait = false;
} }
/// Set a breakpoint /// Set a breakpoint
@ -162,38 +213,64 @@ impl CPU {
pub fn singlestep(&mut self, bus: &mut Bus) -> &mut Self { pub fn singlestep(&mut self, bus: &mut Bus) -> &mut Self {
self.flags.pause = false; self.flags.pause = false;
self.tick(bus); self.tick(bus);
self.flags.vbi_wait = false;
self.flags.pause = true; self.flags.pause = true;
self self
} }
/// Unpauses the emulator for `steps` ticks /// Unpauses the emulator for `steps` ticks
/// Ticks the timers every `rate` ticks /// Ticks the timers every `rate` ticks
pub fn multistep(&mut self, bus: &mut Bus, steps: usize, rate: usize) -> &mut Self { pub fn multistep(&mut self, bus: &mut Bus, steps: usize) -> &mut Self {
for _ in 0..steps { for _ in 0..steps {
self.tick(bus); self.tick(bus);
if rate != 0 && self.cycle % rate == rate - 1 { self.vertical_blank();
self.tick_timer();
}
} }
self self
} }
/// Ticks the delay and sound timers /// Signals the start of a vertical blank
pub fn tick_timer(&mut self) -> &mut Self { ///
/// - Ticks the sound and delay timers
/// - Disables framepause
pub fn vertical_blank(&mut self) -> &mut Self {
if self.flags.pause { if self.flags.pause {
return self; return self;
} }
self.delay = self.delay.saturating_sub(1); // Use a monotonic counter when testing
self.sound = self.sound.saturating_sub(1); if let Some(speed) = self.flags.monotonic {
if self.flags.vbi_wait {
self.flags.vbi_wait = !(self.cycle % speed == 0);
}
self.delay -= 1.0 / speed as f64;
self.sound -= 1.0 / speed as f64;
return self;
};
let time = self.timer.elapsed().as_secs_f64() * 60.0;
self.timer = Instant::now();
if time > 1.0 {
self.flags.vbi_wait = false;
}
if self.delay > 0.0 {
self.delay -= time;
}
if self.sound > 0.0 {
self.sound -= time;
}
self self
} }
/// Runs a single instruction /// Runs a single instruction
pub fn tick(&mut self, bus: &mut Bus) -> &mut Self { pub fn tick(&mut self, bus: &mut Bus) -> &mut Self {
// Do nothing if paused // Do nothing if paused
if self.flags.pause || self.flags.keypause { if self.flags.pause || self.flags.vbi_wait || self.flags.keypause {
// always tick in test mode
if self.flags.monotonic.is_some() {
self.cycle += 1;
}
return self; return self;
} }
let time = Instant::now(); self.cycle += 1;
// fetch opcode // fetch opcode
let opcode: u16 = bus.read(self.pc); let opcode: u16 = bus.read(self.pc);
let pc = self.pc; let pc = self.pc;
@ -318,9 +395,9 @@ impl CPU {
0x65 => self.load_dma(x, bus), 0x65 => self.load_dma(x, bus),
_ => self.unimplemented(opcode), _ => self.unimplemented(opcode),
}, },
_ => unimplemented!("Extracted nibble from byte, got >nibble?"), _ => unreachable!("Extracted nibble from byte, got >nibble?"),
} }
let elapsed = time.elapsed(); let elapsed = self.timer.elapsed();
// Print opcode disassembly: // Print opcode disassembly:
if self.flags.debug { if self.flags.debug {
std::println!( std::println!(
@ -331,7 +408,6 @@ impl CPU {
elapsed.dimmed() elapsed.dimmed()
); );
} }
self.cycle += 1;
// process breakpoints // process breakpoints
if self.breakpoints.contains(&self.pc) { if self.breakpoints.contains(&self.pc) {
self.flags.pause = true; self.flags.pause = true;
@ -346,8 +422,7 @@ impl CPU {
self.pc, self.pc,
self.sp, self.sp,
self.i, self.i,
self self.v
.v
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, gpr)| { .map(|(i, gpr)| {
@ -384,21 +459,22 @@ impl Default for CPU {
sp: 0xefe, sp: 0xefe,
i: 0, i: 0,
v: [0; 16], v: [0; 16],
delay: 0, delay: 0.0,
sound: 0, sound: 0.0,
cycle: 0, cycle: 0,
keys: [false; 16], keys: [false; 16],
flags: ControlFlags { flags: ControlFlags {
debug: true, debug: true,
..Default::default() ..Default::default()
}, },
timer: Instant::now(),
breakpoints: vec![], breakpoints: vec![],
disassembler: Disassemble::default(), disassembler: Disassemble::default(),
} }
} }
} }
// Below this point, comments may be duplicated per impl' block, // Below this point, comments may be duplicated per impl' block,
// since some opcodes handle multiple instructions. // since some opcodes handle multiple instructions.
// | 0aaa | Issues a "System call" (ML routine) // | 0aaa | Issues a "System call" (ML routine)
@ -529,46 +605,40 @@ impl CPU {
// | 8xyE | X = X << 1 | // | 8xyE | X = X << 1 |
impl CPU { impl CPU {
/// 8xy0: Loads the value of y into x /// 8xy0: Loads the value of y into x
///
/// # Authenticity
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
#[inline] #[inline]
fn load(&mut self, x: Reg, y: Reg) { fn load(&mut self, x: Reg, y: Reg) {
self.v[x] = self.v[y]; self.v[x] = self.v[y];
if self.flags.authentic {
self.v[0xf] = 0;
}
} }
/// 8xy1: Performs bitwise or of vX and vY, and stores the result in vX /// 8xy1: Performs bitwise or of vX and vY, and stores the result in vX
/// ///
/// # Authenticity /// # Quirk
/// The original chip-8 interpreter will clobber vF for any 8-series instruction /// The original chip-8 interpreter will clobber vF for any 8-series instruction
#[inline] #[inline]
fn or(&mut self, x: Reg, y: Reg) { fn or(&mut self, x: Reg, y: Reg) {
self.v[x] |= self.v[y]; self.v[x] |= self.v[y];
if self.flags.authentic { if self.flags.quirks.bin_ops {
self.v[0xf] = 0; self.v[0xf] = 0;
} }
} }
/// 8xy2: Performs bitwise and of vX and vY, and stores the result in vX /// 8xy2: Performs bitwise and of vX and vY, and stores the result in vX
/// ///
/// # Authenticity /// # Quirk
/// The original chip-8 interpreter will clobber vF for any 8-series instruction /// The original chip-8 interpreter will clobber vF for any 8-series instruction
#[inline] #[inline]
fn and(&mut self, x: Reg, y: Reg) { fn and(&mut self, x: Reg, y: Reg) {
self.v[x] &= self.v[y]; self.v[x] &= self.v[y];
if self.flags.authentic { if self.flags.quirks.bin_ops {
self.v[0xf] = 0; self.v[0xf] = 0;
} }
} }
/// 8xy3: Performs bitwise xor of vX and vY, and stores the result in vX /// 8xy3: Performs bitwise xor of vX and vY, and stores the result in vX
/// ///
/// # Authenticity /// # Quirk
/// The original chip-8 interpreter will clobber vF for any 8-series instruction /// The original chip-8 interpreter will clobber vF for any 8-series instruction
#[inline] #[inline]
fn xor(&mut self, x: Reg, y: Reg) { fn xor(&mut self, x: Reg, y: Reg) {
self.v[x] ^= self.v[y]; self.v[x] ^= self.v[y];
if self.flags.authentic { if self.flags.quirks.bin_ops {
self.v[0xf] = 0; self.v[0xf] = 0;
} }
} }
@ -587,13 +657,12 @@ impl CPU {
self.v[0xf] = (!carry).into(); self.v[0xf] = (!carry).into();
} }
/// 8xy6: Performs bitwise right shift of vX /// 8xy6: Performs bitwise right shift of vX
/// ///
/// # Authenticity /// # Quirk
/// On the original chip-8 interpreter, this would perform the operation on vY /// On the original chip-8 interpreter, this shifts vY and stores the result in vX
/// and store the result in vX. This behavior was left out, for now.
#[inline] #[inline]
fn shift_right(&mut self, x: Reg, y: Reg) { fn shift_right(&mut self, x: Reg, y: Reg) {
let src: Reg = if self.flags.authentic {y} else {x}; let src: Reg = if self.flags.quirks.shift { y } else { x };
let shift_out = self.v[src] & 1; let shift_out = self.v[src] & 1;
self.v[x] = self.v[src] >> 1; self.v[x] = self.v[src] >> 1;
self.v[0xf] = shift_out; self.v[0xf] = shift_out;
@ -606,13 +675,13 @@ impl CPU {
self.v[0xf] = (!carry).into(); self.v[0xf] = (!carry).into();
} }
/// 8X_E: Performs bitwise left shift of vX /// 8X_E: Performs bitwise left shift of vX
/// ///
/// # Authenticity /// # Quirk
/// On the original chip-8 interpreter, this would perform the operation on vY /// On the original chip-8 interpreter, this would perform the operation on vY
/// and store the result in vX. This behavior was left out, for now. /// and store the result in vX. This behavior was left out, for now.
#[inline] #[inline]
fn shift_left(&mut self, x: Reg, y: Reg) { fn shift_left(&mut self, x: Reg, y: Reg) {
let src: Reg = if self.flags.authentic {y} else {x}; let src: Reg = if self.flags.quirks.shift { y } else { x };
let shift_out: u8 = self.v[src] >> 7; let shift_out: u8 = self.v[src] >> 7;
self.v[x] = self.v[src] << 1; self.v[x] = self.v[src] << 1;
self.v[0xf] = shift_out; self.v[0xf] = shift_out;
@ -646,9 +715,17 @@ impl CPU {
// | Baaa | Jump to &adr + v0 // | Baaa | Jump to &adr + v0
impl CPU { impl CPU {
/// Badr: Jump to &adr + v0 /// Badr: Jump to &adr + v0
///
/// Quirk:
/// On the Super-Chip, this does stupid shit
#[inline] #[inline]
fn jump_indexed(&mut self, a: Adr) { fn jump_indexed(&mut self, a: Adr) {
self.pc = a.wrapping_add(self.v[0] as Adr); let reg = if self.flags.quirks.stupid_jumps {
a as usize >> 8
} else {
0
};
self.pc = a.wrapping_add(self.v[reg] as Adr);
} }
} }
@ -664,9 +741,14 @@ impl CPU {
// | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY) // | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY)
impl CPU { impl CPU {
/// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY) /// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY)
#[inline] ///
/// # Quirk
/// On the original chip-8 interpreter, this will wait for a VBI
fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
let (x, y) = (self.v[x] as u16, self.v[y] as u16); let (x, y) = (self.v[x] as u16 % 64, self.v[y] as u16 % 32);
if self.flags.quirks.draw_wait {
self.flags.vbi_wait = true;
}
self.v[0xf] = 0; self.v[0xf] = 0;
for byte in 0..n as u16 { for byte in 0..n as u16 {
if y + byte > 32 { if y + byte > 32 {
@ -736,19 +818,15 @@ impl CPU {
/// ``` /// ```
#[inline] #[inline]
fn load_delay_timer(&mut self, x: Reg) { fn load_delay_timer(&mut self, x: Reg) {
self.v[x] = self.delay; self.v[x] = self.delay as u8;
} }
/// Fx0A: Wait for key, then vX = K /// Fx0A: Wait for key, then vX = K
#[inline] #[inline]
fn wait_for_key(&mut self, x: Reg) { fn wait_for_key(&mut self, x: Reg) {
let mut pressed = false; if let Some(key) = self.flags.lastkey {
for bit in 0..16 { self.v[x] = key as u8;
if self.keys[bit] { self.flags.lastkey = None;
self.v[x] = bit as u8; } else {
pressed = true;
}
}
if !pressed {
self.pc = self.pc.wrapping_sub(2); self.pc = self.pc.wrapping_sub(2);
self.flags.keypause = true; self.flags.keypause = true;
} }
@ -759,7 +837,7 @@ impl CPU {
/// ``` /// ```
#[inline] #[inline]
fn store_delay_timer(&mut self, x: Reg) { fn store_delay_timer(&mut self, x: Reg) {
self.delay = self.v[x]; self.delay = self.v[x] as f64;
} }
/// Fx18: Load vX into ST /// Fx18: Load vX into ST
/// ```py /// ```py
@ -767,7 +845,7 @@ impl CPU {
/// ``` /// ```
#[inline] #[inline]
fn store_sound_timer(&mut self, x: Reg) { fn store_sound_timer(&mut self, x: Reg) {
self.sound = self.v[x]; self.sound = self.v[x] as f64;
} }
/// Fx1e: Add vX to I, /// Fx1e: Add vX to I,
/// ```py /// ```py
@ -794,8 +872,8 @@ impl CPU {
bus.write(self.i, x / 100 % 10); bus.write(self.i, x / 100 % 10);
} }
/// Fx55: DMA Stor from I to registers 0..X /// Fx55: DMA Stor from I to registers 0..X
/// ///
/// # Authenticity /// # Quirk
/// The original chip-8 interpreter uses I to directly index memory, /// The original chip-8 interpreter uses I to directly index memory,
/// with the side effect of leaving I as I+X+1 after the transfer is done. /// with the side effect of leaving I as I+X+1 after the transfer is done.
#[inline] #[inline]
@ -809,13 +887,13 @@ impl CPU {
{ {
*value = self.v[reg] *value = self.v[reg]
} }
if self.flags.authentic { if self.flags.quirks.dma_inc {
self.i += x as Adr + 1; self.i += x as Adr + 1;
} }
} }
/// Fx65: DMA Load from I to registers 0..X /// Fx65: DMA Load from I to registers 0..X
/// ///
/// # Authenticity /// # Quirk
/// The original chip-8 interpreter uses I to directly index memory, /// The original chip-8 interpreter uses I to directly index memory,
/// with the side effect of leaving I as I+X+1 after the transfer is done. /// with the side effect of leaving I as I+X+1 after the transfer is done.
#[inline] #[inline]
@ -829,7 +907,7 @@ impl CPU {
{ {
self.v[reg] = *value; self.v[reg] = *value;
} }
if self.flags.authentic { if self.flags.quirks.dma_inc {
self.i += x as Adr + 1; self.i += x as Adr + 1;
} }
} }

View File

@ -14,6 +14,7 @@ fn setup_environment() -> (CPU, Bus) {
flags: ControlFlags { flags: ControlFlags {
debug: true, debug: true,
pause: false, pause: false,
monotonic: Some(8),
..Default::default() ..Default::default()
}, },
..CPU::default() ..CPU::default()
@ -29,6 +30,10 @@ fn setup_environment() -> (CPU, Bus) {
) )
} }
fn print_screen(bytes: &[u8]) {
bus! {Screen [0..0x100] = bytes}.print_screen().unwrap()
}
/// Unused instructions /// Unused instructions
/// ///
/// TODO: Exhaustively test unused instructions /// TODO: Exhaustively test unused instructions
@ -72,7 +77,7 @@ mod sys {
let sp_orig = cpu.sp; let sp_orig = cpu.sp;
// Place the address on the stack // Place the address on the stack
bus.write(cpu.sp.wrapping_add(2), test_addr); bus.write(cpu.sp.wrapping_add(2), test_addr);
cpu.ret(&mut bus); cpu.ret(&mut bus);
// Verify the current address is the address from the stack // Verify the current address is the address from the stack
@ -251,7 +256,6 @@ mod math {
} }
/// 8xy0: Loads the value of y into x /// 8xy0: Loads the value of y into x
// TODO: Test with authentic flag set
#[test] #[test]
fn load() { fn load() {
let (mut cpu, _) = setup_environment(); let (mut cpu, _) = setup_environment();
@ -275,10 +279,11 @@ mod math {
} }
/// 8xy1: Performs bitwise or of vX and vY, and stores the result in vX /// 8xy1: Performs bitwise or of vX and vY, and stores the result in vX
// TODO: Test with authentic flag set // TODO: Test with bin_ops quirk flag set
#[test] #[test]
fn or() { fn or_inaccurate() {
let (mut cpu, _) = setup_environment(); let (mut cpu, _) = setup_environment();
cpu.flags.quirks.bin_ops = false;
for word in 0..=0xffff { for word in 0..=0xffff {
let (a, b) = (word as u8, (word >> 4) as u8); let (a, b) = (word as u8, (word >> 4) as u8);
let expected_result = a | b; let expected_result = a | b;
@ -297,10 +302,11 @@ mod math {
} }
/// 8xy2: Performs bitwise and of vX and vY, and stores the result in vX /// 8xy2: Performs bitwise and of vX and vY, and stores the result in vX
// TODO: Test with authentic flag set // TODO: Test with bin_ops quirk flag set
#[test] #[test]
fn and() { fn and_inaccurate() {
let (mut cpu, _) = setup_environment(); let (mut cpu, _) = setup_environment();
cpu.flags.quirks.bin_ops = false;
for word in 0..=0xffff { for word in 0..=0xffff {
let (a, b) = (word as u8, (word >> 4) as u8); let (a, b) = (word as u8, (word >> 4) as u8);
let expected_result = a & b; let expected_result = a & b;
@ -319,10 +325,11 @@ mod math {
} }
/// 8xy3: Performs bitwise xor of vX and vY, and stores the result in vX /// 8xy3: Performs bitwise xor of vX and vY, and stores the result in vX
// TODO: Test with authentic flag set // TODO: Test with bin_ops quirk flag set
#[test] #[test]
fn xor() { fn xor_inaccurate() {
let (mut cpu, _) = setup_environment(); let (mut cpu, _) = setup_environment();
cpu.flags.quirks.bin_ops = false;
for word in 0..=0xffff { for word in 0..=0xffff {
let (a, b) = (word as u8, (word >> 4) as u8); let (a, b) = (word as u8, (word >> 4) as u8);
let expected_result = a ^ b; let expected_result = a ^ b;
@ -521,41 +528,74 @@ mod io {
mod display { mod display {
use super::*; use super::*;
#[derive(Debug)]
struct ScreenTest { struct ScreenTest {
program: &'static [u8], program: &'static [u8],
screen: &'static [u8], screen: &'static [u8],
steps: usize, steps: usize,
rate: usize, quirks: Quirks,
} }
const SCREEN_TESTS: [ScreenTest; 4] = [ const SCREEN_TESTS: [ScreenTest; 4] = [
// Passing BC_test // Passing BC_test
// # Quirks:
// - Requires
ScreenTest { ScreenTest {
program: include_bytes!("../../chip-8/BC_test.ch8"), program: include_bytes!("../../chip-8/BC_test.ch8"),
screen: include_bytes!("tests/BC_test.ch8_197.bin"), screen: include_bytes!("tests/screens/BC_test.ch8/197.bin"),
steps: 197, steps: 250,
rate: 8, quirks: Quirks {
bin_ops: true,
shift: false,
draw_wait: true,
dma_inc: false,
stupid_jumps: false,
},
}, },
// The IBM Logo // The IBM Logo
ScreenTest { ScreenTest {
program: include_bytes!("../../chip-8/IBM Logo.ch8"), program: include_bytes!("../../chip-8/IBM Logo.ch8"),
screen: include_bytes!("tests/IBM Logo.ch8_20.bin"), screen: include_bytes!("tests/screens/IBM Logo.ch8/20.bin"),
steps: 20, steps: 20,
rate: 8, quirks: Quirks {
bin_ops: true,
shift: true,
draw_wait: false,
dma_inc: true,
stupid_jumps: false,
},
}, },
// Rule 22 cellular automata // Rule 22 cellular automata
// # Quirks
// - Requires draw_wait false, or it just takes AGES.
ScreenTest { ScreenTest {
program: include_bytes!("../../chip-8/1dcell.ch8"), program: include_bytes!("../../chip-8/1dcell.ch8"),
screen: include_bytes!("tests/1dcell.ch8_123342.bin"), screen: include_bytes!("tests/screens/1dcell.ch8/123342.bin"),
steps: 123342, steps: 123342,
rate: 8, quirks: Quirks {
bin_ops: true,
shift: true,
draw_wait: false,
dma_inc: true,
stupid_jumps: false,
},
}, },
// Rule 60 cellular automata // Rule 60 cellular automata
ScreenTest { ScreenTest {
program: include_bytes!("../../chip-8/1dcell.ch8"), program: include_bytes!("../../chip-8/1dcell.ch8"),
screen: include_bytes!("tests/1dcell.ch8_2391162.bin"), screen: include_bytes!("tests/screens/1dcell.ch8/2391162.bin"),
steps: 2391162, steps: 2391162,
rate: 8, quirks: Quirks {
bin_ops: true,
shift: true,
draw_wait: false,
dma_inc: true,
stupid_jumps: false,
},
}, },
]; ];
@ -564,11 +604,16 @@ mod io {
fn draw() { fn draw() {
for test in SCREEN_TESTS { for test in SCREEN_TESTS {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut bus) = setup_environment();
cpu.flags.quirks = test.quirks;
// Load the test program // Load the test program
bus = bus.load_region(Program, test.program); bus = bus.load_region(Program, test.program);
// Run the test program for the specified number of steps // Run the test program for the specified number of steps
cpu.multistep(&mut bus, test.steps, test.rate); while cpu.cycle() < test.steps {
cpu.multistep(&mut bus, test.steps - cpu.cycle());
}
// Compare the screen to the reference screen buffer // Compare the screen to the reference screen buffer
bus.print_screen().unwrap();
print_screen(test.screen);
assert_eq!(bus.get_region(Screen).unwrap(), test.screen); assert_eq!(bus.get_region(Screen).unwrap(), test.screen);
} }
} }
@ -605,7 +650,7 @@ mod io {
for word in 0..=0xff { for word in 0..=0xff {
for x in 0..=0xf { for x in 0..=0xf {
// set the register under test to `word` // set the register under test to `word`
cpu.delay = word; cpu.delay = word as f64;
// do the thing // do the thing
cpu.load_delay_timer(x); cpu.load_delay_timer(x);
// validate the result // validate the result
@ -625,7 +670,7 @@ mod io {
// do the thing // do the thing
cpu.store_delay_timer(x); cpu.store_delay_timer(x);
// validate the result // validate the result
assert_eq!(cpu.delay, word); assert_eq!(cpu.delay, word as f64);
} }
} }
} }
@ -641,7 +686,7 @@ mod io {
// do the thing // do the thing
cpu.store_sound_timer(x); cpu.store_sound_timer(x);
// validate the result // validate the result
assert_eq!(cpu.sound, word); assert_eq!(cpu.sound, word as f64);
} }
} }
} }
@ -744,20 +789,30 @@ mod io {
} }
/// Fx55: DMA Stor from I to registers 0..X /// Fx55: DMA Stor from I to registers 0..X
// TODO: Test with authentic flag unset // TODO: Test with dma_inc quirk set
// TODO: Test with authentic flag set #[test]
//#[test]
#[allow(dead_code)] #[allow(dead_code)]
fn dma_store() { fn dma_store() {
todo!() let (mut cpu, mut bus) = setup_environment();
// Load values into registers const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
// Perform DMA store // Load some test data into memory
// Check that let addr = 0x456;
cpu.v.as_mut_slice().write(DATA).unwrap();
for len in 0..16 {
// Perform DMA store
cpu.i = addr as u16;
cpu.store_dma(len, &mut bus);
// Check that bus grabbed the correct data
let mut bus = bus.get_mut(addr..addr + DATA.len()).unwrap();
assert_eq!(bus[0..=len], DATA[0..=len]);
assert_eq!(bus[len + 1..], [0; 16][len + 1..]);
// clear
bus.write(&[0; 16]).unwrap();
}
} }
/// Fx65: DMA Load from I to registers 0..X /// Fx65: DMA Load from I to registers 0..X
// TODO: Test with authentic flag unset // TODO: Test with dma_inc quirk set
// TODO: Test with authentic flag set
#[test] #[test]
#[allow(dead_code)] #[allow(dead_code)]
fn dma_load() { fn dma_load() {
@ -781,3 +836,72 @@ mod io {
} }
} }
} }
/// These are a series of interpreter tests using Timendus's incredible test suite
mod chip8_test_suite {
use super::*;
struct SuiteTest {
program: &'static [u8],
screen: &'static [u8],
}
fn run_screentest(test: SuiteTest, mut cpu: CPU, mut bus: Bus) {
// Load the test program
bus = bus.load_region(Program, test.program);
// The test suite always initiates a keypause on test completion
while !cpu.flags.keypause {
cpu.multistep(&mut bus, 8);
}
// Compare the screen to the reference screen buffer
bus.print_screen().unwrap();
bus! {crate::bus::Region::Screen [0..256] = test.screen}
.print_screen()
.unwrap();
assert_eq!(bus.get_region(Screen).unwrap(), test.screen);
}
#[test]
fn splash_screen() {
let (mut c, b) = setup_environment();
c.flags.quirks = true.into();
run_screentest(
SuiteTest {
program: include_bytes!("tests/chip8-test-suite/bin/chip8-test-suite.ch8"),
screen: include_bytes!("tests/screens/chip8-test-suite.ch8/splash.bin"),
},
c,
b,
)
}
#[test]
fn flags_test() {
let (mut c, mut b) = setup_environment();
c.flags.quirks = true.into();
b.write(0x1ffu16, 3u8);
run_screentest(
SuiteTest {
program: include_bytes!("tests/chip8-test-suite/bin/chip8-test-suite.ch8"),
screen: include_bytes!("tests/screens/chip8-test-suite.ch8/flags.bin"),
},
c,
b,
)
}
#[test]
fn quirks_test() {
let (mut c, mut b) = setup_environment();
c.flags.quirks = true.into();
b.write(0x1feu16, 0x0104u16);
run_screentest(
SuiteTest {
program: include_bytes!("tests/chip8-test-suite/bin/chip8-test-suite.ch8"),
screen: include_bytes!("tests/screens/chip8-test-suite.ch8/quirks.bin"),
},
c,
b,
)
}
}