From 674af62465d8129e5e9ba0c9011eee7e375516e4 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 14 Apr 2023 21:25:41 -0500 Subject: [PATCH] cpu.rs: Break into submodules --- src/bin/chirp-minifb/main.rs | 2 +- src/cpu.rs | 782 +------------------------------- src/cpu/flags.rs | 58 +++ src/cpu/instruction.rs | 844 +++++++++++++++++++++++++---------- src/cpu/mode.rs | 31 ++ src/cpu/quirks.rs | 60 +++ src/cpu/tests.rs | 4 +- src/lib.rs | 2 +- tests/chip8_test_suite.rs | 2 +- tests/integration.rs | 22 +- 10 files changed, 796 insertions(+), 1011 deletions(-) create mode 100644 src/cpu/flags.rs create mode 100644 src/cpu/mode.rs create mode 100644 src/cpu/quirks.rs diff --git a/src/bin/chirp-minifb/main.rs b/src/bin/chirp-minifb/main.rs index 56f613e..ff61e92 100644 --- a/src/bin/chirp-minifb/main.rs +++ b/src/bin/chirp-minifb/main.rs @@ -139,7 +139,7 @@ impl State { 0xefe, Dis::default(), options.breakpoints, - ControlFlags { + Flags { quirks: options.mode.unwrap_or_default().into(), debug: options.debug, pause: options.pause, diff --git a/src/cpu.rs b/src/cpu.rs index 3c4b174..bd0f905 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -13,8 +13,17 @@ pub trait Disassembler { } pub mod disassembler; +pub mod flags; +pub mod instruction; +pub mod mode; +pub mod quirks; -use self::disassembler::{Dis, Insn}; +use self::{ + disassembler::{Dis, Insn}, + flags::Flags, + mode::Mode, + quirks::Quirks, +}; use crate::{ bus::{Bus, Read, Region, Write}, error::{Error, Result}, @@ -22,153 +31,12 @@ use crate::{ use imperative_rs::InstructionSet; use owo_colors::OwoColorize; use rand::random; -use std::{str::FromStr, time::Instant}; +use std::time::Instant; type Reg = usize; type Adr = u16; type Nib = u8; -/// Selects the memory behavior of the interpreter -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Mode { - /// VIP emulation mode - #[default] - Chip8, - /// Chip-48 emulation mode - SChip, - /// XO-Chip emulation mode - XOChip, -} - -impl FromStr for Mode { - type Err = Error; - - fn from_str(s: &str) -> std::result::Result { - match s.to_lowercase().as_str() { - "chip8" | "chip-8" => Ok(Mode::Chip8), - "schip" | "superchip" => Ok(Mode::SChip), - "xo-chip" | "xochip" => Ok(Mode::XOChip), - _ => Err(Error::InvalidMode { - mode: s.to_string(), - }), - } - } -} - -/// Controls the authenticity behavior of the CPU on a granular level. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Quirks { - /// Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0 - pub bin_ops: bool, - /// Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX - pub shift: bool, - /// Draw operations shouldn't pause execution until the next timer tick - pub draw_wait: bool, - /// DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1 - pub dma_inc: bool, - /// Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`. - pub stupid_jumps: bool, -} - -impl From for Quirks { - fn from(value: bool) -> Self { - if value { - Quirks { - bin_ops: true, - shift: true, - draw_wait: true, - dma_inc: true, - stupid_jumps: true, - } - } else { - Quirks { - bin_ops: false, - shift: false, - draw_wait: false, - dma_inc: false, - stupid_jumps: false, - } - } - } -} - -impl From for Quirks { - fn from(value: Mode) -> Self { - match value { - Mode::Chip8 => false.into(), - Mode::SChip => true.into(), - Mode::XOChip => Self { - bin_ops: true, - shift: false, - draw_wait: true, - dma_inc: false, - stupid_jumps: false, - }, - } - } -} - -impl Default for Quirks { - fn default() -> Self { - Self::from(false) - } -} - -/// Represents flags that aid in operation, but aren't inherent to the CPU -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ControlFlags { - /// Set when debug (live disassembly) mode enabled - pub debug: bool, - /// Set when the emulator is paused by the user and should not update - pub pause: bool, - /// Set when the emulator is waiting for a keypress - pub keypause: bool, - /// Set when the emulator is waiting for a frame to be drawn - pub draw_wait: bool, - /// Set when the emulator is in high-res mode - pub draw_mode: bool, - /// Set to the last key that's been *released* after a keypause - pub lastkey: Option, - /// Represents the current emulator [Mode] - pub mode: Mode, - /// Represents the set of emulator [Quirks] to enable, independent of the [Mode] - pub quirks: Quirks, - /// Represents the number of instructions to run per tick of the internal timer - pub monotonic: Option, -} - -impl ControlFlags { - /// Toggles debug mode - /// - /// # Examples - /// ```rust - /// # use chirp::*; - /// let mut cpu = CPU::default(); - /// assert_eq!(true, cpu.flags.debug); - /// // Toggle debug mode - /// cpu.flags.debug(); - /// assert_eq!(false, cpu.flags.debug); - /// ``` - pub fn debug(&mut self) { - self.debug = !self.debug - } - - /// Toggles pause - /// - /// # Examples - /// ```rust - /// # use chirp::*; - /// let mut cpu = CPU::default(); - /// assert_eq!(false, cpu.flags.pause); - /// // Pause the cpu - /// cpu.flags.pause(); - /// assert_eq!(true, cpu.flags.pause); - /// ``` - pub fn pause(&mut self) { - self.pause = !self.pause - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Timers { frame: Instant, @@ -190,7 +58,7 @@ impl Default for Timers { pub struct CPU { /// Flags that control how the CPU behaves, but which aren't inherent to the /// implementation. Includes [Quirks], target IPF, etc. - pub flags: ControlFlags, + pub flags: Flags, // memory map info screen: Adr, font: Adr, @@ -235,7 +103,7 @@ impl CPU { sp: Adr, disassembler: Dis, breakpoints: Vec, - flags: ControlFlags, + flags: Flags, ) -> Self { CPU { disassembler, @@ -734,7 +602,7 @@ impl Default for CPU { sound: 0.0, cycle: 0, keys: [false; 16], - flags: ControlFlags { + flags: Flags { debug: true, ..Default::default() }, @@ -744,625 +612,3 @@ impl Default for CPU { } } } - -impl CPU { - /// Executes a single [Insn] - #[inline(always)] - #[rustfmt::skip] - fn execute(&mut self, bus: &mut Bus, instruction: Insn) { - match instruction { - // Core Chip-8 instructions - Insn::cls => self.clear_screen(bus), - Insn::ret => self.ret(bus), - Insn::jmp { A } => self.jump(A), - Insn::call { A } => self.call(A, bus), - Insn::seb { x, B } => self.skip_equals_immediate(x, B), - Insn::sneb { x, B } => self.skip_not_equals_immediate(x, B), - Insn::se { y, x } => self.skip_equals(x, y), - Insn::movb { x, B } => self.load_immediate(x, B), - Insn::addb { x, B } => self.add_immediate(x, B), - Insn::mov { y, x } => self.load(x, y), - Insn::or { y, x } => self.or(x, y), - Insn::and { y, x } => self.and(x, y), - Insn::xor { y, x } => self.xor(x, y), - Insn::add { y, x } => self.add(x, y), - Insn::sub { y, x } => self.sub(x, y), - Insn::shr { y, x } => self.shift_right(x, y), - Insn::bsub { y, x } => self.backwards_sub(x, y), - Insn::shl { y, x } => self.shift_left(x, y), - Insn::sne { y, x } => self.skip_not_equals(x, y), - Insn::movI { A } => self.load_i_immediate(A), - Insn::jmpr { A } => self.jump_indexed(A), - Insn::rand { x, B } => self.rand(x, B), - Insn::draw { y, x, n } => self.draw(x, y, n, bus), - Insn::sek { x } => self.skip_key_equals(x), - Insn::snek { x } => self.skip_key_not_equals(x), - Insn::getdt { x } => self.load_delay_timer(x), - Insn::waitk { x } => self.wait_for_key(x), - Insn::setdt { x } => self.store_delay_timer(x), - Insn::movst { x } => self.store_sound_timer(x), - Insn::addI { x } => self.add_i(x), - Insn::font { x } => self.load_sprite(x), - Insn::bcd { x } => self.bcd_convert(x, bus), - Insn::dmao { x } => self.store_dma(x, bus), - Insn::dmai { x } => self.load_dma(x, bus), - // Super-Chip extensions - Insn::scd { n } => self.scroll_down(n, bus), - Insn::scr => self.scroll_right(bus), - Insn::scl => self.scroll_left(bus), - Insn::halt => self.flags.pause(), - Insn::lores => self.init_lores(bus), - Insn::hires => self.init_hires(bus), - Insn::hfont { x } => self.load_big_sprite(x), - Insn::flgo { x } => self.store_flags(x, bus), - Insn::flgi { x } => self.load_flags(x, bus), - } - } -} - -// Below this point, comments may be duplicated per impl' block, -// since some opcodes handle multiple instructions. - -// |`0aaa`| Issues a "System call" (ML routine) -// -// |opcode| effect | -// |------|------------------------------------| -// |`00e0`| Clear screen memory to all 0 | -// |`00ee`| Return from subroutine | -impl CPU { - /// |`00e0`| Clears the screen memory to 0 - #[inline(always)] - fn clear_screen(&mut self, bus: &mut Bus) { - bus.clear_region(Region::Screen); - } - /// |`00ee`| Returns from subroutine - #[inline(always)] - fn ret(&mut self, bus: &impl Read) { - self.sp = self.sp.wrapping_add(2); - self.pc = bus.read(self.sp); - } -} - -// |`1aaa`| Sets pc to an absolute address -impl CPU { - /// |`1aaa`| Sets the program counter to an absolute address - #[inline(always)] - fn jump(&mut self, a: Adr) { - // jump to self == halt - if a.wrapping_add(2) == self.pc { - self.flags.pause = true; - } - self.pc = a; - } -} - -// |`2aaa`| Pushes pc onto the stack, then jumps to a -impl CPU { - /// |`2aaa`| Pushes pc onto the stack, then jumps to a - #[inline(always)] - fn call(&mut self, a: Adr, bus: &mut impl Write) { - bus.write(self.sp, self.pc); - self.sp = self.sp.wrapping_sub(2); - self.pc = a; - } -} - -// |`3xbb`| Skips next instruction if register X == b -impl CPU { - /// |`3xbb`| Skips the next instruction if register X == b - #[inline(always)] - fn skip_equals_immediate(&mut self, x: Reg, b: u8) { - if self.v[x] == b { - self.pc = self.pc.wrapping_add(2); - } - } -} - -// |`4xbb`| Skips next instruction if register X != b -impl CPU { - /// |`4xbb`| Skips the next instruction if register X != b - #[inline(always)] - fn skip_not_equals_immediate(&mut self, x: Reg, b: u8) { - if self.v[x] != b { - self.pc = self.pc.wrapping_add(2); - } - } -} - -// |`5xyn`| Performs a register-register comparison -// -// |opcode| effect | -// |------|------------------------------------| -// |`5XY0`| Skip next instruction if vX == vY | -impl CPU { - /// |`5xy0`| Skips the next instruction if register X != register Y - #[inline(always)] - fn skip_equals(&mut self, x: Reg, y: Reg) { - if self.v[x] == self.v[y] { - self.pc = self.pc.wrapping_add(2); - } - } -} - -// |`6xbb`| Loads immediate byte b into register vX -impl CPU { - /// |`6xbb`| Loads immediate byte b into register vX - #[inline(always)] - fn load_immediate(&mut self, x: Reg, b: u8) { - self.v[x] = b; - } -} - -// |`7xbb`| Adds immediate byte b to register vX -impl CPU { - /// |`7xbb`| Adds immediate byte b to register vX - #[inline(always)] - fn add_immediate(&mut self, x: Reg, b: u8) { - self.v[x] = self.v[x].wrapping_add(b); - } -} - -// |`8xyn`| Performs ALU operation -// -// |opcode| effect | -// |------|------------------------------------| -// |`8xy0`| Y = X | -// |`8xy1`| X = X | Y | -// |`8xy2`| X = X & Y | -// |`8xy3`| X = X ^ Y | -// |`8xy4`| X = X + Y; Set vF=carry | -// |`8xy5`| X = X - Y; Set vF=carry | -// |`8xy6`| X = X >> 1 | -// |`8xy7`| X = Y - X; Set vF=carry | -// |`8xyE`| X = X << 1 | -impl CPU { - /// |`8xy0`| Loads the value of y into x - #[inline(always)] - fn load(&mut self, x: Reg, y: Reg) { - self.v[x] = self.v[y]; - } - /// |`8xy1`| Performs bitwise or of vX and vY, and stores the result in vX - /// - /// # Quirk - /// The original chip-8 interpreter will clobber vF for any 8-series instruction - #[inline(always)] - fn or(&mut self, x: Reg, y: Reg) { - self.v[x] |= self.v[y]; - if !self.flags.quirks.bin_ops { - self.v[0xf] = 0; - } - } - /// |`8xy2`| Performs bitwise and of vX and vY, and stores the result in vX - /// - /// # Quirk - /// The original chip-8 interpreter will clobber vF for any 8-series instruction - #[inline(always)] - fn and(&mut self, x: Reg, y: Reg) { - self.v[x] &= self.v[y]; - if !self.flags.quirks.bin_ops { - self.v[0xf] = 0; - } - } - /// |`8xy3`| Performs bitwise xor of vX and vY, and stores the result in vX - /// - /// # Quirk - /// The original chip-8 interpreter will clobber vF for any 8-series instruction - #[inline(always)] - fn xor(&mut self, x: Reg, y: Reg) { - self.v[x] ^= self.v[y]; - if !self.flags.quirks.bin_ops { - self.v[0xf] = 0; - } - } - /// |`8xy4`| Performs addition of vX and vY, and stores the result in vX - #[inline(always)] - fn add(&mut self, x: Reg, y: Reg) { - let carry; - (self.v[x], carry) = self.v[x].overflowing_add(self.v[y]); - self.v[0xf] = carry.into(); - } - /// |`8xy5`| Performs subtraction of vX and vY, and stores the result in vX - #[inline(always)] - fn sub(&mut self, x: Reg, y: Reg) { - let carry; - (self.v[x], carry) = self.v[x].overflowing_sub(self.v[y]); - self.v[0xf] = (!carry).into(); - } - /// |`8xy6`| Performs bitwise right shift of vX - /// - /// # Quirk - /// On the original chip-8 interpreter, this shifts vY and stores the result in vX - #[inline(always)] - fn shift_right(&mut self, x: Reg, y: Reg) { - let src: Reg = if self.flags.quirks.shift { x } else { y }; - let shift_out = self.v[src] & 1; - self.v[x] = self.v[src] >> 1; - self.v[0xf] = shift_out; - } - /// |`8xy7`| Performs subtraction of vY and vX, and stores the result in vX - #[inline(always)] - fn backwards_sub(&mut self, x: Reg, y: Reg) { - let carry; - (self.v[x], carry) = self.v[y].overflowing_sub(self.v[x]); - self.v[0xf] = (!carry).into(); - } - /// 8X_E: Performs bitwise left shift of vX - /// - /// # Quirk - /// 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. - #[inline(always)] - fn shift_left(&mut self, x: Reg, y: Reg) { - let src: Reg = if self.flags.quirks.shift { x } else { y }; - let shift_out: u8 = self.v[src] >> 7; - self.v[x] = self.v[src] << 1; - self.v[0xf] = shift_out; - } -} - -// |`9xyn`| Performs a register-register comparison -// -// |opcode| effect | -// |------|------------------------------------| -// |`9XY0`| Skip next instruction if vX != vY | -impl CPU { - /// |`9xy0`| Skip next instruction if X != y - #[inline(always)] - fn skip_not_equals(&mut self, x: Reg, y: Reg) { - if self.v[x] != self.v[y] { - self.pc = self.pc.wrapping_add(2); - } - } -} - -// |`Aaaa`| Load address #a into register I -impl CPU { - /// |`Aadr`| Load address #adr into register I - #[inline(always)] - fn load_i_immediate(&mut self, a: Adr) { - self.i = a; - } -} - -// |`Baaa`| Jump to &adr + v0 -impl CPU { - /// |`Badr`| Jump to &adr + v0 - /// - /// Quirk: - /// On the Super-Chip, this does stupid shit - #[inline(always)] - fn jump_indexed(&mut self, a: 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); - } -} - -// |`Cxbb`| Stores a random number & the provided byte into vX -impl CPU { - /// |`Cxbb`| Stores a random number & the provided byte into vX - #[inline(always)] - fn rand(&mut self, x: Reg, b: u8) { - self.v[x] = random::() & b; - } -} - -// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY) -impl CPU { - /// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY) - /// - /// # Quirk - /// On the original chip-8 interpreter, this will wait for a VBI - #[inline(always)] - fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { - if !self.flags.quirks.draw_wait { - self.flags.draw_wait = true; - } - // self.draw_hires handles both hi-res mode and drawing 16x16 sprites - if self.flags.draw_mode || n == 0 { - self.draw_hires(x, y, n, bus); - } else { - self.draw_lores(x, y, n, bus); - } - } - - #[inline(always)] - fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { - self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, bus); - } - - #[inline(always)] - fn draw_sprite(&mut self, x: u16, y: u16, n: Nib, w: u16, h: u16, bus: &mut Bus) { - let w_bytes = w / 8; - self.v[0xf] = 0; - if let Some(sprite) = bus.get(self.i as usize..(self.i + n as u16) as usize) { - let sprite = sprite.to_vec(); - for (line, &sprite) in sprite.iter().enumerate() { - let line = line as u16; - if y + line >= h { - break; - } - let sprite = (sprite as u16) << (8 - (x % 8)) - & if (x % w) >= (w - 8) { 0xff00 } else { 0xffff }; - let addr = |x, y| -> u16 { (y + line) * w_bytes + (x / 8) + self.screen }; - let screen: u16 = bus.read(addr(x, y)); - bus.write(addr(x, y), screen ^ sprite); - if screen & sprite != 0 { - self.v[0xf] = 1; - } - } - } - } -} - -// |`Exbb`| Skips instruction on value of keypress -// -// |opcode| effect | -// |------|------------------------------------| -// |`eX9e`| Skip next instruction if key == vX | -// |`eXa1`| Skip next instruction if key != vX | -impl CPU { - /// |`Ex9E`| Skip next instruction if key == vX - #[inline(always)] - fn skip_key_equals(&mut self, x: Reg) { - if self.keys[self.v[x] as usize & 0xf] { - self.pc += 2; - } - } - /// |`ExaE`| Skip next instruction if key != vX - #[inline(always)] - fn skip_key_not_equals(&mut self, x: Reg) { - if !self.keys[self.v[x] as usize & 0xf] { - self.pc += 2; - } - } -} - -// |`Fxbb`| Performs IO -// -// |opcode| effect | -// |------|------------------------------------| -// |`fX07`| Set vX to value in delay timer | -// |`fX0a`| Wait for input, store key in vX | -// |`fX15`| Set sound timer to the value in vX | -// |`fX18`| set delay timer to the value in vX | -// |`fX1e`| Add vX to I | -// |`fX29`| Load sprite for character x into I | -// |`fX33`| BCD convert X into I[0..3] | -// |`fX55`| DMA Stor from I to registers 0..=X | -// |`fX65`| DMA Load from I to registers 0..=X | -impl CPU { - /// |`Fx07`| Get the current DT, and put it in vX - /// ```py - /// vX = DT - /// ``` - #[inline(always)] - fn load_delay_timer(&mut self, x: Reg) { - self.v[x] = self.delay as u8; - } - /// |`Fx0A`| Wait for key, then vX = K - #[inline(always)] - fn wait_for_key(&mut self, x: Reg) { - if let Some(key) = self.flags.lastkey { - self.v[x] = key as u8; - self.flags.lastkey = None; - } else { - self.pc = self.pc.wrapping_sub(2); - self.flags.keypause = true; - } - } - /// |`Fx15`| Load vX into DT - /// ```py - /// DT = vX - /// ``` - #[inline(always)] - fn store_delay_timer(&mut self, x: Reg) { - self.delay = self.v[x] as f64; - } - /// |`Fx18`| Load vX into ST - /// ```py - /// ST = vX; - /// ``` - #[inline(always)] - fn store_sound_timer(&mut self, x: Reg) { - self.sound = self.v[x] as f64; - } - /// |`Fx1e`| Add vX to I, - /// ```py - /// I += vX; - /// ``` - #[inline(always)] - fn add_i(&mut self, x: Reg) { - self.i += self.v[x] as u16; - } - /// |`Fx29`| Load sprite for character x into I - /// ```py - /// I = sprite(X); - /// ``` - #[inline(always)] - fn load_sprite(&mut self, x: Reg) { - self.i = self.font + (5 * (self.v[x] as Adr % 0x10)); - } - /// |`Fx33`| BCD convert X into I`[0..3]` - #[inline(always)] - fn bcd_convert(&mut self, x: Reg, bus: &mut Bus) { - let x = self.v[x]; - bus.write(self.i.wrapping_add(2), x % 10); - bus.write(self.i.wrapping_add(1), x / 10 % 10); - bus.write(self.i, x / 100 % 10); - } - /// |`Fx55`| DMA Stor from I to registers 0..=X - /// - /// # Quirk - /// 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. - #[inline(always)] - fn store_dma(&mut self, x: Reg, bus: &mut Bus) { - let i = self.i as usize; - for (reg, value) in bus - .get_mut(i..=i + x) - .unwrap_or_default() - .iter_mut() - .enumerate() - { - *value = self.v[reg] - } - if !self.flags.quirks.dma_inc { - self.i += x as Adr + 1; - } - } - /// |`Fx65`| DMA Load from I to registers 0..=X - /// - /// # Quirk - /// 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. - #[inline(always)] - fn load_dma(&mut self, x: Reg, bus: &mut Bus) { - let i = self.i as usize; - for (reg, value) in bus.get(i..=i + x).unwrap_or_default().iter().enumerate() { - self.v[reg] = *value; - } - if !self.flags.quirks.dma_inc { - self.i += x as Adr + 1; - } - } -} - -//////////////// SUPER CHIP //////////////// - -impl CPU { - /// |`00cN`| Scroll the screen down N lines - #[inline(always)] - fn scroll_down(&mut self, n: Nib, bus: &mut Bus) { - match self.flags.draw_mode { - true => { - // Get a line from the bus - for i in (0..16 * (64 - n as usize)).step_by(16).rev() { - let i = i + self.screen as usize; - let line: u128 = bus.read(i); - bus.write(i - (n as usize * 16), 0u128); - bus.write(i, line); - } - } - false => { - // Get a line from the bus - for i in (0..8 * (32 - n as usize)).step_by(8).rev() { - let i = i + self.screen as usize; - let line: u64 = bus.read(i); - bus.write(i, 0u64); - bus.write(i + (n as usize * 8), line); - } - } - } - } - - /// |`00fb`| Scroll the screen right - #[inline(always)] - fn scroll_right(&mut self, bus: &mut (impl Read + Write)) { - // Get a line from the bus - for i in (0..16 * 64).step_by(16) { - //let line: u128 = bus.read(self.screen + i) >> 4; - bus.write(self.screen + i, bus.read(self.screen + i) >> 4); - } - } - /// |`00fc`| Scroll the screen right - #[inline(always)] - fn scroll_left(&mut self, bus: &mut (impl Read + Write)) { - // Get a line from the bus - for i in (0..16 * 64).step_by(16) { - let line: u128 = (bus.read(self.screen + i) & !(0xf << 124)) << 4; - bus.write(self.screen + i, line); - } - } - - /// |`Dxyn`| - /// Super-Chip extension high-resolution graphics mode - #[inline(always)] - fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { - if !self.flags.quirks.draw_wait { - self.flags.draw_wait = true; - } - let (w, h) = match self.flags.draw_mode { - true => (128, 64), - false => (64, 32), - }; - let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); - match n { - 0 => self.draw_schip_sprite(x, y, w, bus), - _ => self.draw_sprite(x, y, n, w, h, bus), - } - } - /// Draws a 16x16 Super Chip sprite - #[inline(always)] - fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, bus: &mut Bus) { - self.v[0xf] = 0; - let w_bytes = w / 8; - if let Some(sprite) = bus.get(self.i as usize..(self.i + 32) as usize) { - let sprite = sprite.to_owned(); - for (line, sprite) in sprite.chunks(2).enumerate() { - let sprite = u16::from_be_bytes( - sprite - .try_into() - .expect("Chunks should only return 2 bytes"), - ); - let addr = (y + line as u16) * w_bytes + x / 8 + self.screen; - let sprite = (sprite as u32) << (16 - (x % 8)); - let screen: u32 = bus.read(addr); - bus.write(addr, screen ^ sprite); - if screen & sprite != 0 { - self.v[0xf] += 1; - } - } - } - } - - /// |`Fx30`| (Super-Chip) 16x16 equivalent of Fx29 - /// - /// TODO: Actually make and import the 16x font - #[inline(always)] - fn load_big_sprite(&mut self, x: Reg) { - self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10)); - } - - /// |`Fx75`| (Super-Chip) Save to "flag registers" - /// I just chuck it in 0x0..0xf. Screw it. - #[inline(always)] - fn store_flags(&mut self, x: Reg, bus: &mut Bus) { - // TODO: Save these, maybe - for (reg, value) in bus - .get_mut(0..=x) - .unwrap_or_default() - .iter_mut() - .enumerate() - { - *value = self.v[reg] - } - } - - /// |`Fx85`| (Super-Chip) Load from "flag registers" - /// I just chuck it in 0x0..0xf. Screw it. - #[inline(always)] - fn load_flags(&mut self, x: Reg, bus: &mut Bus) { - for (reg, value) in bus.get(0..=x).unwrap_or_default().iter().enumerate() { - self.v[reg] = *value; - } - } - - /// Initialize lores mode - fn init_lores(&mut self, bus: &mut Bus) { - self.flags.draw_mode = false; - let scraddr = self.screen as usize; - bus.set_region(Region::Screen, scraddr..scraddr + 256); - self.clear_screen(bus); - } - /// Initialize hires mode - fn init_hires(&mut self, bus: &mut Bus) { - self.flags.draw_mode = true; - let scraddr = self.screen as usize; - bus.set_region(Region::Screen, scraddr..scraddr + 1024); - self.clear_screen(bus); - } -} diff --git a/src/cpu/flags.rs b/src/cpu/flags.rs new file mode 100644 index 0000000..4acd8f0 --- /dev/null +++ b/src/cpu/flags.rs @@ -0,0 +1,58 @@ +//! Represents flags that aid in implementation but aren't a part of the Chip-8 spec + +use super::{Mode, Quirks}; + +/// Represents flags that aid in operation, but aren't inherent to the CPU +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Flags { + /// Set when debug (live disassembly) mode enabled + pub debug: bool, + /// Set when the emulator is paused by the user and should not update + pub pause: bool, + /// Set when the emulator is waiting for a keypress + pub keypause: bool, + /// Set when the emulator is waiting for a frame to be drawn + pub draw_wait: bool, + /// Set when the emulator is in high-res mode + pub draw_mode: bool, + /// Set to the last key that's been *released* after a keypause + pub lastkey: Option, + /// Represents the current emulator [Mode] + pub mode: Mode, + /// Represents the set of emulator [Quirks] to enable, independent of the [Mode] + pub quirks: Quirks, + /// Represents the number of instructions to run per tick of the internal timer + pub monotonic: Option, +} + +impl Flags { + /// Toggles debug mode + /// + /// # Examples + /// ```rust + /// # use chirp::*; + /// let mut cpu = CPU::default(); + /// assert_eq!(true, cpu.flags.debug); + /// // Toggle debug mode + /// cpu.flags.debug(); + /// assert_eq!(false, cpu.flags.debug); + /// ``` + pub fn debug(&mut self) { + self.debug = !self.debug + } + + /// Toggles pause + /// + /// # Examples + /// ```rust + /// # use chirp::*; + /// let mut cpu = CPU::default(); + /// assert_eq!(false, cpu.flags.pause); + /// // Pause the cpu + /// cpu.flags.pause(); + /// assert_eq!(true, cpu.flags.pause); + /// ``` + pub fn pause(&mut self) { + self.pause = !self.pause + } +} diff --git a/src/cpu/instruction.rs b/src/cpu/instruction.rs index 33b13b0..5db5019 100644 --- a/src/cpu/instruction.rs +++ b/src/cpu/instruction.rs @@ -1,235 +1,625 @@ // (c) 2023 John A. Breaux // This code is licensed under MIT license (see LICENSE.txt for details) -//! Represents a chip-8 instruction as a Rust enum +//! Contains implementations for each instruction defined in [super::disassembler] -use super::{Adr, Nib, Reg}; -type Word = Adr; -type Byte = u8; -type Ins = Nib; +use super::*; -/// Extract the instruction nibble from a word -#[inline] -pub fn i(ins: Word) -> Ins { - (ins >> 12) as Ins & 0xf -} -/// Extracts the X-register nibble from a word -#[inline] -pub fn x(ins: Word) -> Reg { - ins as Reg >> 8 & 0xf -} -/// Extracts the Y-register nibble from a word -#[inline] -pub fn y(ins: u16) -> Reg { - ins as Reg >> 4 & 0xf -} -/// Extracts the nibble-sized immediate from a word -#[inline] -pub fn n(ins: Word) -> Nib { - ins as Nib & 0xf -} -/// Extracts the byte-sized immediate from a word -#[inline] -pub fn b(ins: Word) -> Byte { - ins as Byte -} -/// Extracts the address-sized immediate from a word -#[inline] -pub fn a(ins: Word) -> Adr { - ins & 0x0fff -} -/// Restores the instruction nibble into a word -#[inline] -pub fn ii(i: Ins) -> u16 { - (i as Word & 0xf) << 12 -} -/// Restores the X-register nibble into a word -#[inline] -pub fn xi(x: Reg) -> Word { - (x as Word & 0xf) << 8 -} -/// Restores the Y-register nibble into a word -#[inline] -pub fn yi(y: Reg) -> Word { - (y as Word & 0xf) << 4 -} -/// Restores the nibble-sized immediate into a word -#[inline] -pub fn ni(n: Nib) -> Word { - n as Word & 0xf -} -/// Restores the byte-sized immediate into a word -#[inline] -pub fn bi(b: Byte) -> Word { - b as Word -} -/// Captures the operand and type of a Chip-8 instruction -pub enum Chip8Instruction { - Unimplemented(Word), - Clear, - Return, - Sys(Adr), - Jump(Adr), - Call(Adr), - SkipEqualsByte(Reg, Byte), - SkipNotEqualsByte(Reg, Byte), - SkipEquals(Reg, Reg), - LoadImmediate(Reg, Byte), - AddImmediate(Reg, Byte), - Copy(Reg, Reg), - Or(Reg, Reg), - And(Reg, Reg), - Xor(Reg, Reg), - Add(Reg, Reg), - Sub(Reg, Reg), - ShiftRight(Reg, Reg), - BackwardsSub(Reg, Reg), - ShiftLeft(Reg, Reg), - SkipNotEquals(Reg, Reg), - LoadIndirect(Adr), - JumpIndexed(Adr), - Rand(Reg, Byte), - Draw(Reg, Reg, Nib), - SkipEqualsKey(Reg), - SkipNotEqualsKey(Reg), - StoreDelay(Reg), - WaitForKey(Reg), - LoadDelay(Reg), - LoadSound(Reg), - AddIndirect(Reg), - LoadSprite(Reg), - BcdConvert(Reg), - DmaStore(Reg), - DmaLoad(Reg), -} - -impl TryFrom for Chip8Instruction { - type Error = crate::error::Error; - /// Converts a 16-bit word into a Chip8Instruction, when possible. - fn try_from(opcode: Word) -> Result { - use crate::error::Error::*; - let (i, x, y, n, b, a) = ( - i(opcode), - x(opcode), - y(opcode), - n(opcode), - b(opcode), - a(opcode), - ); - if i > 0xf { - return Err(FunkyMath { - word: opcode, - explanation: "Instruction nibble greater than 0xf".into(), - }); +impl CPU { + /// Executes a single [Insn] + #[inline(always)] + #[rustfmt::skip] + pub(super) fn execute(&mut self, bus: &mut Bus, instruction: Insn) { + match instruction { + // Core Chip-8 instructions + Insn::cls => self.clear_screen(bus), + Insn::ret => self.ret(bus), + Insn::jmp { A } => self.jump(A), + Insn::call { A } => self.call(A, bus), + Insn::seb { x, B } => self.skip_equals_immediate(x, B), + Insn::sneb { x, B } => self.skip_not_equals_immediate(x, B), + Insn::se { y, x } => self.skip_equals(x, y), + Insn::movb { x, B } => self.load_immediate(x, B), + Insn::addb { x, B } => self.add_immediate(x, B), + Insn::mov { y, x } => self.load(x, y), + Insn::or { y, x } => self.or(x, y), + Insn::and { y, x } => self.and(x, y), + Insn::xor { y, x } => self.xor(x, y), + Insn::add { y, x } => self.add(x, y), + Insn::sub { y, x } => self.sub(x, y), + Insn::shr { y, x } => self.shift_right(x, y), + Insn::bsub { y, x } => self.backwards_sub(x, y), + Insn::shl { y, x } => self.shift_left(x, y), + Insn::sne { y, x } => self.skip_not_equals(x, y), + Insn::movI { A } => self.load_i_immediate(A), + Insn::jmpr { A } => self.jump_indexed(A), + Insn::rand { x, B } => self.rand(x, B), + Insn::draw { y, x, n } => self.draw(x, y, n, bus), + Insn::sek { x } => self.skip_key_equals(x), + Insn::snek { x } => self.skip_key_not_equals(x), + Insn::getdt { x } => self.load_delay_timer(x), + Insn::waitk { x } => self.wait_for_key(x), + Insn::setdt { x } => self.store_delay_timer(x), + Insn::movst { x } => self.store_sound_timer(x), + Insn::addI { x } => self.add_i(x), + Insn::font { x } => self.load_sprite(x), + Insn::bcd { x } => self.bcd_convert(x, bus), + Insn::dmao { x } => self.store_dma(x, bus), + Insn::dmai { x } => self.load_dma(x, bus), + // Super-Chip extensions + Insn::scd { n } => self.scroll_down(n, bus), + Insn::scr => self.scroll_right(bus), + Insn::scl => self.scroll_left(bus), + Insn::halt => self.flags.pause(), + Insn::lores => self.init_lores(bus), + Insn::hires => self.init_hires(bus), + Insn::hfont { x } => self.load_big_sprite(x), + Insn::flgo { x } => self.store_flags(x, bus), + Insn::flgi { x } => self.load_flags(x, bus), } - Ok(match i { - // # Issue a system call - // |opcode| effect | - // |------|------------------------------------| - // | 00e0 | Clear screen memory to all 0 | - // | 00ee | Return from subroutine | - 0x0 => match a { - 0xe0 => Self::Clear, - 0xee => Self::Return, - _ => Self::Sys(a), - }, - // | 1aaa | Sets pc to an absolute address - 0x1 => Self::Jump(a), - // | 2aaa | Pushes pc onto the stack, then jumps to a - 0x2 => Self::Call(a), - // | 3xbb | Skips next instruction if register X == b - 0x3 => Self::SkipEqualsByte(x, b), - // | 4xbb | Skips next instruction if register X != b - 0x4 => Self::SkipNotEqualsByte(x, b), - // # Performs a register-register comparison - // |opcode| effect | - // |------|------------------------------------| - // | 9XY0 | Skip next instruction if vX == vY | - 0x5 => match n { - 0x0 => Self::SkipEquals(x, y), - _ => Self::Unimplemented(opcode), - }, - // 6xbb: Loads immediate byte b into register vX - 0x6 => Self::LoadImmediate(x, b), - // 7xbb: Adds immediate byte b to register vX - 0x7 => Self::AddImmediate(x, b), - // # Performs ALU operation - // |opcode| effect | - // |------|------------------------------------| - // | 8xy0 | X = Y | - // | 8xy1 | X = X | Y | - // | 8xy2 | X = X & Y | - // | 8xy3 | X = X ^ Y | - // | 8xy4 | X = X + Y; Set vF=carry | - // | 8xy5 | X = X - Y; Set vF=carry | - // | 8xy6 | X = X >> 1 | - // | 8xy7 | X = Y - X; Set vF=carry | - // | 8xyE | X = X << 1 | - 0x8 => match n { - 0x0 => Self::Copy(x, y), - 0x1 => Self::Or(x, y), - 0x2 => Self::And(x, y), - 0x3 => Self::Xor(x, y), - 0x4 => Self::Add(x, y), - 0x5 => Self::Sub(x, y), - 0x6 => Self::ShiftRight(x, y), - 0x7 => Self::BackwardsSub(x, y), - 0xE => Self::ShiftLeft(x, y), - _ => Self::Unimplemented(opcode), - }, - // # Performs a register-register comparison - // |opcode| effect | - // |------|------------------------------------| - // | 9XY0 | Skip next instruction if vX != vY | - 0x9 => match n { - 0 => Self::SkipNotEquals(x, y), - _ => Self::Unimplemented(opcode), - }, - // Aaaa: Load address #a into register I - 0xa => Self::LoadIndirect(a), - // Baaa: Jump to &adr + v0 - 0xb => Self::JumpIndexed(a), - // Cxbb: Stores a random number & the provided byte into vX - 0xc => Self::Rand(x, b), - // Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY) - 0xd => Self::Draw(x, y, n), - - // # Skips instruction on value of keypress - // |opcode| effect | - // |------|------------------------------------| - // | eX9e | Skip next instruction if key == #X | - // | eXa1 | Skip next instruction if key != #X | - 0xe => match b { - 0x9e => Self::SkipEqualsKey(x), - 0xa1 => Self::SkipNotEqualsKey(x), - _ => Self::Unimplemented(opcode), - }, - - // # Performs IO - // |opcode| effect | - // |------|------------------------------------| - // | fX07 | Set vX to value in delay timer | - // | fX0a | Wait for input, store in vX m | - // | fX15 | Set sound timer to the value in vX | - // | fX18 | set delay timer to the value in vX | - // | fX1e | Add x to I | - // | fX29 | Load sprite for character x into I | - // | fX33 | BCD convert X into I[0..3] | - // | fX55 | DMA Stor from I to registers 0..X | - // | fX65 | DMA Load from I to registers 0..X | - 0xf => match b { - 0x07 => Self::StoreDelay(x), - 0x0A => Self::WaitForKey(x), - 0x15 => Self::LoadDelay(x), - 0x18 => Self::LoadSound(x), - 0x1E => Self::AddIndirect(x), - 0x29 => Self::LoadSprite(x), - 0x33 => Self::BcdConvert(x), - 0x55 => Self::DmaStore(x), - 0x65 => Self::DmaLoad(x), - _ => Self::Unimplemented(opcode), - }, - _ => unreachable!("i somehow mutated from <= 0xf to > 0xf"), - }) + } +} + +// |`0aaa`| Issues a "System call" (ML routine) +// +// |opcode| effect | +// |------|------------------------------------| +// |`00e0`| Clear screen memory to all 0 | +// |`00ee`| Return from subroutine | +impl CPU { + /// |`00e0`| Clears the screen memory to 0 + #[inline(always)] + pub(super) fn clear_screen(&mut self, bus: &mut Bus) { + bus.clear_region(Region::Screen); + } + /// |`00ee`| Returns from subroutine + #[inline(always)] + pub(super) fn ret(&mut self, bus: &impl Read) { + self.sp = self.sp.wrapping_add(2); + self.pc = bus.read(self.sp); + } +} + +// |`1aaa`| Sets pc to an absolute address +impl CPU { + /// |`1aaa`| Sets the program counter to an absolute address + #[inline(always)] + pub(super) fn jump(&mut self, a: Adr) { + // jump to self == halt + if a.wrapping_add(2) == self.pc { + self.flags.pause = true; + } + self.pc = a; + } +} + +// |`2aaa`| Pushes pc onto the stack, then jumps to a +impl CPU { + /// |`2aaa`| Pushes pc onto the stack, then jumps to a + #[inline(always)] + pub(super) fn call(&mut self, a: Adr, bus: &mut impl Write) { + bus.write(self.sp, self.pc); + self.sp = self.sp.wrapping_sub(2); + self.pc = a; + } +} + +// |`3xbb`| Skips next instruction if register X == b +impl CPU { + /// |`3xbb`| Skips the next instruction if register X == b + #[inline(always)] + pub(super) fn skip_equals_immediate(&mut self, x: Reg, b: u8) { + if self.v[x] == b { + self.pc = self.pc.wrapping_add(2); + } + } +} + +// |`4xbb`| Skips next instruction if register X != b +impl CPU { + /// |`4xbb`| Skips the next instruction if register X != b + #[inline(always)] + pub(super) fn skip_not_equals_immediate(&mut self, x: Reg, b: u8) { + if self.v[x] != b { + self.pc = self.pc.wrapping_add(2); + } + } +} + +// |`5xyn`| Performs a register-register comparison +// +// |opcode| effect | +// |------|------------------------------------| +// |`5XY0`| Skip next instruction if vX == vY | +impl CPU { + /// |`5xy0`| Skips the next instruction if register X != register Y + #[inline(always)] + pub(super) fn skip_equals(&mut self, x: Reg, y: Reg) { + if self.v[x] == self.v[y] { + self.pc = self.pc.wrapping_add(2); + } + } +} + +// |`6xbb`| Loads immediate byte b into register vX +impl CPU { + /// |`6xbb`| Loads immediate byte b into register vX + #[inline(always)] + pub(super) fn load_immediate(&mut self, x: Reg, b: u8) { + self.v[x] = b; + } +} + +// |`7xbb`| Adds immediate byte b to register vX +impl CPU { + /// |`7xbb`| Adds immediate byte b to register vX + #[inline(always)] + pub(super) fn add_immediate(&mut self, x: Reg, b: u8) { + self.v[x] = self.v[x].wrapping_add(b); + } +} + +// |`8xyn`| Performs ALU operation +// +// |opcode| effect | +// |------|------------------------------------| +// |`8xy0`| Y = X | +// |`8xy1`| X = X | Y | +// |`8xy2`| X = X & Y | +// |`8xy3`| X = X ^ Y | +// |`8xy4`| X = X + Y; Set vF=carry | +// |`8xy5`| X = X - Y; Set vF=carry | +// |`8xy6`| X = X >> 1 | +// |`8xy7`| X = Y - X; Set vF=carry | +// |`8xyE`| X = X << 1 | +impl CPU { + /// |`8xy0`| Loads the value of y into x + #[inline(always)] + pub(super) fn load(&mut self, x: Reg, y: Reg) { + self.v[x] = self.v[y]; + } + /// |`8xy1`| Performs bitwise or of vX and vY, and stores the result in vX + /// + /// # Quirk + /// The original chip-8 interpreter will clobber vF for any 8-series instruction + #[inline(always)] + pub(super) fn or(&mut self, x: Reg, y: Reg) { + self.v[x] |= self.v[y]; + if !self.flags.quirks.bin_ops { + self.v[0xf] = 0; + } + } + /// |`8xy2`| Performs bitwise and of vX and vY, and stores the result in vX + /// + /// # Quirk + /// The original chip-8 interpreter will clobber vF for any 8-series instruction + #[inline(always)] + pub(super) fn and(&mut self, x: Reg, y: Reg) { + self.v[x] &= self.v[y]; + if !self.flags.quirks.bin_ops { + self.v[0xf] = 0; + } + } + /// |`8xy3`| Performs bitwise xor of vX and vY, and stores the result in vX + /// + /// # Quirk + /// The original chip-8 interpreter will clobber vF for any 8-series instruction + #[inline(always)] + pub(super) fn xor(&mut self, x: Reg, y: Reg) { + self.v[x] ^= self.v[y]; + if !self.flags.quirks.bin_ops { + self.v[0xf] = 0; + } + } + /// |`8xy4`| Performs addition of vX and vY, and stores the result in vX + #[inline(always)] + pub(super) fn add(&mut self, x: Reg, y: Reg) { + let carry; + (self.v[x], carry) = self.v[x].overflowing_add(self.v[y]); + self.v[0xf] = carry.into(); + } + /// |`8xy5`| Performs subtraction of vX and vY, and stores the result in vX + #[inline(always)] + pub(super) fn sub(&mut self, x: Reg, y: Reg) { + let carry; + (self.v[x], carry) = self.v[x].overflowing_sub(self.v[y]); + self.v[0xf] = (!carry).into(); + } + /// |`8xy6`| Performs bitwise right shift of vX + /// + /// # Quirk + /// On the original chip-8 interpreter, this shifts vY and stores the result in vX + #[inline(always)] + pub(super) fn shift_right(&mut self, x: Reg, y: Reg) { + let src: Reg = if self.flags.quirks.shift { x } else { y }; + let shift_out = self.v[src] & 1; + self.v[x] = self.v[src] >> 1; + self.v[0xf] = shift_out; + } + /// |`8xy7`| Performs subtraction of vY and vX, and stores the result in vX + #[inline(always)] + pub(super) fn backwards_sub(&mut self, x: Reg, y: Reg) { + let carry; + (self.v[x], carry) = self.v[y].overflowing_sub(self.v[x]); + self.v[0xf] = (!carry).into(); + } + /// 8X_E: Performs bitwise left shift of vX + /// + /// # Quirk + /// 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. + #[inline(always)] + pub(super) fn shift_left(&mut self, x: Reg, y: Reg) { + let src: Reg = if self.flags.quirks.shift { x } else { y }; + let shift_out: u8 = self.v[src] >> 7; + self.v[x] = self.v[src] << 1; + self.v[0xf] = shift_out; + } +} + +// |`9xyn`| Performs a register-register comparison +// +// |opcode| effect | +// |------|------------------------------------| +// |`9XY0`| Skip next instruction if vX != vY | +impl CPU { + /// |`9xy0`| Skip next instruction if X != y + #[inline(always)] + pub(super) fn skip_not_equals(&mut self, x: Reg, y: Reg) { + if self.v[x] != self.v[y] { + self.pc = self.pc.wrapping_add(2); + } + } +} + +// |`Aaaa`| Load address #a into register I +impl CPU { + /// |`Aadr`| Load address #adr into register I + #[inline(always)] + pub(super) fn load_i_immediate(&mut self, a: Adr) { + self.i = a; + } +} + +// |`Baaa`| Jump to &adr + v0 +impl CPU { + /// |`Badr`| Jump to &adr + v0 + /// + /// Quirk: + /// On the Super-Chip, this does stupid shit + #[inline(always)] + pub(super) fn jump_indexed(&mut self, a: 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); + } +} + +// |`Cxbb`| Stores a random number & the provided byte into vX +impl CPU { + /// |`Cxbb`| Stores a random number & the provided byte into vX + #[inline(always)] + pub(super) fn rand(&mut self, x: Reg, b: u8) { + self.v[x] = random::() & b; + } +} + +// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY) +impl CPU { + /// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY) + /// + /// # Quirk + /// On the original chip-8 interpreter, this will wait for a VBI + #[inline(always)] + pub(super) fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + if !self.flags.quirks.draw_wait { + self.flags.draw_wait = true; + } + // self.draw_hires handles both hi-res mode and drawing 16x16 sprites + if self.flags.draw_mode || n == 0 { + self.draw_hires(x, y, n, bus); + } else { + self.draw_lores(x, y, n, bus); + } + } + + #[inline(always)] + pub(super) fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, bus); + } + + #[inline(always)] + pub(super) fn draw_sprite(&mut self, x: u16, y: u16, n: Nib, w: u16, h: u16, bus: &mut Bus) { + let w_bytes = w / 8; + self.v[0xf] = 0; + if let Some(sprite) = bus.get(self.i as usize..(self.i + n as u16) as usize) { + let sprite = sprite.to_vec(); + for (line, &sprite) in sprite.iter().enumerate() { + let line = line as u16; + if y + line >= h { + break; + } + let sprite = (sprite as u16) << (8 - (x % 8)) + & if (x % w) >= (w - 8) { 0xff00 } else { 0xffff }; + let addr = |x, y| -> u16 { (y + line) * w_bytes + (x / 8) + self.screen }; + let screen: u16 = bus.read(addr(x, y)); + bus.write(addr(x, y), screen ^ sprite); + if screen & sprite != 0 { + self.v[0xf] = 1; + } + } + } + } +} + +// |`Exbb`| Skips instruction on value of keypress +// +// |opcode| effect | +// |------|------------------------------------| +// |`eX9e`| Skip next instruction if key == vX | +// |`eXa1`| Skip next instruction if key != vX | +impl CPU { + /// |`Ex9E`| Skip next instruction if key == vX + #[inline(always)] + pub(super) fn skip_key_equals(&mut self, x: Reg) { + if self.keys[self.v[x] as usize & 0xf] { + self.pc += 2; + } + } + /// |`ExaE`| Skip next instruction if key != vX + #[inline(always)] + pub(super) fn skip_key_not_equals(&mut self, x: Reg) { + if !self.keys[self.v[x] as usize & 0xf] { + self.pc += 2; + } + } +} + +// |`Fxbb`| Performs IO +// +// |opcode| effect | +// |------|------------------------------------| +// |`fX07`| Set vX to value in delay timer | +// |`fX0a`| Wait for input, store key in vX | +// |`fX15`| Set sound timer to the value in vX | +// |`fX18`| set delay timer to the value in vX | +// |`fX1e`| Add vX to I | +// |`fX29`| Load sprite for character x into I | +// |`fX33`| BCD convert X into I[0..3] | +// |`fX55`| DMA Stor from I to registers 0..=X | +// |`fX65`| DMA Load from I to registers 0..=X | +impl CPU { + /// |`Fx07`| Get the current DT, and put it in vX + /// ```py + /// vX = DT + /// ``` + #[inline(always)] + pub(super) fn load_delay_timer(&mut self, x: Reg) { + self.v[x] = self.delay as u8; + } + /// |`Fx0A`| Wait for key, then vX = K + #[inline(always)] + pub(super) fn wait_for_key(&mut self, x: Reg) { + if let Some(key) = self.flags.lastkey { + self.v[x] = key as u8; + self.flags.lastkey = None; + } else { + self.pc = self.pc.wrapping_sub(2); + self.flags.keypause = true; + } + } + /// |`Fx15`| Load vX into DT + /// ```py + /// DT = vX + /// ``` + #[inline(always)] + pub(super) fn store_delay_timer(&mut self, x: Reg) { + self.delay = self.v[x] as f64; + } + /// |`Fx18`| Load vX into ST + /// ```py + /// ST = vX; + /// ``` + #[inline(always)] + pub(super) fn store_sound_timer(&mut self, x: Reg) { + self.sound = self.v[x] as f64; + } + /// |`Fx1e`| Add vX to I, + /// ```py + /// I += vX; + /// ``` + #[inline(always)] + pub(super) fn add_i(&mut self, x: Reg) { + self.i += self.v[x] as u16; + } + /// |`Fx29`| Load sprite for character x into I + /// ```py + /// I = sprite(X); + /// ``` + #[inline(always)] + pub(super) fn load_sprite(&mut self, x: Reg) { + self.i = self.font + (5 * (self.v[x] as Adr % 0x10)); + } + /// |`Fx33`| BCD convert X into I`[0..3]` + #[inline(always)] + pub(super) fn bcd_convert(&mut self, x: Reg, bus: &mut Bus) { + let x = self.v[x]; + bus.write(self.i.wrapping_add(2), x % 10); + bus.write(self.i.wrapping_add(1), x / 10 % 10); + bus.write(self.i, x / 100 % 10); + } + /// |`Fx55`| DMA Stor from I to registers 0..=X + /// + /// # Quirk + /// 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. + #[inline(always)] + pub(super) fn store_dma(&mut self, x: Reg, bus: &mut Bus) { + let i = self.i as usize; + for (reg, value) in bus + .get_mut(i..=i + x) + .unwrap_or_default() + .iter_mut() + .enumerate() + { + *value = self.v[reg] + } + if !self.flags.quirks.dma_inc { + self.i += x as Adr + 1; + } + } + /// |`Fx65`| DMA Load from I to registers 0..=X + /// + /// # Quirk + /// 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. + #[inline(always)] + pub(super) fn load_dma(&mut self, x: Reg, bus: &mut Bus) { + let i = self.i as usize; + for (reg, value) in bus.get(i..=i + x).unwrap_or_default().iter().enumerate() { + self.v[reg] = *value; + } + if !self.flags.quirks.dma_inc { + self.i += x as Adr + 1; + } + } +} + +//////////////// SUPER CHIP //////////////// + +impl CPU { + /// |`00cN`| Scroll the screen down N lines + #[inline(always)] + pub(super) fn scroll_down(&mut self, n: Nib, bus: &mut Bus) { + match self.flags.draw_mode { + true => { + // Get a line from the bus + for i in (0..16 * (64 - n as usize)).step_by(16).rev() { + let i = i + self.screen as usize; + let line: u128 = bus.read(i); + bus.write(i - (n as usize * 16), 0u128); + bus.write(i, line); + } + } + false => { + // Get a line from the bus + for i in (0..8 * (32 - n as usize)).step_by(8).rev() { + let i = i + self.screen as usize; + let line: u64 = bus.read(i); + bus.write(i, 0u64); + bus.write(i + (n as usize * 8), line); + } + } + } + } + + /// |`00fb`| Scroll the screen right + #[inline(always)] + pub(super) fn scroll_right(&mut self, bus: &mut (impl Read + Write)) { + // Get a line from the bus + for i in (0..16 * 64).step_by(16) { + //let line: u128 = bus.read(self.screen + i) >> 4; + bus.write(self.screen + i, bus.read(self.screen + i) >> 4); + } + } + /// |`00fc`| Scroll the screen right + #[inline(always)] + pub(super) fn scroll_left(&mut self, bus: &mut (impl Read + Write)) { + // Get a line from the bus + for i in (0..16 * 64).step_by(16) { + let line: u128 = (bus.read(self.screen + i) & !(0xf << 124)) << 4; + bus.write(self.screen + i, line); + } + } + + /// |`Dxyn`| + /// Super-Chip extension high-resolution graphics mode + #[inline(always)] + pub(super) fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + if !self.flags.quirks.draw_wait { + self.flags.draw_wait = true; + } + let (w, h) = match self.flags.draw_mode { + true => (128, 64), + false => (64, 32), + }; + let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); + match n { + 0 => self.draw_schip_sprite(x, y, w, bus), + _ => self.draw_sprite(x, y, n, w, h, bus), + } + } + /// Draws a 16x16 Super Chip sprite + #[inline(always)] + pub(super) fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, bus: &mut Bus) { + self.v[0xf] = 0; + let w_bytes = w / 8; + if let Some(sprite) = bus.get(self.i as usize..(self.i + 32) as usize) { + let sprite = sprite.to_owned(); + for (line, sprite) in sprite.chunks(2).enumerate() { + let sprite = u16::from_be_bytes( + sprite + .try_into() + .expect("Chunks should only return 2 bytes"), + ); + let addr = (y + line as u16) * w_bytes + x / 8 + self.screen; + let sprite = (sprite as u32) << (16 - (x % 8)); + let screen: u32 = bus.read(addr); + bus.write(addr, screen ^ sprite); + if screen & sprite != 0 { + self.v[0xf] += 1; + } + } + } + } + + /// |`Fx30`| (Super-Chip) 16x16 equivalent of Fx29 + /// + /// TODO: Actually make and import the 16x font + #[inline(always)] + pub(super) fn load_big_sprite(&mut self, x: Reg) { + self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10)); + } + + /// |`Fx75`| (Super-Chip) Save to "flag registers" + /// I just chuck it in 0x0..0xf. Screw it. + #[inline(always)] + pub(super) fn store_flags(&mut self, x: Reg, bus: &mut Bus) { + // TODO: Save these, maybe + for (reg, value) in bus + .get_mut(0..=x) + .unwrap_or_default() + .iter_mut() + .enumerate() + { + *value = self.v[reg] + } + } + + /// |`Fx85`| (Super-Chip) Load from "flag registers" + /// I just chuck it in 0x0..0xf. Screw it. + #[inline(always)] + pub(super) fn load_flags(&mut self, x: Reg, bus: &mut Bus) { + for (reg, value) in bus.get(0..=x).unwrap_or_default().iter().enumerate() { + self.v[reg] = *value; + } + } + + /// Initialize lores mode + pub(super) fn init_lores(&mut self, bus: &mut Bus) { + self.flags.draw_mode = false; + let scraddr = self.screen as usize; + bus.set_region(Region::Screen, scraddr..scraddr + 256); + self.clear_screen(bus); + } + /// Initialize hires mode + pub(super) fn init_hires(&mut self, bus: &mut Bus) { + self.flags.draw_mode = true; + let scraddr = self.screen as usize; + bus.set_region(Region::Screen, scraddr..scraddr + 1024); + self.clear_screen(bus); } } diff --git a/src/cpu/mode.rs b/src/cpu/mode.rs new file mode 100644 index 0000000..5e42e87 --- /dev/null +++ b/src/cpu/mode.rs @@ -0,0 +1,31 @@ +//! Selects the memory behavior of the [super::CPU] + +use crate::error::Error; +use std::str::FromStr; + +/// Selects the memory behavior of the interpreter +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Mode { + /// VIP emulation mode + #[default] + Chip8, + /// Chip-48 emulation mode + SChip, + /// XO-Chip emulation mode + XOChip, +} + +impl FromStr for Mode { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "chip8" | "chip-8" => Ok(Mode::Chip8), + "schip" | "superchip" => Ok(Mode::SChip), + "xo-chip" | "xochip" => Ok(Mode::XOChip), + _ => Err(Error::InvalidMode { + mode: s.to_string(), + }), + } + } +} diff --git a/src/cpu/quirks.rs b/src/cpu/quirks.rs new file mode 100644 index 0000000..90a5af2 --- /dev/null +++ b/src/cpu/quirks.rs @@ -0,0 +1,60 @@ +//! Controls the authenticity behavior of the CPU on a granular level. +use super::Mode; +/// Controls the authenticity behavior of the CPU on a granular level. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Quirks { + /// Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0 + pub bin_ops: bool, + /// Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX + pub shift: bool, + /// Draw operations shouldn't pause execution until the next timer tick + pub draw_wait: bool, + /// DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1 + pub dma_inc: bool, + /// Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`. + pub stupid_jumps: bool, +} + +impl From for Quirks { + fn from(value: bool) -> Self { + if value { + Quirks { + bin_ops: true, + shift: true, + draw_wait: true, + dma_inc: true, + stupid_jumps: true, + } + } else { + Quirks { + bin_ops: false, + shift: false, + draw_wait: false, + dma_inc: false, + stupid_jumps: false, + } + } + } +} + +impl From for Quirks { + fn from(value: Mode) -> Self { + match value { + Mode::Chip8 => false.into(), + Mode::SChip => true.into(), + Mode::XOChip => Self { + bin_ops: true, + shift: false, + draw_wait: true, + dma_inc: false, + stupid_jumps: false, + }, + } + } +} + +impl Default for Quirks { + fn default() -> Self { + Self::from(false) + } +} diff --git a/src/cpu/tests.rs b/src/cpu/tests.rs index ee5ceb7..45f2163 100644 --- a/src/cpu/tests.rs +++ b/src/cpu/tests.rs @@ -1,7 +1,7 @@ // (c) 2023 John A. Breaux // This code is licensed under MIT license (see LICENSE.txt for details) -//! Tests for cpu.rs +//! Unit tests for [super::CPU] //! //! These run instructions, and ensure their output is consistent with previous builds //! @@ -23,7 +23,7 @@ mod decode; fn setup_environment() -> (CPU, Bus) { ( CPU { - flags: ControlFlags { + flags: Flags { debug: true, pause: false, monotonic: Some(8), diff --git a/src/lib.rs b/src/lib.rs index 6ff4e71..3ef8738 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ pub mod error; // Common imports for Chirp pub use bus::{Bus, Read, Region::*, Write}; -pub use cpu::{disassembler::Dis, ControlFlags, Mode, CPU}; +pub use cpu::{disassembler::Dis, flags::Flags, mode::Mode, quirks::Quirks, CPU}; pub use error::Result; /// Holds the state of a Chip-8 diff --git a/tests/chip8_test_suite.rs b/tests/chip8_test_suite.rs index 0d27b25..5cd8fe3 100644 --- a/tests/chip8_test_suite.rs +++ b/tests/chip8_test_suite.rs @@ -4,7 +4,7 @@ pub use chirp::*; fn setup_environment() -> (CPU, Bus) { let mut cpu = CPU::default(); - cpu.flags = ControlFlags { + cpu.flags = Flags { debug: true, pause: false, monotonic: Some(8), diff --git a/tests/integration.rs b/tests/integration.rs index f90e684..e8b7248 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -126,7 +126,7 @@ mod cpu { //#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[test] fn clone() { - let cf1 = ControlFlags { + let cf1 = Flags { debug: false, pause: false, keypause: false, @@ -140,13 +140,13 @@ mod cpu { } #[test] fn debug() { - println!("{:?}", ControlFlags::default()); + println!("{:?}", Flags::default()); } #[test] fn default() { assert_eq!( - ControlFlags::default(), - ControlFlags { + Flags::default(), + Flags { debug: false, pause: false, keypause: false, @@ -157,8 +157,8 @@ mod cpu { } #[test] fn eq() { - let cf1 = ControlFlags::default(); - let cf2 = ControlFlags { + let cf1 = Flags::default(); + let cf2 = Flags { debug: true, pause: true, keypause: true, @@ -169,8 +169,8 @@ mod cpu { } #[test] fn ord() { - let cf1 = ControlFlags::default(); - let cf2 = ControlFlags { + let cf1 = Flags::default(); + let cf2 = Flags { debug: true, pause: true, keypause: true, @@ -178,12 +178,12 @@ mod cpu { ..Default::default() }; assert!(cf1 < cf2); - assert_eq!(ControlFlags::default(), cf1.min(cf2)); + assert_eq!(Flags::default(), cf1.min(cf2)); } #[test] fn hash() { let mut hasher = DefaultHasher::new(); - ControlFlags::default().hash(&mut hasher); + Flags::default().hash(&mut hasher); println!("{:?}", hasher); } } @@ -215,7 +215,7 @@ fn error() { mod quirks { use super::*; - use chirp::cpu::Quirks; + use chirp::cpu::quirks::Quirks; #[test] fn from_true() {