diff --git a/Cargo.toml b/Cargo.toml index 75e6eb6..038b6f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,40 @@ [package] name = "chirp" -version = "0.1.0" +version = "0.1.1" edition = "2021" +ignore = ["justfile", ".gitmodules", "chip8-test-suite", "chip8Archive"] +default-run = "chirp" authors = ["John Breaux"] license = "MIT" +publish = false + [features] -default = ["unstable"] +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" +required-features = ["minifb"] + +[[bin]] +name = "chirp-disasm" +required-features = ["default"] + +[[bin]] +name = "chirp-iced" +required-features = ["iced"] + +[[bin]] +name = "chirp-shot-viewer" +required-features = ["default", "drawille"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.release] opt-level = 3 @@ -26,12 +49,14 @@ overflow-checks = false [dependencies] -gumdrop = "^0.8.1" +drawille = {version = "0.3.0", optional = true} iced = {version = "0.8.0", optional = true} -imperative-rs = "0.3.1" -minifb = { version = "^0.24.0", features = ["wayland"] } -owo-colors = "^3" -rand = "^0.8.5" 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" +imperative-rs = "0.3.1" +owo-colors = "^3" +rand = "^0.8.5" thiserror = "^1.0.39" diff --git a/justfile b/justfile index 1c4562e..ea37bb5 100644 --- a/justfile +++ b/justfile @@ -7,21 +7,24 @@ rat: test: cargo nextest run -chirp: - cargo run --bin chirp-minifb -- chip8-test-suite/bin/chip8-test-suite.ch8 +run rom: + cargo run -- '{{rom}}' + +debug rom: + cargo run -- -d '{{rom}}' # Run at 2100000 instructions per frame, and output per-frame runtime statistics bench: - cargo run --bin chirp-minifb --release -- chip8Archive/roms/1dcell.ch8 -xP -s10 -S2100000 + cargo run --release -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip flame rom: CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10 flamebench: - CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- chip8Archive/roms/1dcell.ch8 -xPs10 -S2100000 + CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip cover: cargo llvm-cov --open --doctests tokei: - tokei --exclude tests/chip8-test-suite + tokei --exclude chip8-test-suite --exclude chip8Archive diff --git a/readme.md b/readme.md index 24aa5f1..791b83b 100644 --- a/readme.md +++ b/readme.md @@ -6,8 +6,9 @@ I don't know! So I wrote this, to see if i can find out. ## Features: -- 32 * 64 1bpp pixel display, scaled 16x +- 64 * 128 1bpp pixel display, scaled 8x - Full coverage of the original Chip-8 insn set +- Partial coverage of the Super Chip-8 extension set - 64-bit floating point internal sound/delay timers - Pause/Resume - Set and unset breakpoints @@ -65,6 +66,7 @@ Optional arguments: ## TODO: +- [ ] Move the screen, stack, charset, and program memory into the CPU - [ ] Implement sound - [ ] Finish unit tests for "quirks" - [ ] Make pausing/unpausing the emulator less messy diff --git a/src/bin/chirp-disasm/main.rs b/src/bin/chirp-disasm/main.rs index fa0ac8f..8327581 100644 --- a/src/bin/chirp-disasm/main.rs +++ b/src/bin/chirp-disasm/main.rs @@ -1,24 +1,8 @@ -use chirp::{cpu::Disassembler, error::Result, *}; +use chirp::{error::Result, *}; use gumdrop::*; use owo_colors::OwoColorize; use std::{fs::read, path::PathBuf}; -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)] -struct Arguments { - #[options(help = "Show help text")] - help: bool, - #[options(help = "Load a ROM to run on Chirp", free, required)] - pub file: PathBuf, - #[options(help = "Load address (usually 200)", parse(try_from_str = "parse_hex"))] - pub loadaddr: u16, - #[options(help = "Start disassembling at offset...")] - pub offset: usize, -} - -fn parse_hex(value: &str) -> std::result::Result { - u16::from_str_radix(value, 16) -} - fn main() -> Result<()> { let options = Arguments::parse_args_default_or_exit(); let contents = &read(&options.file)?; @@ -40,3 +24,19 @@ fn main() -> Result<()> { } Ok(()) } + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)] +struct Arguments { + #[options(help = "Show help text")] + help: bool, + #[options(help = "Load a ROM to run on Chirp", free, required)] + pub file: PathBuf, + #[options(help = "Load address (usually 200)", parse(try_from_str = "parse_hex"))] + pub loadaddr: u16, + #[options(help = "Start disassembling at offset...")] + pub offset: usize, +} + +fn parse_hex(value: &str) -> std::result::Result { + u16::from_str_radix(value, 16) +} diff --git a/src/bin/chirp-minifb/main.rs b/src/bin/chirp-minifb/main.rs index dca6b2f..ff61e92 100644 --- a/src/bin/chirp-minifb/main.rs +++ b/src/bin/chirp-minifb/main.rs @@ -19,6 +19,23 @@ use std::{ }; use ui::*; +pub fn main() -> Result<()> { + let options = Arguments::parse_args_default_or_exit(); + let state = State::new(options)?; + for result in state { + if let Err(e) = result { + eprintln!("{}", e.bold().red()); + break; + } + } + Ok(()) +} + +/// Parses a hexadecimal string into a u16 +fn parse_hex(value: &str) -> std::result::Result { + u16::from_str_radix(value, 16) +} + #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)] struct Arguments { #[options(help = "Load a ROM to run on Chirp.", required, free)] @@ -36,6 +53,13 @@ struct Arguments { pub step: Option, #[options(help = "Enable performance benchmarking on stderr (requires -S)")] pub perf: bool, + + #[options( + help = "Run in (Chip8, SChip, XOChip) mode.", + //parse(from_str = "parse_mode") + )] + pub mode: Option, + #[options( short = "z", help = "Disable setting vF to 0 after a bitwise operation." @@ -62,7 +86,6 @@ struct Arguments { help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]." )] pub jumping: bool, - #[options( long = "break", help = "Set breakpoints for the emulator to stop at.", @@ -116,14 +139,8 @@ impl State { 0xefe, Dis::default(), options.breakpoints, - ControlFlags { - quirks: chirp::cpu::Quirks { - bin_ops: options.vfreset, - shift: options.shift, - draw_wait: options.drawsync, - dma_inc: options.memory, - stupid_jumps: options.jumping, - }, + Flags { + quirks: options.mode.unwrap_or_default().into(), debug: options.debug, pause: options.pause, monotonic: options.speed, @@ -131,9 +148,15 @@ impl State { }, ), }, - ui: UIBuilder::new(64, 32, &options.file).build()?, + ui: UIBuilder::new(128, 64, &options.file).build()?, ft: Instant::now(), }; + // Flip the state of the quirks + state.ch8.cpu.flags.quirks.bin_ops ^= options.vfreset; + state.ch8.cpu.flags.quirks.dma_inc ^= options.memory; + state.ch8.cpu.flags.quirks.draw_wait ^= options.drawsync; + state.ch8.cpu.flags.quirks.shift ^= options.shift; + state.ch8.cpu.flags.quirks.stupid_jumps ^= options.jumping; state.ch8.bus.write(0x1feu16, options.data); Ok(state) } @@ -154,9 +177,10 @@ impl State { let time = time.elapsed(); let nspt = time.as_secs_f64() / ticks as f64; eprintln!( - "{ticks},\t{time:.05?},\t{:.4}nspt,\t{}ipf", + "{ticks},\t{time:.05?},\t{:.4} nspt,\t{} ipf,\t{} mips", nspt * 1_000_000_000.0, ((1.0 / 60.0f64) / nspt).trunc(), + (1.0 / nspt).trunc() / 1_000_000.0, ); } } @@ -201,20 +225,3 @@ impl Iterator for State { Some(Ok(())) } } - -fn main() -> Result<()> { - let options = Arguments::parse_args_default_or_exit(); - let state = State::new(options)?; - for result in state { - if let Err(e) = result { - eprintln!("{}", e.bold().red()); - break; - } - } - Ok(()) -} - -/// Parses a hexadecimal string into a u16 -fn parse_hex(value: &str) -> std::result::Result { - u16::from_str_radix(value, 16) -} diff --git a/src/bin/chirp-minifb/ui.rs b/src/bin/chirp-minifb/ui.rs index 5a72c36..b623721 100644 --- a/src/bin/chirp-minifb/ui.rs +++ b/src/bin/chirp-minifb/ui.rs @@ -56,14 +56,14 @@ impl UIBuilder { impl Default for UIBuilder { fn default() -> Self { UIBuilder { - width: 64, - height: 32, + width: 128, + height: 64, name: Some("Chip-8 Interpreter"), rom: None, window_options: WindowOptions { title: true, resize: false, - scale: Scale::X16, + scale: Scale::X8, scale_mode: ScaleMode::AspectRatioStretch, none: true, ..Default::default() @@ -106,12 +106,24 @@ impl FrameBuffer { } pub fn render(&mut self, window: &mut Window, bus: &Bus) -> Result<()> { if let Some(screen) = bus.get_region(Region::Screen) { + // Resizing the buffer does not unmap memory. + // After the first use of high-res mode, this is pretty cheap + (self.width, self.height) = match screen.len() { + 256 => (64, 32), + 1024 => (128, 64), + _ => { + unimplemented!("Screen must be 64*32 or 128*64"); + } + }; + self.buffer.resize(self.width * self.height, 0); for (idx, byte) in screen.iter().enumerate() { for bit in 0..8 { self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 { self.format.fg } else { self.format.bg + // .wrapping_add(0x001104 * (idx / self.width) as u32) + // .wrapping_add(0x141000 * (idx & 3) as u32) } } } diff --git a/src/bin/chirp-shot-viewer/main.rs b/src/bin/chirp-shot-viewer/main.rs index 91b14a6..9b8f023 100644 --- a/src/bin/chirp-shot-viewer/main.rs +++ b/src/bin/chirp-shot-viewer/main.rs @@ -2,6 +2,9 @@ use chirp::{error::Result, *}; use std::{env::args, fs::read}; fn main() -> Result<()> { - bus! {Screen [0..0x100] = &read(args().nth(1).unwrap_or("screen_dump.bin".to_string()))?} - .print_screen() + for screen in args().skip(1).inspect(|screen| println!("{screen}")) { + let screen = read(screen)?; + bus! {Screen [0..screen.len()] = &screen}.print_screen()?; + } + Ok(()) } diff --git a/src/bus.rs b/src/bus.rs index f330009..8efb185 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -166,6 +166,23 @@ impl Bus { } self } + /// Updates an existing named range (Region) + /// # Examples + /// ```rust + ///# use chirp::*; + ///# fn main() -> Result<()> { + /// let bus = Bus::new().add_region(Program, 0..1234); + /// assert_eq!(1234, bus.len()); + ///# Ok(()) + ///# } + /// ``` + pub fn set_region(&mut self, name: Region, range: Range) -> &mut Self { + self.with_size(range.end); + if let Some(region) = self.region.get_mut(name as usize) { + *region = Some(range); + } + self + } /// Loads data into a named region /// # Examples /// ```rust @@ -307,15 +324,43 @@ impl Bus { pub fn print_screen(&self) -> Result<()> { const REGION: Region = Region::Screen; if let Some(screen) = self.get_region(REGION) { + let len_log2 = screen.len().ilog2() / 2; + #[allow(unused_variables)] + let (width, height) = (2u32.pow(len_log2 - 1), 2u32.pow(len_log2)); + // draw with the drawille library, if available + #[cfg(feature = "drawille")] + { + use drawille::Canvas; + let mut canvas = Canvas::new(width * 8, height); + let width = width * 8; + screen + .iter() + .enumerate() + .flat_map(|(bytei, byte)| { + (0..8) + .into_iter() + .enumerate() + .filter_map(move |(biti, bit)| { + if (byte << bit) & 0x80 != 0 { + Some(bytei * 8 + biti) + } else { + None + } + }) + }) + .for_each(|index| canvas.set(index as u32 % (width), index as u32 / (width))); + println!("{}", canvas.frame()); + } + #[cfg(not(feature = "drawille"))] for (index, byte) in screen.iter().enumerate() { - if index % 8 == 0 { - print!("|"); + if index % width as usize == 0 { + print!("{index:03x}|"); } print!( "{}", - format!("{byte:08b}").replace('0', " ").replace('1', "██") + format!("{byte:08b}").replace('0', " ").replace('1', "█") ); - if index % 8 == 7 { + if index % width as usize == width as usize - 1 { println!("|"); } } @@ -339,7 +384,43 @@ impl Read for Bus { fn read(&self, addr: impl Into) -> u16 { let addr: usize = addr.into(); if let Some(bytes) = self.memory.get(addr..addr + 2) { - u16::from_be_bytes(bytes.try_into().expect("asked for 2 bytes, got != 2 bytes")) + u16::from_be_bytes(bytes.try_into().expect("Should get 2 bytes")) + } else { + 0xc5c5 + } + } +} + +impl Read for Bus { + /// Read a u16 from address `addr` + fn read(&self, addr: impl Into) -> u32 { + let addr: usize = addr.into(); + if let Some(bytes) = self.memory.get(addr..addr + 4) { + u32::from_be_bytes(bytes.try_into().expect("Should get 4 bytes")) + } else { + 0xc5c5 + } + } +} + +impl Read for Bus { + /// Read a u16 from address `addr` + fn read(&self, addr: impl Into) -> u64 { + let addr: usize = addr.into(); + if let Some(bytes) = self.memory.get(addr..addr + 8) { + u64::from_be_bytes(bytes.try_into().expect("Should get 8 bytes")) + } else { + 0xc5c5 + } + } +} + +impl Read for Bus { + /// Read a u16 from address `addr` + fn read(&self, addr: impl Into) -> u128 { + let addr: usize = addr.into(); + if let Some(bytes) = self.memory.get(addr..addr + 16) { + u128::from_be_bytes(bytes.try_into().expect("Should get 16 bytes")) } else { 0xc5c5 } @@ -366,6 +447,36 @@ impl Write for Bus { } } +impl Write for Bus { + /// Write a u16 to address `addr` + fn write(&mut self, addr: impl Into, data: u32) { + let addr: usize = addr.into(); + if let Some(slice) = self.get_mut(addr..addr + 4) { + data.to_be_bytes().as_mut().swap_with_slice(slice); + } + } +} + +impl Write for Bus { + /// Write a u16 to address `addr` + fn write(&mut self, addr: impl Into, data: u64) { + let addr: usize = addr.into(); + if let Some(slice) = self.get_mut(addr..addr + 8) { + data.to_be_bytes().as_mut().swap_with_slice(slice); + } + } +} + +impl Write for Bus { + /// Write a u16 to address `addr` + fn write(&mut self, addr: impl Into, data: u128) { + let addr: usize = addr.into(); + if let Some(slice) = self.get_mut(addr..addr + 16) { + data.to_be_bytes().as_mut().swap_with_slice(slice); + } + } +} + #[cfg(target_feature = "rhexdump")] impl Display for Bus { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/src/cpu.rs b/src/cpu.rs index 6af16ce..23749ae 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -6,15 +6,18 @@ #[cfg(test)] mod tests; -/// Disassembles Chip-8 instructions -pub trait Disassembler { - /// Disassemble a single instruction - fn once(&self, insn: u16) -> String; -} - pub mod disassembler; +pub mod flags; +pub mod instruction; +pub mod mode; +pub mod quirks; -use self::disassembler::{Dis, Insn}; +use self::{ + disassembler::{Dis, Disassembler, Insn}, + flags::Flags, + mode::Mode, + quirks::Quirks, +}; use crate::{ bus::{Bus, Read, Region, Write}, error::{Error, Result}, @@ -28,100 +31,6 @@ type Reg = usize; type Adr = u16; type Nib = u8; -/// 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: false, - } - } else { - Quirks { - bin_ops: false, - shift: false, - draw_wait: false, - dma_inc: false, - stupid_jumps: false, - } - } - } -} - -impl Default for Quirks { - fn default() -> Self { - Self::from(false) - } -} - -/// 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 to the last key that's been *released* after a keypause - pub lastkey: Option, - /// Represents the set of emulator [Quirks] to enable - 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, @@ -142,8 +51,8 @@ impl Default for Timers { #[derive(Clone, Debug, PartialEq)] 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, + /// chip-8. Includes [Quirks], target IPF, etc. + pub flags: Flags, // memory map info screen: Adr, font: Adr, @@ -188,7 +97,7 @@ impl CPU { sp: Adr, disassembler: Dis, breakpoints: Vec, - flags: ControlFlags, + flags: Flags, ) -> Self { CPU { disassembler, @@ -563,7 +472,7 @@ impl CPU { /// ``` pub fn tick(&mut self, bus: &mut Bus) -> Result<&mut Self> { // Do nothing if paused - if self.flags.pause || self.flags.draw_wait || self.flags.keypause { + if self.flags.is_paused() { // always tick in test mode if self.flags.monotonic.is_some() { self.cycle += 1; @@ -687,7 +596,7 @@ impl Default for CPU { sound: 0.0, cycle: 0, keys: [false; 16], - flags: ControlFlags { + flags: Flags { debug: true, ..Default::default() }, @@ -697,471 +606,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 { - 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 { B, x } => self.skip_equals_immediate(x, B), - Insn::sneb { B, x } => self.skip_not_equals_immediate(x, B), - Insn::se { y, x } => self.skip_equals(x, y), - Insn::movb { B, x } => self.load_immediate(x, B), - Insn::addb { B, x } => self.add_immediate(x, B), - Insn::mov { x, y } => 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 { B, x } => self.rand(x, B), - Insn::draw { x, y, 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), - } - } -} - -// 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) { - if let Some(screen) = bus.get_region_mut(Region::Screen) { - screen.fill(0); - } - } - /// |`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) { - let (x, y) = (self.v[x] as u16 % 64, self.v[y] as u16 % 32); - if !self.flags.quirks.draw_wait { - self.flags.draw_wait = true; - } - self.v[0xf] = 0; - for byte in 0..n as u16 { - if y + byte > 32 { - return; - } - // Calculate the lower bound address based on the X,Y position on the screen - let addr = (y + byte) * 8 + (x & 0x3f) / 8 + self.screen; - // Read a byte of sprite data into a u16, and shift it x % 8 bits - let sprite: u8 = bus.read(self.i + byte); - let sprite = - (sprite as u16) << (8 - (x & 7)) & if x % 64 > 56 { 0xff00 } else { 0xffff }; - // Read a u16 from the bus containing the two bytes which might need to be updated - let mut screen: u16 = bus.read(addr); - // Save the bits-toggled-off flag if necessary - if screen & sprite != 0 { - self.v[0xF] = 1 - } - // Update the screen word by XORing the sprite byte - screen ^= sprite; - // Save the result to the screen - bus.write(addr, screen); - } - } -} - -// |`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) { - let x = self.v[x] as usize; - if self.keys[x] { - self.pc += 2; - } - } - /// |`ExaE`| Skip next instruction if key != vX - #[inline(always)] - fn skip_key_not_equals(&mut self, x: Reg) { - let x = self.v[x] as usize; - if !self.keys[x] { - 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; - } - } -} diff --git a/src/cpu/disassembler.rs b/src/cpu/disassembler.rs index 8bc585c..94ec4ad 100644 --- a/src/cpu/disassembler.rs +++ b/src/cpu/disassembler.rs @@ -1,14 +1,20 @@ //! A disassembler for Chip-8 opcodes #![allow(clippy::bad_bit_mask)] -use super::Disassembler; 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, @@ -77,7 +83,7 @@ pub enum Insn { rand { B: u8, x: usize }, /// | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY) #[opcode = "0xdxyn"] - draw { x: usize, y: usize, n: u8 }, + draw { y: usize, x: usize, n: u8 }, /// | eX9e | Skip next instruction if key == vX #[opcode = "0xex9e"] sek { x: usize }, @@ -111,19 +117,49 @@ pub enum Insn { // | 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{x:X}, v{y: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}"), @@ -135,11 +171,11 @@ impl Display for Insn { 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{x:X}, v{y: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 { x, y, n } => write!(f, "draw #{n:x}, v{x:X}, v{y: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}"), @@ -151,6 +187,16 @@ impl Display for Insn { 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}"), } } } diff --git a/src/cpu/flags.rs b/src/cpu/flags.rs new file mode 100644 index 0000000..b9c05b7 --- /dev/null +++ b/src/cpu/flags.rs @@ -0,0 +1,63 @@ +//! 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 implementation but aren't a part of the Chip-8 spec +#[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 + } + + /// Gets whether the CPU is paused for any reason + pub fn is_paused(&self) -> bool { + self.pause || self.draw_wait || self.keypause + } +} diff --git a/src/cpu/instruction.rs b/src/cpu/instruction.rs index 33b13b0..99386fc 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 [Insn] as private member functions of [CPU] -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..3397d76 --- /dev/null +++ b/src/cpu/mode.rs @@ -0,0 +1,34 @@ +//! Selects the memory behavior of the [super::CPU] +//! +//! Since [super::Quirks] implements [From], +//! this can be used to select the appropriate quirk-set + +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..4f2e283 --- /dev/null +++ b/src/cpu/quirks.rs @@ -0,0 +1,60 @@ +//! Controls the [Quirks] 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/error.rs b/src/error.rs index bc35717..2212e89 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,12 +52,19 @@ pub enum Error { /// The offending register reg: usize, }, + /// Tried to convert string into mode, but it did not match. + #[error("Invalid mode: {mode}")] + InvalidMode { + /// The string which failed to become a mode + mode: String, + }, /// Error originated in [std::io] #[error(transparent)] IoError(#[from] std::io::Error), /// Error originated in [std::array::TryFromSliceError] #[error(transparent)] TryFromSliceError(#[from] std::array::TryFromSliceError), + #[cfg(feature = "minifb")] /// Error originated in [minifb] #[error(transparent)] MinifbError(#[from] minifb::Error), diff --git a/src/lib.rs b/src/lib.rs index b2db64b..ec24a46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,14 @@ pub mod error; // Common imports for Chirp pub use bus::{Bus, Read, Region::*, Write}; -pub use cpu::{disassembler::Dis, ControlFlags, CPU}; -pub use error::Result; +pub use cpu::{ + disassembler::{Dis, Disassembler}, + flags::Flags, + mode::Mode, + quirks::Quirks, + CPU, +}; +pub use error::{Error, Result}; /// Holds the state of a Chip-8 #[derive(Clone, Debug, Default, PartialEq)] diff --git a/tests/chip8_test_suite.rs b/tests/chip8_test_suite.rs index f5feba1..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), @@ -17,7 +17,7 @@ fn setup_environment() -> (CPU, Bus) { Charset [0x0050..0x00A0] = include_bytes!("../src/mem/charset.bin"), // Load the ROM file into RAM Program [0x0200..0x1000] = include_bytes!("../chip8-test-suite/bin/chip8-test-suite.ch8"), - // Create a screen, and fill it with garbage data + // Create a screen, and fill it with Screen [0x0F00..0x1000] = include_bytes!("chip8_test_suite.rs"), }, ) diff --git a/tests/integration.rs b/tests/integration.rs index 14003aa..e8b7248 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -126,70 +126,64 @@ 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, draw_wait: false, lastkey: None, - quirks: Default::default(), monotonic: None, + ..Default::default() }; let cf2 = cf1.clone(); assert_eq!(cf1, cf2) } #[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, draw_wait: false, - lastkey: Default::default(), - quirks: Default::default(), - monotonic: Default::default() + ..Default::default() } ) } #[test] fn eq() { - let cf1 = ControlFlags::default(); - let cf2 = ControlFlags { + let cf1 = Flags::default(); + let cf2 = Flags { debug: true, pause: true, keypause: true, draw_wait: true, - lastkey: Default::default(), - quirks: Default::default(), - monotonic: Default::default(), + ..Default::default() }; assert_ne!(cf1, cf2); } #[test] fn ord() { - let cf1 = ControlFlags::default(); - let cf2 = ControlFlags { + let cf1 = Flags::default(); + let cf2 = Flags { debug: true, pause: true, keypause: true, draw_wait: true, - lastkey: Default::default(), - quirks: Default::default(), - monotonic: Default::default(), + ..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); } } @@ -221,7 +215,7 @@ fn error() { mod quirks { use super::*; - use chirp::cpu::Quirks; + use chirp::cpu::quirks::Quirks; #[test] fn from_true() { @@ -233,7 +227,7 @@ mod quirks { shift: true, draw_wait: true, dma_inc: true, - stupid_jumps: false, + stupid_jumps: true, } ) }