diff --git a/Cargo.toml b/Cargo.toml index 3f6591d..1ce8546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,21 +2,11 @@ name = "chirp" version = "0.1.1" edition = "2021" -default-run = "chirp" +default-run = "chirp-imgui" authors = ["John Breaux"] license = "MIT" publish = false - -[features] -default = ["unstable", "drawille", "minifb"] -unstable = [] -drawille = ["dep:drawille"] -iced = ["dep:iced"] -minifb = ["dep:minifb"] -rhexdump = ["dep:rhexdump"] -serde = ["dep:serde"] - [[bin]] name = "chirp" path = "src/bin/chirp-minifb/main.rs" @@ -27,13 +17,28 @@ name = "chirp-disasm" required-features = ["default"] [[bin]] -name = "chirp-iced" -required-features = ["iced"] +name = "chirp-imgui" +required-features = ["imgui"] [[bin]] name = "chirp-shot-viewer" required-features = ["default", "drawille"] +[features] +default = ["drawille", "imgui", "serde"] +nightly = [] +drawille = ["dep:drawille"] +imgui = [ + "dep:imgui", + "dep:imgui-wgpu", + "dep:imgui-winit-support", + "dep:winit", + "dep:winit_input_helper", + "dep:pixels", +] +minifb = ["dep:minifb"] +rhexdump = ["dep:rhexdump"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.release] opt-level = 3 @@ -48,14 +53,22 @@ overflow-checks = false [dependencies] -drawille = {version = "0.3.0", optional = true} -iced = {version = "0.8.0", optional = true} -rhexdump = {version = "^0.1.1", optional = true } -serde = { version = "^1.0", features = ["derive"], optional = true } -minifb = { version = "^0.24.0", optional = true } -gumdrop = "^0.8.1" +imgui = { version = "^0.10", optional = true } +imgui-winit-support = { version = "^0.10", optional = true } +imgui-wgpu = { version = "^0", optional = true } +pixels = { version = "^0", optional = true } +# TODO: When imgui-winit-support updates to 0.28 (Soon:tm:), update winit and winit_input_helper +winit = { version = "0.27.5", features = ["default", "x11"], optional = true } +winit_input_helper = { version = "^0.13.0", optional = true } + +drawille = { version = "0.3.0", optional = true } +rhexdump = { version = "0.1.1", optional = true } +serde = { version = "1.0.0", features = ["derive"], optional = true } +minifb = { version = "0.24.0", optional = true } + +gumdrop = "0.8.1" imperative-rs = "0.3.1" -owo-colors = "^3" -rand = "^0.8.5" -thiserror = "^1.0.39" +owo-colors = "3" +rand = "0.8.5" +thiserror = "1.0.39" diff --git a/src/cpu.rs b/src/cpu.rs index 08a4ce3..e2f9901 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -6,54 +6,43 @@ #[cfg(test)] mod tests; +pub mod behavior; pub mod bus; -pub mod disassembler; pub mod flags; pub mod instruction; pub mod mode; pub mod quirks; use self::{ - bus::{Bus, Get, ReadWrite, Region}, - disassembler::{Dis, Disassembler, Insn}, + bus::{Bus, Get, ReadWrite, Region::*}, flags::Flags, + instruction::{ + disassembler::{Dis, Disassembler}, + Insn, + }, mode::Mode, quirks::Quirks, }; -use crate::error::{Error, Result}; +use crate::{ + bus, + error::{Error, Result}, +}; use imperative_rs::InstructionSet; use owo_colors::OwoColorize; -use rand::random; -use std::time::Instant; +use std::fmt::Debug; type Reg = usize; type Adr = u16; type Nib = u8; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -struct Timers { - frame: Instant, - insn: Instant, -} - -impl Default for Timers { - fn default() -> Self { - let now = Instant::now(); - Self { - frame: now, - insn: now, - } - } -} - /// Represents the internal state of the CPU interpreter -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub struct CPU { /// Flags that control how the CPU behaves, but which aren't inherent to the /// chip-8. Includes [Quirks], target IPF, etc. pub flags: Flags, // memory map info - screen: Adr, + screen: Bus, font: Adr, // memory stack: Vec, @@ -66,7 +55,6 @@ pub struct CPU { // I/O keys: [bool; 16], // Execution data - timers: Timers, cycle: usize, breakpoints: Vec, disassembler: Dis, @@ -74,7 +62,6 @@ pub struct CPU { // public interface impl CPU { - // TODO: implement From<&bus> for CPU /// Constructs a new CPU, taking all configurable parameters /// # Examples /// ```rust @@ -90,22 +77,36 @@ impl CPU { /// dbg!(cpu); /// ``` pub fn new( - screen: Adr, + rom: impl AsRef, font: Adr, pc: Adr, disassembler: Dis, breakpoints: Vec, flags: Flags, - ) -> Self { - CPU { + ) -> Result { + let mut cpu = CPU { disassembler, - screen, font, pc, breakpoints, flags, ..Default::default() - } + }; + // load the provided rom + cpu.load_program(rom)?; + Ok(cpu) + } + + /// Loads a program into the CPU's program space + pub fn load_program(&mut self, rom: impl AsRef) -> Result<&mut Self> { + self.load_program_bytes(&std::fs::read(rom)?) + } + + /// Loads bytes into the CPU's program space + pub fn load_program_bytes(&mut self, rom: &[u8]) -> Result<&mut Self> { + self.screen.clear_region(Program); + self.screen.load_region(Program, rom)?; + Ok(self) } /// Presses a key, and reports whether the key's state changed. @@ -292,6 +293,35 @@ impl CPU { self.flags.draw_wait = false; } + /// Resets the emulator. + /// + /// Touches the [Flags] (keypause, draw_wait, draw_mode, and lastkey), + /// stack, pc, registers, keys, and cycle count. + /// + /// Does not touch [Quirks], [Mode], [Dis], breakpoints, or memory map. + pub fn reset(&mut self) { + self.flags = Flags { + keypause: false, + draw_wait: false, + draw_mode: false, + lastkey: None, + ..self.flags + }; + // clear the stack + self.stack.truncate(0); + // Reset the program counter + self.pc = 0x200; + // Zero the registers + self.i = 0; + self.v = [0; 16]; + self.delay = 0.0; + self.sound = 0.0; + // I/O + self.keys = [false; 16]; + // Execution data + self.cycle = 0; + } + /// Set a breakpoint // TODO: Unit test this pub fn set_break(&mut self, point: Adr) -> &mut Self { @@ -378,53 +408,15 @@ impl CPU { /// assert_eq!(0x202, cpu.pc()); /// assert_eq!(0x20, cpu.cycle()); /// ``` - pub fn multistep(&mut self, bus: &mut Bus, steps: usize) -> Result<&mut Self> { + pub fn multistep(&mut self, screen: &mut Bus, steps: usize) -> Result<&mut Self> { for _ in 0..steps { - self.tick(bus)?; - self.vertical_blank(); - } - Ok(self) - } - - /// Simulates vertical blanking - /// - /// If monotonic timing is `enabled`: - /// - Ticks the sound and delay timers according to CPU cycle count - /// - Disables framepause - /// If monotonic timing is `disabled`: - /// - Subtracts the elapsed time in fractions of a frame - /// from st/dt - /// - Disables framepause if the duration exceeds that of a frame - #[inline(always)] - pub fn vertical_blank(&mut self) -> &mut Self { - if self.flags.pause { - return self; - } - // Use a monotonic counter when testing - if let Some(speed) = self.flags.monotonic { - if self.flags.draw_wait { - self.flags.draw_wait = self.cycle % speed != 0; - } - let speed = 1.0 / speed as f64; + self.tick(screen)?; + let speed = 1.0 / steps as f64; self.delay -= speed; self.sound -= speed; - return self; - }; - - // Convert the elapsed time to 60ths of a second - let frame = Instant::now(); - let time = (frame - self.timers.frame).as_secs_f64() * 60.0; - self.timers.frame = frame; - if time > 1.0 { - self.flags.draw_wait = false; } - if self.delay > 0.0 { - self.delay -= time; - } - if self.sound > 0.0 { - self.sound -= time; - } - self + self.flags.draw_wait = false; + Ok(self) } /// Executes a single instruction @@ -466,7 +458,7 @@ impl CPU { /// dbg!(cpu.tick(&mut bus)) /// .expect_err("Should return Error::InvalidInstruction { 0xffff }"); /// ``` - pub fn tick(&mut self, bus: &mut Bus) -> Result<&mut Self> { + pub fn tick(&mut self, screen: &mut Bus) -> Result<&mut Self> { // Do nothing if paused if self.flags.is_paused() { // always tick in test mode @@ -505,25 +497,21 @@ impl CPU { } // decode opcode - if let Ok((inc, insn)) = Insn::decode(opcode) { + if let Ok((inc, insn)) = Insn::decode(opchunk) { self.pc = self.pc.wrapping_add(inc as u16); - self.execute(bus, insn); + self.execute(screen, insn); } else { return Err(Error::UnimplementedInstruction { word: u16::from_be_bytes(*opcode), }); } - if self.flags.debug { - println!("{:?}", self.timers.insn.elapsed().bright_black()); - } - // process breakpoints if !self.breakpoints.is_empty() && self.breakpoints.contains(&self.pc) { self.flags.pause = true; return Err(Error::BreakpointHit { addr: self.pc, - next: bus.read(self.pc), + next: self.screen.read(self.pc), }); } Ok(self) @@ -572,6 +560,25 @@ impl CPU { } } +impl Debug for CPU { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CPU") + .field("flags", &self.flags) + .field("font", &self.font) + .field("stack", &self.stack) + .field("pc", &self.pc) + .field("i", &self.i) + .field("v", &self.v) + .field("delay", &self.delay) + .field("sound", &self.sound) + .field("keys", &self.keys) + .field("cycle", &self.cycle) + .field("breakpoints", &self.breakpoints) + .field("disassembler", &self.disassembler) + .finish_non_exhaustive() + } +} + impl Default for CPU { /// Constructs a new CPU with sane defaults and debug mode ON /// @@ -589,7 +596,10 @@ impl Default for CPU { fn default() -> Self { CPU { stack: vec![], - screen: 0xf00, + screen: bus! { + Charset [0x0050..0x00a0] = include_bytes!("mem/charset.bin"), + Program [0x0200..0x1000], + }, font: 0x050, pc: 0x200, i: 0, @@ -602,7 +612,6 @@ impl Default for CPU { debug: true, ..Default::default() }, - timers: Default::default(), breakpoints: vec![], disassembler: Dis::default(), } diff --git a/src/cpu/behavior.rs b/src/cpu/behavior.rs new file mode 100644 index 0000000..99e54c5 --- /dev/null +++ b/src/cpu/behavior.rs @@ -0,0 +1,669 @@ +// (c) 2023 John A. Breaux +// This code is licensed under MIT license (see LICENSE for details) + +//! Contains implementations for each Chip-8 [Insn] + +use super::{bus::Region, *}; +use rand::random; + +impl CPU { + /// Executes a single [Insn] + #[rustfmt::skip] + #[inline(always)] + pub(super) fn execute(&mut self, screen: &mut Bus, instruction: Insn) { + match instruction { + // Core Chip-8 instructions + Insn::cls => self.clear_screen(screen), + Insn::ret => self.ret(), + Insn::jmp { A } => self.jump(A), + Insn::call { A } => self.call(A), + 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, screen), + 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), + Insn::dmao { x } => self.store_dma(x), + Insn::dmai { x } => self.load_dma(x), + // Super-Chip extensions + Insn::scd { n } => self.scroll_down(n, screen), + Insn::scr => self.scroll_right(screen), + Insn::scl => self.scroll_left(screen), + Insn::halt => self.flags.pause(), + Insn::lores => self.init_lores(screen), + Insn::hires => self.init_hires(screen), + Insn::hfont { x } => self.load_big_sprite(x), + Insn::flgo { x } => self.store_flags(x), + Insn::flgi { x } => self.load_flags(x), + // XO-Chip extensions + _ => unimplemented!(), + } + } +} + +/// |`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) { + self.pc = self.stack.pop().unwrap_or(0x200); + } +} + +/// Super Chip screen-control routines +/// +/// |opcode| effect | +/// |------|------------------------------------| +/// |`00cN`| Scroll the screen down N lines | +/// |`00fb`| Scroll the screen right | +/// |`00fc`| Scroll the screen left | +/// |`00fe`| Initialize lores mode | +/// |`00ff`| Initialize hires mode | +impl CPU { + /// # |`00cN`| + /// Scroll the screen down N lines + #[inline(always)] + pub(super) fn scroll_down(&mut self, n: Nib, screen: &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 line: u128 = screen.read(i); + screen.write(i - (n as usize * 16), 0u128); + screen.write(i, line); + } + } + false => { + // Get a line from the bus + for i in (0..8 * (32 - n as usize)).step_by(8).rev() { + let line: u64 = screen.read(i); + screen.write(i, 0u64); + screen.write(i + (n as usize * 8), line); + } + } + } + } + + /// # |`00fb`| + /// Scroll the screen right + #[inline(always)] + pub(super) fn scroll_right(&mut self, screen: &mut impl ReadWrite) { + // Get a line from the bus + for i in (0..16 * 64_usize).step_by(16) { + //let line: u128 = bus.read(self.screen + i) >> 4; + screen.write(i, screen.read(i) >> 4); + } + } + /// # |`00fc`| + /// Scroll the screen left + #[inline(always)] + pub(super) fn scroll_left(&mut self, screen: &mut impl ReadWrite) { + // Get a line from the bus + for i in (0..16 * 64_usize).step_by(16) { + let line: u128 = u128::wrapping_shl(screen.read(i), 4); + screen.write(i, line); + } + } + /// # |`00fe`| + /// Initialize lores mode + pub(super) fn init_lores(&mut self, screen: &mut Bus) { + self.flags.draw_mode = false; + screen.set_region(Region::Screen, 0..256); + self.clear_screen(screen); + } + /// # |`00ff`| + /// Initialize hires mode + pub(super) fn init_hires(&mut self, screen: &mut Bus) { + self.flags.draw_mode = true; + screen.set_region(Region::Screen, 0..1024); + self.clear_screen(screen); + } +} + +/// |`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) { + self.stack.push(self.pc); + 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, screen: &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, screen); + } else { + self.draw_lores(x, y, n, screen); + } + } + + /// |`Dxyn`| Chip-8: Draws n-byte sprite to the screen at coordinates (vX, vY) + #[inline(always)] + pub(super) fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, scr: &mut Bus) { + self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, scr); + } + + #[inline(always)] + pub(super) fn draw_sprite(&mut self, x: u16, y: u16, n: Nib, w: u16, h: u16, screen: &mut Bus) { + let w_bytes = w / 8; + self.v[0xf] = 0; + if let Some(sprite) = self + .screen + .get(self.i as usize..(self.i + n as u16) as usize) + { + for (line, &sprite) in sprite.iter().enumerate() { + let line = line as u16; + let sprite = ((sprite as u16) << (8 - (x % 8))).to_be_bytes(); + for (addr, &byte) in sprite.iter().enumerate().filter_map(|(idx, byte)| { + let x = (x / 8) + idx as u16; + Some(( + if self.flags.quirks.screen_wrap { + ((y + line) % h * w_bytes + (x % w_bytes)) % (w_bytes * h) + } else if x < w_bytes { + (y + line) * w_bytes + x + } else { + return None; + }, + byte, + )) + }) { + let display: u8 = screen.read(addr); + screen.write(addr, byte ^ display); + if byte & display != 0 { + self.v[0xf] = 1; + } + } + } + } + } +} +/// |`Dxyn`| Super-Chip extension high-resolution graphics mode +impl CPU { + /// |`Dxyn`| Super-Chip extension high-resolution graphics mode + #[inline(always)] + pub(super) fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, screen: &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, screen), + _ => self.draw_sprite(x, y, n, w, h, screen), + } + } + /// Draws a 16x16 Super Chip sprite + #[inline(always)] + pub(super) fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, screen: &mut Bus) { + self.v[0xf] = 0; + let w_bytes = w / 8; + if let Some(sprite) = self.screen.get(self.i as usize..(self.i + 32) as usize) { + let sprite = sprite.to_owned(); + for (line, sprite) in sprite.chunks_exact(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; + let sprite = (sprite as u32) << (16 - (x % 8)); + let display: u32 = screen.read(addr); + screen.write(addr, display ^ sprite); + if display & 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) { + let x = self.v[x]; + self.screen.write(self.i.wrapping_add(2), x % 10); + self.screen.write(self.i.wrapping_add(1), x / 10 % 10); + self.screen.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) { + let i = self.i as usize; + for (reg, value) in self + .screen + .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) { + let i = self.i as usize; + for (reg, value) in self + .screen + .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; + } + } +} + +/// |`Fxbb`| Super Chip: Performs IO +/// +/// |opcode| effect | +/// |------|------------------------------------| +/// |`Fx30`| 16x16 equivalent of load_sprite | +/// |`Fx75`| Save to "flag registers" | +/// |`Fx85`| Load from "flag registers" | +impl CPU { + /// |`Fx30`| (Super-Chip) 16x16 equivalent of [CPU::load_sprite] + /// + /// 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) { + // TODO: Save these, maybe + for (reg, value) in self + .screen + .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) { + for (reg, value) in self + .screen + .get(0..=x) + .unwrap_or_default() + .iter() + .enumerate() + { + self.v[reg] = *value; + } + } +} diff --git a/src/cpu/disassembler.rs b/src/cpu/disassembler.rs deleted file mode 100644 index 94ec4ad..0000000 --- a/src/cpu/disassembler.rs +++ /dev/null @@ -1,230 +0,0 @@ -//! A disassembler for Chip-8 opcodes -#![allow(clippy::bad_bit_mask)] -use imperative_rs::InstructionSet; -use owo_colors::{OwoColorize, Style}; -use std::fmt::Display; - -/// Disassembles Chip-8 instructions -pub trait Disassembler { - /// Disassemble a single instruction - fn once(&self, insn: u16) -> String; -} - -#[allow(non_camel_case_types, non_snake_case, missing_docs)] -#[derive(Clone, Copy, Debug, InstructionSet, PartialEq, Eq)] -/// Implements a Disassembler using imperative_rs -pub enum Insn { - // Base instruction set - /// | 00e0 | Clear screen memory to 0s - #[opcode = "0x00e0"] - cls, - /// | 00ee | Return from subroutine - #[opcode = "0x00ee"] - ret, - /// | 1aaa | Jumps to an absolute address - #[opcode = "0x1AAA"] - jmp { A: u16 }, - /// | 2aaa | Pushes pc onto the stack, then jumps to a - #[opcode = "0x2AAA"] - call { A: u16 }, - /// | 3xbb | Skips next instruction if register X == b - #[opcode = "0x3xBB"] - seb { B: u8, x: usize }, - /// | 4xbb | Skips next instruction if register X != b - #[opcode = "0x4xBB"] - sneb { B: u8, x: usize }, - /// | 9XY0 | Skip next instruction if vX == vY | - #[opcode = "0x5xy0"] - se { y: usize, x: usize }, - /// | 6xbb | Loads immediate byte b into register vX - #[opcode = "0x6xBB"] - movb { B: u8, x: usize }, - /// | 7xbb | Adds immediate byte b to register vX - #[opcode = "0x7xBB"] - addb { B: u8, x: usize }, - /// | 8xy0 | Loads the value of y into x - #[opcode = "0x8xy0"] - mov { x: usize, y: usize }, - /// | 8xy1 | Performs bitwise or of vX and vY, and stores the result in vX - #[opcode = "0x8xy1"] - or { y: usize, x: usize }, - /// | 8xy2 | Performs bitwise and of vX and vY, and stores the result in vX - #[opcode = "0x8xy2"] - and { y: usize, x: usize }, - /// | 8xy3 | Performs bitwise xor of vX and vY, and stores the result in vX - #[opcode = "0x8xy3"] - xor { y: usize, x: usize }, - /// | 8xy4 | Performs addition of vX and vY, and stores the result in vX - #[opcode = "0x8xy4"] - add { y: usize, x: usize }, - /// | 8xy5 | Performs subtraction of vX and vY, and stores the result in vX - #[opcode = "0x8xy5"] - sub { y: usize, x: usize }, - /// | 8xy6 | Performs bitwise right shift of vX (or vY) - #[opcode = "0x8xy6"] - shr { y: usize, x: usize }, - /// | 8xy7 | Performs subtraction of vY and vX, and stores the result in vX - #[opcode = "0x8xy7"] - bsub { y: usize, x: usize }, - /// | 8xyE | Performs bitwise left shift of vX - #[opcode = "0x8xye"] - shl { y: usize, x: usize }, - /// | 9XY0 | Skip next instruction if vX != vY - #[opcode = "0x9xy0"] - sne { y: usize, x: usize }, - /// | Aaaa | Load address #a into register I - #[opcode = "0xaAAA"] - movI { A: u16 }, - /// | Baaa | Jump to &adr + v0 - #[opcode = "0xbAAA"] - jmpr { A: u16 }, - /// | Cxbb | Stores a random number & the provided byte into vX - #[opcode = "0xcxBB"] - rand { B: u8, x: usize }, - /// | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY) - #[opcode = "0xdxyn"] - draw { y: usize, x: usize, n: u8 }, - /// | eX9e | Skip next instruction if key == vX - #[opcode = "0xex9e"] - sek { x: usize }, - /// | eXa1 | Skip next instruction if key != vX - #[opcode = "0xexa1"] - snek { x: usize }, - // | fX07 | Set vX to value in delay timer - #[opcode = "0xfx07"] - getdt { x: usize }, - // | fX0a | Wait for input, store key in vX - #[opcode = "0xfx0a"] - waitk { x: usize }, - // | fX15 | Set sound timer to the value in vX - #[opcode = "0xfx15"] - setdt { x: usize }, - // | fX18 | set delay timer to the value in vX - #[opcode = "0xfx18"] - movst { x: usize }, - // | fX1e | Add vX to I - #[opcode = "0xfx1e"] - addI { x: usize }, - // | fX29 | Load sprite for character x into I - #[opcode = "0xfx29"] - font { x: usize }, - // | fX33 | BCD convert X into I[0..3] - #[opcode = "0xfx33"] - bcd { x: usize }, - // | fX55 | DMA Stor from I to registers 0..X - #[opcode = "0xfx55"] - dmao { x: usize }, - // | fX65 | DMA Load from I to registers 0..X - #[opcode = "0xfx65"] - dmai { x: usize }, - - // Super Chip extensions - /// | 00cN | Scroll the screen down - #[opcode = "0x00cn"] - scd { n: u8 }, - /// | 00fb | Scroll the screen right - #[opcode = "0x00fb"] - scr, - /// | 00fc | Scroll the screen left - #[opcode = "0x00fc"] - scl, - /// | 00fd | Exit (halt and catch fire) - #[opcode = "0x00fd"] - halt, - /// | 00fe | Return to low-resolution mode - #[opcode = "0x00fe"] - lores, - /// | 00ff | Enter high-resolution mode - #[opcode = "0x00ff"] - hires, - /// | fx30 | Enter high-resolution mode - #[opcode = "0xfx30"] - hfont { x: usize }, - /// | fx75 | Save to "flag registers" - #[opcode = "0xfx75"] - flgo { x: usize }, - /// | fx85 | Load from "flag registers" - #[opcode = "0xfx85"] - flgi { x: usize }, -} - -impl Display for Insn { - #[rustfmt::skip] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - // Base instruction set - Insn::cls => write!(f, "cls "), - Insn::ret => write!(f, "ret "), - Insn::jmp { A } => write!(f, "jmp {A:03x}"), - Insn::call { A } => write!(f, "call {A:03x}"), - Insn::seb { B, x } => write!(f, "se #{B:02x}, v{x:X}"), - Insn::sneb { B, x } => write!(f, "sne #{B:02x}, v{x:X}"), - Insn::se { y, x } => write!(f, "se v{y:X}, v{x:X}"), - Insn::movb { B, x } => write!(f, "mov #{B:02x}, v{x:X}"), - Insn::addb { B, x } => write!(f, "add #{B:02x}, v{x:X}"), - Insn::mov { x, y } => write!(f, "mov v{y:X}, v{x:X}"), - Insn::or { y, x } => write!(f, "or v{y:X}, v{x:X}"), - Insn::and { y, x } => write!(f, "and v{y:X}, v{x:X}"), - Insn::xor { y, x } => write!(f, "xor v{y:X}, v{x:X}"), - Insn::add { y, x } => write!(f, "add v{y:X}, v{x:X}"), - Insn::sub { y, x } => write!(f, "sub v{y:X}, v{x:X}"), - Insn::shr { y, x } => write!(f, "shr v{y:X}, v{x:X}"), - Insn::bsub { y, x } => write!(f, "bsub v{y:X}, v{x:X}"), - Insn::shl { y, x } => write!(f, "shl v{y:X}, v{x:X}"), - Insn::sne { y, x } => write!(f, "sne v{y:X}, v{x:X}"), - Insn::movI { A } => write!(f, "mov ${A:03x}, I"), - Insn::jmpr { A } => write!(f, "jmp ${A:03x}+v0"), - Insn::rand { B, x } => write!(f, "rand #{B:02x}, v{x:X}"), - Insn::draw { y, x, n } => write!(f, "draw #{n:x}, v{x:X}, v{y:X}"), - Insn::sek { x } => write!(f, "sek v{x:X}"), - Insn::snek { x } => write!(f, "snek v{x:X}"), - Insn::getdt { x } => write!(f, "mov DT, v{x:X}"), - Insn::waitk { x } => write!(f, "waitk v{x:X}"), - Insn::setdt { x } => write!(f, "mov v{x:X}, DT"), - Insn::movst { x } => write!(f, "mov v{x:X}, ST"), - Insn::addI { x } => write!(f, "add v{x:X}, I"), - Insn::font { x } => write!(f, "font v{x:X}, I"), - Insn::bcd { x } => write!(f, "bcd v{x:X}, &I"), - Insn::dmao { x } => write!(f, "dmao v{x:X}"), - Insn::dmai { x } => write!(f, "dmai v{x:X}"), - // Super Chip extensions - Insn::scd { n } => write!(f, "scd #{n:x}"), - Insn::scr => write!(f, "scr "), - Insn::scl => write!(f, "scl "), - Insn::halt => write!(f, "halt "), - Insn::lores => write!(f, "lores "), - Insn::hires => write!(f, "hires "), - Insn::hfont { x } => write!(f, "hfont v{x:X}"), - Insn::flgo { x } => write!(f, "flgo v{x:X}"), - Insn::flgi { x } => write!(f, "flgi v{x:X}"), - } - } -} - -/// Disassembles Chip-8 instructions, printing them in the provided [owo_colors::Style]s -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Dis { - /// Styles invalid instructions - pub invalid: Style, - /// Styles valid instruction - pub normal: Style, -} - -impl Default for Dis { - fn default() -> Self { - Self { - invalid: Style::new().bold().red(), - normal: Style::new().green(), - } - } -} - -impl Disassembler for Dis { - fn once(&self, insn: u16) -> String { - if let Ok((_, insn)) = Insn::decode(&insn.to_be_bytes()) { - format!("{}", insn.style(self.normal)) - } else { - format!("{}", format_args!("inval {insn:04x}").style(self.invalid)) - } - } -} diff --git a/src/cpu/instruction.rs b/src/cpu/instruction.rs index a71fa87..58cbd9c 100644 --- a/src/cpu/instruction.rs +++ b/src/cpu/instruction.rs @@ -1,632 +1,221 @@ // (c) 2023 John A. Breaux -// This code is licensed under MIT license (see LICENSE.txt for details) +// This code is licensed under MIT license (see LICENSE for details) +#![allow(clippy::bad_bit_mask)] +//! Contains the definition of a Chip-8 [Insn] -//! Contains implementations for each [Insn] as private member functions of [CPU] +pub mod disassembler; -use super::*; +use imperative_rs::InstructionSet; +use std::fmt::Display; -impl CPU { - /// Executes a single [Insn] - #[inline(always)] + +#[allow(non_camel_case_types, non_snake_case, missing_docs)] +#[derive(Clone, Copy, Debug, InstructionSet, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +/// Implements a Disassembler using imperative_rs +pub enum Insn { + // Base instruction set + /// | 00e0 | Clear screen memory to 0s + #[opcode = "0x00e0"] + cls, + /// | 00ee | Return from subroutine + #[opcode = "0x00ee"] + ret, + /// | 1aaa | Jumps to an absolute address + #[opcode = "0x1AAA"] + jmp { A: u16 }, + /// | 2aaa | Pushes pc onto the stack, then jumps to a + #[opcode = "0x2AAA"] + call { A: u16 }, + /// | 3xbb | Skips next instruction if register X == b + #[opcode = "0x3xBB"] + seb { B: u8, x: usize }, + /// | 4xbb | Skips next instruction if register X != b + #[opcode = "0x4xBB"] + sneb { B: u8, x: usize }, + /// | 9XY0 | Skip next instruction if vX == vY | + #[opcode = "0x5xy0"] + se { y: usize, x: usize }, + /// | 6xbb | Loads immediate byte b into register vX + #[opcode = "0x6xBB"] + movb { B: u8, x: usize }, + /// | 7xbb | Adds immediate byte b to register vX + #[opcode = "0x7xBB"] + addb { B: u8, x: usize }, + /// | 8xy0 | Loads the value of y into x + #[opcode = "0x8xy0"] + mov { x: usize, y: usize }, + /// | 8xy1 | Performs bitwise or of vX and vY, and stores the result in vX + #[opcode = "0x8xy1"] + or { y: usize, x: usize }, + /// | 8xy2 | Performs bitwise and of vX and vY, and stores the result in vX + #[opcode = "0x8xy2"] + and { y: usize, x: usize }, + /// | 8xy3 | Performs bitwise xor of vX and vY, and stores the result in vX + #[opcode = "0x8xy3"] + xor { y: usize, x: usize }, + /// | 8xy4 | Performs addition of vX and vY, and stores the result in vX + #[opcode = "0x8xy4"] + add { y: usize, x: usize }, + /// | 8xy5 | Performs subtraction of vX and vY, and stores the result in vX + #[opcode = "0x8xy5"] + sub { y: usize, x: usize }, + /// | 8xy6 | Performs bitwise right shift of vX (or vY) + #[opcode = "0x8xy6"] + shr { y: usize, x: usize }, + /// | 8xy7 | Performs subtraction of vY and vX, and stores the result in vX + #[opcode = "0x8xy7"] + bsub { y: usize, x: usize }, + /// | 8xyE | Performs bitwise left shift of vX + #[opcode = "0x8xye"] + shl { y: usize, x: usize }, + /// | 9XY0 | Skip next instruction if vX != vY + #[opcode = "0x9xy0"] + sne { y: usize, x: usize }, + /// | Aaaa | Load address #a into register I + #[opcode = "0xaAAA"] + movI { A: u16 }, + /// | Baaa | Jump to &adr + v0 + #[opcode = "0xbAAA"] + jmpr { A: u16 }, + /// | Cxbb | Stores a random number & the provided byte into vX + #[opcode = "0xcxBB"] + rand { B: u8, x: usize }, + /// | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY) + #[opcode = "0xdxyn"] + draw { y: usize, x: usize, n: u8 }, + /// | eX9e | Skip next instruction if key == vX + #[opcode = "0xex9e"] + sek { x: usize }, + /// | eXa1 | Skip next instruction if key != vX + #[opcode = "0xexa1"] + snek { x: usize }, + /// | fX07 | Set vX to value in delay timer + #[opcode = "0xfx07"] + getdt { x: usize }, + /// | fX0a | Wait for input, store key in vX + #[opcode = "0xfx0a"] + waitk { x: usize }, + /// | fX15 | Set sound timer to the value in vX + #[opcode = "0xfx15"] + setdt { x: usize }, + /// | fX18 | set delay timer to the value in vX + #[opcode = "0xfx18"] + movst { x: usize }, + /// | fX1e | Add vX to I + #[opcode = "0xfx1e"] + addI { x: usize }, + /// | fX29 | Load sprite for character x into I + #[opcode = "0xfx29"] + font { x: usize }, + /// | fX33 | BCD convert X into I[0..3] + #[opcode = "0xfx33"] + bcd { x: usize }, + /// | fX55 | DMA Stor from I to registers 0..X + #[opcode = "0xfx55"] + dmao { x: usize }, + /// | fX65 | DMA Load from I to registers 0..X + #[opcode = "0xfx65"] + dmai { x: usize }, + + // Super Chip extensions + /// | 00cN | Scroll the screen down + #[opcode = "0x00cn"] + scd { n: u8 }, + /// | 00fb | Scroll the screen right + #[opcode = "0x00fb"] + scr, + /// | 00fc | Scroll the screen left + #[opcode = "0x00fc"] + scl, + /// | 00fd | Exit (halt and catch fire) + #[opcode = "0x00fd"] + halt, + /// | 00fe | Return to low-resolution mode + #[opcode = "0x00fe"] + lores, + /// | 00ff | Enter high-resolution mode + #[opcode = "0x00ff"] + hires, + /// | fx30 | Enter high-resolution mode + #[opcode = "0xfx30"] + hfont { x: usize }, + /// | fx75 | Save to "flag registers" + #[opcode = "0xfx75"] + flgo { x: usize }, + /// | fx85 | Load from "flag registers" + #[opcode = "0xfx85"] + flgi { x: usize }, + + // XO-Chip instructions + /// | 00dN | Scroll the screen up + #[opcode = "0x00dn"] + scu { n: u8 }, + /// | 5XY2 | DMA Load from I to vX..vY + #[opcode = "0x5xy2"] + dmaro { y: usize, x: usize }, + /// | 5XY3 | DMA Load from I to vX..vY + #[opcode = "0x5xy3"] + dmari { y: usize, x: usize }, + /// | F000 | Load long address into character I + #[opcode = "0xf000_iiii"] + long { i: usize }, +} + +impl Display for Insn { #[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(), - Insn::jmp { A } => self.jump(A), - Insn::call { A } => self.call(A), - 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), - } - } -} - -// |`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) { - self.pc = self.stack.pop().unwrap_or(0x200); - } -} - -// |`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) { - self.stack.push(self.pc); - 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; - let sprite = ((sprite as u16) << (8 - (x % 8))).to_be_bytes(); - for (addr, &byte) in sprite.iter().enumerate().filter_map(|(idx, byte)| { - let x = (x / 8) + idx as u16; - Some(( - if self.flags.quirks.screen_wrap { - ((y + line) % h * w_bytes + (x % w_bytes)) % (w_bytes * h) - } else if x < w_bytes { - (y + line) * w_bytes + x - } else { - return None; - } + self.screen, - byte, - )) - }) { - let screen: u8 = bus.read(addr); - bus.write(addr, byte ^ screen); - if byte & screen != 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 ReadWrite + ReadWrite)) { - // 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 ReadWrite + ReadWrite)) { - // 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_exact(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); + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + // Base instruction set + Insn::cls => write!(f, "cls "), + Insn::ret => write!(f, "ret "), + Insn::jmp { A } => write!(f, "jmp {A:03x}"), + Insn::call { A } => write!(f, "call {A:03x}"), + Insn::seb { B, x } => write!(f, "se #{B:02x}, v{x:X}"), + Insn::sneb { B, x } => write!(f, "sne #{B:02x}, v{x:X}"), + Insn::se { y, x } => write!(f, "se v{y:X}, v{x:X}"), + Insn::movb { B, x } => write!(f, "mov #{B:02x}, v{x:X}"), + Insn::addb { B, x } => write!(f, "add #{B:02x}, v{x:X}"), + Insn::mov { x, y } => write!(f, "mov v{y:X}, v{x:X}"), + Insn::or { y, x } => write!(f, "or v{y:X}, v{x:X}"), + Insn::and { y, x } => write!(f, "and v{y:X}, v{x:X}"), + Insn::xor { y, x } => write!(f, "xor v{y:X}, v{x:X}"), + Insn::add { y, x } => write!(f, "add v{y:X}, v{x:X}"), + Insn::sub { y, x } => write!(f, "sub v{y:X}, v{x:X}"), + Insn::shr { y, x } => write!(f, "shr v{y:X}, v{x:X}"), + Insn::bsub { y, x } => write!(f, "bsub v{y:X}, v{x:X}"), + Insn::shl { y, x } => write!(f, "shl v{y:X}, v{x:X}"), + Insn::sne { y, x } => write!(f, "sne v{y:X}, v{x:X}"), + Insn::movI { A } => write!(f, "mov ${A:03x}, I"), + Insn::jmpr { A } => write!(f, "jmp ${A:03x}+v0"), + Insn::rand { B, x } => write!(f, "rand #{B:02x}, v{x:X}"), + Insn::draw { y, x, n } => write!(f, "draw #{n:x}, v{x:X}, v{y:X}"), + Insn::sek { x } => write!(f, "sek v{x:X}"), + Insn::snek { x } => write!(f, "snek v{x:X}"), + Insn::getdt { x } => write!(f, "mov DT, v{x:X}"), + Insn::waitk { x } => write!(f, "waitk v{x:X}"), + Insn::setdt { x } => write!(f, "mov v{x:X}, DT"), + Insn::movst { x } => write!(f, "mov v{x:X}, ST"), + Insn::addI { x } => write!(f, "add v{x:X}, I"), + Insn::font { x } => write!(f, "font v{x:X}, I"), + Insn::bcd { x } => write!(f, "bcd v{x:X}, &I"), + Insn::dmao { x } => write!(f, "dmao v{x:X}"), + Insn::dmai { x } => write!(f, "dmai v{x:X}"), + // Super Chip extensions + Insn::scd { n } => write!(f, "scd #{n:x}"), + Insn::scr => write!(f, "scr "), + Insn::scl => write!(f, "scl "), + Insn::halt => write!(f, "halt "), + Insn::lores => write!(f, "lores "), + Insn::hires => write!(f, "hires "), + Insn::hfont { x } => write!(f, "hfont v{x:X}"), + Insn::flgo { x } => write!(f, "flgo v{x:X}"), + Insn::flgi { x } => write!(f, "flgi v{x:X}"), + // XO-Chip extensions + Insn::scu { n } => write!(f, "scu #{n:x}"), + Insn::dmaro { y, x } => write!(f, "dmaro v{x:X}..v{y:X}"), + Insn::dmari { y, x } => write!(f, "dmari v{x:X}..v{y:X}"), + Insn::long { i } => write!(f, "long ${i:04x}"), + } } } diff --git a/src/cpu/instruction/disassembler.rs b/src/cpu/instruction/disassembler.rs new file mode 100644 index 0000000..1c4fff9 --- /dev/null +++ b/src/cpu/instruction/disassembler.rs @@ -0,0 +1,41 @@ +// (c) 2023 John A. Breaux +// This code is licensed under MIT license (see LICENSE for details) + +//! A disassembler for Chip-8 opcodes +use super::Insn; +use imperative_rs::InstructionSet; +use owo_colors::{OwoColorize, Style}; + +/// Disassembles Chip-8 instructions +pub trait Disassembler { + /// Disassemble a single instruction + fn once(&self, insn: u16) -> String; +} + +/// Disassembles Chip-8 instructions, printing them in the provided [owo_colors::Style]s +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Dis { + /// Styles invalid instructions + pub invalid: Style, + /// Styles valid instruction + pub normal: Style, +} + +impl Default for Dis { + fn default() -> Self { + Self { + invalid: Style::new().bold().red(), + normal: Style::new().green(), + } + } +} + +impl Disassembler for Dis { + fn once(&self, insn: u16) -> String { + if let Ok((_, insn)) = Insn::decode(&insn.to_be_bytes()) { + format!("{}", insn.style(self.normal)) + } else { + format!("{}", format_args!("inval {insn:04x}").style(self.invalid)) + } + } +} diff --git a/src/cpu/tests.rs b/src/cpu/tests.rs index 72be40d..8c16110 100644 --- a/src/cpu/tests.rs +++ b/src/cpu/tests.rs @@ -17,11 +17,12 @@ use crate::{ bus, cpu::bus::{Bus, Region::*}, }; +use rand::random; mod decode; fn setup_environment() -> (CPU, Bus) { - ( + let mut ch8 = ( CPU { flags: Flags { debug: true, @@ -32,14 +33,14 @@ fn setup_environment() -> (CPU, Bus) { ..CPU::default() }, bus! { - // Load the charset into ROM - Charset [0x0050..0x00A0] = include_bytes!("../mem/charset.bin"), - // Load the ROM file into RAM (dummy binary which contains nothing but `jmp pc+2`) - Program [0x0200..0x1000] = include_bytes!("tests/roms/jumptest.ch8"), // Create a screen Screen [0x0F00..0x1000] = include_bytes!("../../chip8Archive/roms/1dcell.ch8"), }, - ) + ); + ch8.0 + .load_program_bytes(include_bytes!("tests/roms/jumptest.ch8")) + .unwrap(); + ch8 } fn print_screen(bytes: &[u8]) { @@ -54,28 +55,28 @@ mod unimplemented { #[test] fn ins_5xyn() { let (mut cpu, mut bus) = setup_environment(); - bus.write(0x200u16, 0x500fu16); + cpu.screen.write(0x200u16, 0x500fu16); cpu.tick(&mut bus) .expect_err("0x500f is not an instruction"); } #[test] fn ins_8xyn() { let (mut cpu, mut bus) = setup_environment(); - bus.write(0x200u16, 0x800fu16); + cpu.screen.write(0x200u16, 0x800fu16); cpu.tick(&mut bus) .expect_err("0x800f is not an instruction"); } #[test] fn ins_9xyn() { let (mut cpu, mut bus) = setup_environment(); - bus.write(0x200u16, 0x900fu16); + cpu.screen.write(0x200u16, 0x900fu16); cpu.tick(&mut bus) .expect_err("0x900f is not an instruction"); } #[test] fn ins_exbb() { let (mut cpu, mut bus) = setup_environment(); - bus.write(0x200u16, 0xe00fu16); + cpu.screen.write(0x200u16, 0xe00fu16); cpu.tick(&mut bus) .expect_err("0xe00f is not an instruction"); } @@ -83,7 +84,7 @@ mod unimplemented { #[test] fn ins_fxbb() { let (mut cpu, mut bus) = setup_environment(); - bus.write(0x200u16, 0xf00fu16); + cpu.screen.write(0x200u16, 0xf00fu16); cpu.tick(&mut bus) .expect_err("0xf00f is not an instruction"); } @@ -755,10 +756,10 @@ mod io { // Debug mode is 5x slower cpu.flags.debug = false; // Load the test program - bus = bus.load_region_owned(Program, test.program); + cpu.screen.load_region(Program, test.program).unwrap(); // Run the test program for the specified number of steps while cpu.cycle() < test.steps { - cpu.multistep(&mut bus, test.steps - cpu.cycle()) + cpu.multistep(&mut bus, 10.min(test.steps - cpu.cycle())) .expect("Draw tests should not contain undefined instructions"); } // Compare the screen to the reference screen buffer @@ -948,7 +949,7 @@ mod io { /// Fx29: Load sprite for character vX into I #[test] fn load_sprite() { - let (mut cpu, bus) = setup_environment(); + let (mut cpu, _) = setup_environment(); for test in TESTS { let reg = 0xf & random::(); // load number into CPU register @@ -958,7 +959,8 @@ mod io { let addr = cpu.i as usize; assert_eq!( - bus.get(addr..addr.wrapping_add(5)) + cpu.screen + .get(addr..addr.wrapping_add(5)) .expect("Region at addr should exist!"), test.output, ); @@ -995,15 +997,18 @@ mod io { #[test] fn bcd_convert() { for test in BCD_TESTS { - let (mut cpu, mut bus) = setup_environment(); + let (mut cpu, _) = setup_environment(); let addr = 0xff0 & random::() as usize; // load CPU registers cpu.i = addr as u16; cpu.v[5] = test.input; - cpu.bcd_convert(5, &mut bus); + cpu.bcd_convert(5); - assert_eq!(bus.get(addr..addr.saturating_add(3)), Some(test.output)) + assert_eq!( + cpu.screen.get(addr..addr.saturating_add(3)), + Some(test.output) + ) } } } @@ -1012,7 +1017,7 @@ mod io { // TODO: Test with dma_inc quirk set #[test] fn dma_store() { - let (mut cpu, mut bus) = setup_environment(); + let (mut cpu, _) = setup_environment(); const DATA: &[u8] = b"ABCDEFGHIJKLMNOP"; // Load some test data into memory let addr = 0x456; @@ -1023,9 +1028,10 @@ mod io { for len in 0..16 { // Perform DMA store cpu.i = addr as u16; - cpu.store_dma(len, &mut bus); + cpu.store_dma(len); // Check that bus grabbed the correct data - let bus = bus + let bus = cpu + .screen .get_mut(addr..addr + DATA.len()) .expect("Getting a mutable slice at addr 0x0456 should not fail"); assert_eq!(bus[0..=len], DATA[0..=len]); @@ -1039,18 +1045,19 @@ mod io { // TODO: Test with dma_inc quirk set #[test] fn dma_load() { - let (mut cpu, mut bus) = setup_environment(); + let (mut cpu, _) = setup_environment(); const DATA: &[u8] = b"ABCDEFGHIJKLMNOP"; // Load some test data into memory let addr = 0x456; - bus.get_mut(addr..addr + DATA.len()) + cpu.screen + .get_mut(addr..addr + DATA.len()) .expect("Getting a mutable slice at addr 0x0456..0x0466 should not fail") .write_all(DATA) .unwrap(); for len in 0..16 { // Perform DMA load cpu.i = addr as u16; - cpu.load_dma(len, &mut bus); + cpu.load_dma(len); // Check that registers grabbed the correct data assert_eq!(cpu.v[0..=len], DATA[0..=len]); assert_eq!(cpu.v[len + 1..], [0; 16][len + 1..]); diff --git a/src/cpu/tests/decode.rs b/src/cpu/tests/decode.rs index 9d247d7..5f14970 100644 --- a/src/cpu/tests/decode.rs +++ b/src/cpu/tests/decode.rs @@ -1,3 +1,6 @@ +// (c) 2023 John A. Breaux +// This code is licensed under MIT license (see LICENSE for details) + //! Exercises the instruction decode logic. use super::*; @@ -9,9 +12,11 @@ fn run_single_op(op: &[u8]) -> CPU { let (mut cpu, mut bus) = ( CPU::default(), bus! { - Program[0x200..0x240] = op, + Screen[0x0..0x1000], }, ); + cpu.screen + .load_region(Program, op).unwrap(); cpu.v = *INDX; cpu.flags.quirks = Quirks::from(false); cpu.tick(&mut bus).unwrap(); // will panic if unimplemented diff --git a/src/lib.rs b/src/lib.rs index 36f359e..e8756b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,8 @@ pub mod error; // Common imports for Chirp pub use cpu::{ bus::{Bus, Get, ReadWrite, Region::*}, - disassembler::{Dis, Disassembler}, flags::Flags, + instruction::disassembler::{Dis, Disassembler}, mode::Mode, quirks::Quirks, CPU, diff --git a/tests/chip8_test_suite.rs b/tests/chip8_test_suite.rs index b023c08..8a9bf19 100644 --- a/tests/chip8_test_suite.rs +++ b/tests/chip8_test_suite.rs @@ -11,7 +11,6 @@ fn setup_environment() -> (CPU, Bus) { cpu.flags = Flags { debug: true, pause: false, - monotonic: Some(10), ..Default::default() }; ( @@ -36,10 +35,10 @@ struct SuiteTest { fn run_screentest(test: SuiteTest, mut cpu: CPU, mut bus: Bus) { // Set the test to run bus.write(0x1ffu16, test.test); - bus.load_region(Program, test.data).unwrap(); + cpu.load_program_bytes(test.data).unwrap(); // The test suite always initiates a keypause on test completion - while !(cpu.flags.keypause || cpu.flags.pause) { - cpu.multistep(&mut bus, 100).unwrap(); + while !(cpu.flags.is_paused()) { + cpu.multistep(&mut bus, 10).unwrap(); } // Compare the screen to the reference screen buffer bus.print_screen().unwrap(); diff --git a/tests/integration.rs b/tests/integration.rs index 2af03ae..2d158fe 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -192,7 +192,7 @@ mod cpu { } mod dis { - use chirp::cpu::disassembler::Insn; + use chirp::cpu::instruction::Insn; use imperative_rs::InstructionSet; #[test]