From acc7629516218765c4a7a366de5999edd783be55 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Mon, 3 Apr 2023 02:01:25 -0500 Subject: [PATCH 01/12] schip: Add preliminary SuperChip support (no test) --- justfile | 11 +- src/bin/chirp-minifb/main.rs | 66 +++++----- src/bin/chirp-minifb/ui.rs | 18 ++- src/bus.rs | 76 ++++++++++- src/cpu.rs | 235 +++++++++++++++++++++++++++++++---- src/cpu/disassembler.rs | 41 ++++++ src/error.rs | 6 + src/lib.rs | 2 +- tests/chip8_test_suite.rs | 2 +- tests/integration.rs | 14 +-- 10 files changed, 384 insertions(+), 87 deletions(-) diff --git a/justfile b/justfile index 1c4562e..496979f 100644 --- a/justfile +++ b/justfile @@ -7,11 +7,14 @@ rat: test: cargo nextest run -chirp: - cargo run --bin chirp-minifb -- chip8-test-suite/bin/chip8-test-suite.ch8 +run rom: + cargo run --bin chirp-minifb -- '{{rom}}' + +debug rom: + cargo run --bin chirp-minifb -- -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 --bin chirp-minifb --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 @@ -23,5 +26,5 @@ cover: cargo llvm-cov --open --doctests tokei: - tokei --exclude tests/chip8-test-suite + tokei --exclude chip8-test-suite --exclude chip8Archive diff --git a/src/bin/chirp-minifb/main.rs b/src/bin/chirp-minifb/main.rs index dca6b2f..c38dc1a 100644 --- a/src/bin/chirp-minifb/main.rs +++ b/src/bin/chirp-minifb/main.rs @@ -36,33 +36,39 @@ struct Arguments { pub step: Option, #[options(help = "Enable performance benchmarking on stderr (requires -S)")] pub perf: bool, - #[options( - short = "z", - help = "Disable setting vF to 0 after a bitwise operation." - )] - pub vfreset: bool, - #[options( - short = "x", - help = "Disable waiting for vblank after issuing a draw call." - )] - pub drawsync: bool, #[options( - short = "c", - help = "Use CHIP-48 style DMA instructions, which don't touch I." + help = "Run in (Chip8, SChip, XOChip) mode.", + //parse(from_str = "parse_mode") )] - pub memory: bool, - #[options( - short = "v", - help = "Use CHIP-48 style bit-shifts, which don't touch vY." - )] - pub shift: bool, - #[options( - short = "b", - help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]." - )] - pub jumping: bool, + pub mode: Option, + // #[options( + // short = "z", + // help = "Disable setting vF to 0 after a bitwise operation." + // )] + // pub vfreset: bool, + // #[options( + // short = "x", + // help = "Disable waiting for vblank after issuing a draw call." + // )] + // pub drawsync: bool, + + // #[options( + // short = "c", + // help = "Use CHIP-48 style DMA instructions, which don't touch I." + // )] + // pub memory: bool, + // #[options( + // short = "v", + // help = "Use CHIP-48 style bit-shifts, which don't touch vY." + // )] + // pub shift: bool, + // #[options( + // short = "b", + // 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.", @@ -105,7 +111,7 @@ impl State { // Load the ROM file into RAM Program [0x0200..0x1000] = &read(&options.file)?, // Create a screen - Screen [0x1000..0x1100], + Screen [0x1000..0x1400], // Create a stack Stack [0x0EA0..0x0F00], }, @@ -117,13 +123,7 @@ impl State { 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, - }, + quirks: options.mode.unwrap_or_default().into(), debug: options.debug, pause: options.pause, monotonic: options.speed, @@ -131,7 +131,7 @@ impl State { }, ), }, - ui: UIBuilder::new(64, 32, &options.file).build()?, + ui: UIBuilder::new(128, 64, &options.file).build()?, ft: Instant::now(), }; state.ch8.bus.write(0x1feu16, options.data); @@ -202,7 +202,7 @@ impl Iterator for State { } } -fn main() -> Result<()> { +pub fn main() -> Result<()> { let options = Arguments::parse_args_default_or_exit(); let state = State::new(options)?; for result in state { diff --git a/src/bin/chirp-minifb/ui.rs b/src/bin/chirp-minifb/ui.rs index 5a72c36..b0b1673 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() @@ -80,9 +80,13 @@ pub struct FrameBufferFormat { impl Default for FrameBufferFormat { fn default() -> Self { + // FrameBufferFormat { + // fg: 0x0011a434, + // bg: 0x001E2431, + // } FrameBufferFormat { - fg: 0x0011a434, - bg: 0x001E2431, + fg: 0xc4c4c4, + bg: 0x000000, } } } @@ -112,6 +116,8 @@ impl FrameBuffer { self.format.fg } else { self.format.bg + // .wrapping_add(0x001104 * (idx / self.width) as u32) + // .wrapping_add(0x141000 * (idx & 3) as u32) } } } @@ -123,7 +129,7 @@ impl FrameBuffer { impl Default for FrameBuffer { fn default() -> Self { - Self::new(64, 32) + Self::new(128, 64) } } diff --git a/src/bus.rs b/src/bus.rs index f330009..93b0140 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -308,14 +308,14 @@ impl Bus { const REGION: Region = Region::Screen; if let Some(screen) = self.get_region(REGION) { for (index, byte) in screen.iter().enumerate() { - if index % 8 == 0 { - print!("|"); + if index % 16 == 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 % 16 == 15 { println!("|"); } } @@ -339,7 +339,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 +402,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..36a1a95 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -22,12 +22,39 @@ use crate::{ use imperative_rs::InstructionSet; use owo_colors::OwoColorize; use rand::random; -use std::time::Instant; +use std::{str::FromStr, time::Instant}; type Reg = usize; type Adr = u16; type Nib = u8; +/// Selects the memory behavior of the interpreter +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Mode { + /// VIP emulation mode + #[default] + Chip8, + /// Chip-48 emulation mode + SChip, + /// XO-Chip emulation mode + XOChip, +} + +impl FromStr for Mode { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "chip8" | "chip-8" => Ok(Mode::Chip8), + "schip" | "superchip" => Ok(Mode::SChip), + "xo-chip" | "xochip" => Ok(Mode::XOChip), + _ => Err(Error::InvalidMode { + mode: s.to_string(), + }), + } + } +} + /// Controls the authenticity behavior of the CPU on a granular level. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Quirks { @@ -51,7 +78,7 @@ impl From for Quirks { shift: true, draw_wait: true, dma_inc: true, - stupid_jumps: false, + stupid_jumps: true, } } else { Quirks { @@ -65,6 +92,22 @@ impl From for Quirks { } } +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) @@ -82,9 +125,13 @@ pub struct ControlFlags { 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 set of emulator [Quirks] to enable + /// 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, @@ -704,6 +751,7 @@ impl CPU { #[rustfmt::skip] fn execute(&mut self, bus: &mut Bus, instruction: Insn) { match instruction { + // Core Chip-8 instructions Insn::cls => self.clear_screen(bus), Insn::ret => self.ret(bus), Insn::jmp { A } => self.jump(A), @@ -713,7 +761,7 @@ impl CPU { 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::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), @@ -726,7 +774,7 @@ impl CPU { 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::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), @@ -738,6 +786,16 @@ impl CPU { 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.scr(bus), + Insn::scl => self.scl(bus), + Insn::halt => self.flags.pause(), + Insn::lores => self.flags.draw_mode = false, + Insn::hires => self.flags.draw_mode = true, + Insn::hfont { x } => self.load_big_sprite(x), + Insn::flgo { x } => self.store_flags(x, bus), + Insn::flgi { x } => self.load_flags(x, bus), } } } @@ -755,9 +813,7 @@ 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); - } + bus.clear_region(Region::Screen); } /// |`00ee`| Returns from subroutine #[inline(always)] @@ -765,6 +821,37 @@ impl CPU { self.sp = self.sp.wrapping_add(2); self.pc = bus.read(self.sp); } + + /// |`00cN`| Scroll the screen down N lines + #[inline(always)] + fn scroll_down(&mut self, n: Nib, bus: &mut Bus) { + // Get a line from the bus + for i in (16 * n as usize..16 * 15).step_by(16).rev() { + let i = i + self.screen as usize; + let line: u128 = bus.read(i); + bus.write(i - (n as usize * 16), line); + bus.write(i, 0u128); + } + } + + /// |`00fb`| Scroll the screen right + #[inline(always)] + fn scr(&mut self, bus: &mut (impl Read + Write)) { + // Get a line from the bus + for i in (0..16 * 64).step_by(16) { + //let line: u128 = bus.read(self.screen + i) >> 4; + bus.write(self.screen + i, bus.read(self.screen + i) >> 4); + } + } + /// |`00fc`| Scroll the screen right + #[inline(always)] + fn scl(&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); + } + } } // |`1aaa`| Sets pc to an absolute address @@ -994,6 +1081,15 @@ impl CPU { } } +/// TODO: Do this more idiomatically, using some iterator chain? +fn doublewide(value: u16) -> u32 { + let mut out: u32 = 0; + for i in 0..16 { + out |= ((value as u32 & 0x1 << i) * 3) << i; + } + out +} + // |`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) @@ -1002,31 +1098,86 @@ impl CPU { /// 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); + // lmaotch + match self.flags.draw_mode { + true => self.draw_hires(x, y, n, bus), + false => self.draw_lores(x, y, n, bus), + } + } + #[inline(always)] + fn draw_lores(&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) = (64, 32); + let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); self.v[0xf] = 0; - for byte in 0..n as u16 { - if y + byte > 32 { - return; + 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, sprite) = (line as u16, *sprite); + // clip + if y + line >= h { + break; + } + // clip + let sprite = (sprite as u16) << (8 - (x % 8)) + & if (x % w) >= (w - 8) { 0xff00 } else { 0xffff }; + // scale + let addr = |x, y| -> u16 { ((y + (2 * line)) * 8 + (x / 8)) * 2 + self.screen }; + let y = y << 1; + let sprite = doublewide(sprite); + for scale in 0..2 { + let screen: u32 = bus.read(addr(x, y + scale)); + bus.write(addr(x, y + scale), screen ^ sprite); + if screen & sprite != 0 { + self.v[0xf] = 1; + } + } } - // 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 + } + } + // Super-Chip extension high-resolution graphics mode + #[inline(always)] + fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + if !self.flags.quirks.draw_wait { + self.flags.draw_wait = true; + } + let (w, h) = (128, 64); + let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); + let w_bytes = w / 8; + self.v[0xf] = 0; + if n == 0 { + 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; + } + } + } + } else { + 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 addr = (y + line as u16) * w_bytes + x / 8 + self.screen; + let sprite = (*sprite as u16) << (8 - (x % 8)); + let screen: u16 = bus.read(addr); + bus.write(addr, screen ^ sprite); + 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); } } } @@ -1164,4 +1315,34 @@ impl CPU { self.i += x as Adr + 1; } } + + /// |`Fx30`| (Super-Chip) 16x16 equivalent of Fx29 + #[inline(always)] + fn load_big_sprite(&mut self, x: Reg) { + self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10)); + } + + /// |`Fx75`| (Super-Chip) Save to "flag registers" + /// I just chuck it in 0x0..0xf. Screw it. + #[inline(always)] + fn store_flags(&mut self, x: Reg, bus: &mut Bus) { + // TODO: Save these, maybe + for (reg, value) in bus + .get_mut(0..=x) + .unwrap_or_default() + .iter_mut() + .enumerate() + { + *value = self.v[reg] + } + } + + /// |`Fx85`| (Super-Chip) Load from "flag registers" + /// I just chuck it in 0x0..0xf. Screw it. + #[inline(always)] + fn load_flags(&mut self, x: Reg, bus: &mut Bus) { + for (reg, value) in bus.get(0..=x).unwrap_or_default().iter().enumerate() { + self.v[reg] = *value; + } + } } diff --git a/src/cpu/disassembler.rs b/src/cpu/disassembler.rs index 8bc585c..9927f9e 100644 --- a/src/cpu/disassembler.rs +++ b/src/cpu/disassembler.rs @@ -9,6 +9,7 @@ use std::fmt::Display; #[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, @@ -111,12 +112,42 @@ 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}"), @@ -151,6 +182,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/error.rs b/src/error.rs index bc35717..fc3e995 100644 --- a/src/error.rs +++ b/src/error.rs @@ -52,6 +52,12 @@ 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), diff --git a/src/lib.rs b/src/lib.rs index b2db64b..6ff4e71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ pub mod error; // Common imports for Chirp pub use bus::{Bus, Read, Region::*, Write}; -pub use cpu::{disassembler::Dis, ControlFlags, CPU}; +pub use cpu::{disassembler::Dis, ControlFlags, Mode, CPU}; pub use error::Result; /// Holds the state of a Chip-8 diff --git a/tests/chip8_test_suite.rs b/tests/chip8_test_suite.rs index f5feba1..0d27b25 100644 --- a/tests/chip8_test_suite.rs +++ b/tests/chip8_test_suite.rs @@ -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..2258eca 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -132,8 +132,8 @@ mod cpu { keypause: false, draw_wait: false, lastkey: None, - quirks: Default::default(), monotonic: None, + ..Default::default() }; let cf2 = cf1.clone(); assert_eq!(cf1, cf2) @@ -151,9 +151,7 @@ mod cpu { pause: false, keypause: false, draw_wait: false, - lastkey: Default::default(), - quirks: Default::default(), - monotonic: Default::default() + ..Default::default() } ) } @@ -165,9 +163,7 @@ mod cpu { pause: true, keypause: true, draw_wait: true, - lastkey: Default::default(), - quirks: Default::default(), - monotonic: Default::default(), + ..Default::default() }; assert_ne!(cf1, cf2); } @@ -179,9 +175,7 @@ mod cpu { 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)); -- 2.25.1 From 9fcae555ce4522a7be4c84e1d7086fecaa18cbc9 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Mon, 3 Apr 2023 05:45:22 -0500 Subject: [PATCH 02/12] chirp-minifb: Add quirk toggles back in --- src/bin/chirp-minifb/main.rs | 61 ++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/src/bin/chirp-minifb/main.rs b/src/bin/chirp-minifb/main.rs index c38dc1a..7038682 100644 --- a/src/bin/chirp-minifb/main.rs +++ b/src/bin/chirp-minifb/main.rs @@ -43,32 +43,32 @@ struct Arguments { )] pub mode: Option, - // #[options( - // short = "z", - // help = "Disable setting vF to 0 after a bitwise operation." - // )] - // pub vfreset: bool, - // #[options( - // short = "x", - // help = "Disable waiting for vblank after issuing a draw call." - // )] - // pub drawsync: bool, + #[options( + short = "z", + help = "Disable setting vF to 0 after a bitwise operation." + )] + pub vfreset: bool, + #[options( + short = "x", + help = "Disable waiting for vblank after issuing a draw call." + )] + pub drawsync: bool, - // #[options( - // short = "c", - // help = "Use CHIP-48 style DMA instructions, which don't touch I." - // )] - // pub memory: bool, - // #[options( - // short = "v", - // help = "Use CHIP-48 style bit-shifts, which don't touch vY." - // )] - // pub shift: bool, - // #[options( - // short = "b", - // help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]." - // )] - // pub jumping: bool, + #[options( + short = "c", + help = "Use CHIP-48 style DMA instructions, which don't touch I." + )] + pub memory: bool, + #[options( + short = "v", + help = "Use CHIP-48 style bit-shifts, which don't touch vY." + )] + pub shift: bool, + #[options( + short = "b", + 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.", @@ -111,7 +111,7 @@ impl State { // Load the ROM file into RAM Program [0x0200..0x1000] = &read(&options.file)?, // Create a screen - Screen [0x1000..0x1400], + Screen [0x1000..0x1100], // Create a stack Stack [0x0EA0..0x0F00], }, @@ -134,6 +134,12 @@ impl State { 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 +160,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, ); } } -- 2.25.1 From 8bb34f25933f4ed2916b1a18d78951c1236de554 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Mon, 3 Apr 2023 05:46:33 -0500 Subject: [PATCH 03/12] schip: Improve architecture & compatibility somewhat --- src/bus.rs | 17 +++ src/cpu.rs | 250 ++++++++++++++++++++++------------------ src/cpu/disassembler.rs | 4 +- 3 files changed, 154 insertions(+), 117 deletions(-) diff --git a/src/bus.rs b/src/bus.rs index 93b0140..2228ddd 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 diff --git a/src/cpu.rs b/src/cpu.rs index 36a1a95..ad4250b 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -788,11 +788,11 @@ impl CPU { Insn::dmai { x } => self.load_dma(x, bus), // Super-Chip extensions Insn::scd { n } => self.scroll_down(n, bus), - Insn::scr => self.scr(bus), - Insn::scl => self.scl(bus), + Insn::scr => self.scroll_right(bus), + Insn::scl => self.scroll_left(bus), Insn::halt => self.flags.pause(), - Insn::lores => self.flags.draw_mode = false, - Insn::hires => self.flags.draw_mode = true, + 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), @@ -821,37 +821,6 @@ impl CPU { self.sp = self.sp.wrapping_add(2); self.pc = bus.read(self.sp); } - - /// |`00cN`| Scroll the screen down N lines - #[inline(always)] - fn scroll_down(&mut self, n: Nib, bus: &mut Bus) { - // Get a line from the bus - for i in (16 * n as usize..16 * 15).step_by(16).rev() { - let i = i + self.screen as usize; - let line: u128 = bus.read(i); - bus.write(i - (n as usize * 16), line); - bus.write(i, 0u128); - } - } - - /// |`00fb`| Scroll the screen right - #[inline(always)] - fn scr(&mut self, bus: &mut (impl Read + Write)) { - // Get a line from the bus - for i in (0..16 * 64).step_by(16) { - //let line: u128 = bus.read(self.screen + i) >> 4; - bus.write(self.screen + i, bus.read(self.screen + i) >> 4); - } - } - /// |`00fc`| Scroll the screen right - #[inline(always)] - fn scl(&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); - } - } } // |`1aaa`| Sets pc to an absolute address @@ -1081,15 +1050,6 @@ impl CPU { } } -/// TODO: Do this more idiomatically, using some iterator chain? -fn doublewide(value: u16) -> u32 { - let mut out: u32 = 0; - for i in 0..16 { - out |= ((value as u32 & 0x1 << i) * 3) << i; - } - out -} - // |`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) @@ -1098,84 +1058,40 @@ impl CPU { /// 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) { - // lmaotch - match self.flags.draw_mode { - true => self.draw_hires(x, y, n, bus), - false => self.draw_lores(x, y, n, bus), - } - } - #[inline(always)] - fn draw_lores(&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) = (64, 32); - let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); + // self.draw_hires handles both hi-res mode and drawing 16x16 sprites + if self.flags.draw_mode || n == 0 { + self.draw_hires(x, y, n, bus); + } else { + self.draw_lores(x, y, n, bus); + } + } + + #[inline(always)] + fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, bus); + } + + #[inline(always)] + fn draw_sprite(&mut self, x: u16, y: u16, n: Nib, w: u16, h: u16, bus: &mut Bus) { + let w_bytes = w / 8; self.v[0xf] = 0; if let Some(sprite) = bus.get(self.i as usize..(self.i + n as u16) as usize) { let sprite = sprite.to_vec(); - for (line, sprite) in sprite.iter().enumerate() { - let (line, sprite) = (line as u16, *sprite); - // clip + for (line, &sprite) in sprite.iter().enumerate() { + let line = line as u16; if y + line >= h { break; } - // clip let sprite = (sprite as u16) << (8 - (x % 8)) & if (x % w) >= (w - 8) { 0xff00 } else { 0xffff }; - // scale - let addr = |x, y| -> u16 { ((y + (2 * line)) * 8 + (x / 8)) * 2 + self.screen }; - let y = y << 1; - let sprite = doublewide(sprite); - for scale in 0..2 { - let screen: u32 = bus.read(addr(x, y + scale)); - bus.write(addr(x, y + scale), screen ^ sprite); - if screen & sprite != 0 { - self.v[0xf] = 1; - } - } - } - } - } - // Super-Chip extension high-resolution graphics mode - #[inline(always)] - fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { - if !self.flags.quirks.draw_wait { - self.flags.draw_wait = true; - } - let (w, h) = (128, 64); - let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); - let w_bytes = w / 8; - self.v[0xf] = 0; - if n == 0 { - 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; - } - } - } - } else { - 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 addr = (y + line as u16) * w_bytes + x / 8 + self.screen; - let sprite = (*sprite as u16) << (8 - (x % 8)); - let screen: u16 = bus.read(addr); - bus.write(addr, screen ^ sprite); - if screen & sprite != 0 { - self.v[0xf] += 1; - } + 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; } } } @@ -1192,16 +1108,14 @@ 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] { + if self.keys[self.v[x] as usize & 0xf] { self.pc += 2; } } /// |`ExaE`| Skip next instruction if key != vX #[inline(always)] fn skip_key_not_equals(&mut self, x: Reg) { - let x = self.v[x] as usize; - if !self.keys[x] { + if !self.keys[self.v[x] as usize & 0xf] { self.pc += 2; } } @@ -1315,8 +1229,99 @@ impl CPU { self.i += x as Adr + 1; } } +} + +//////////////// SUPER CHIP //////////////// + +impl CPU { + /// |`00cN`| Scroll the screen down N lines + #[inline(always)] + fn scroll_down(&mut self, n: Nib, bus: &mut Bus) { + match self.flags.draw_mode { + true => { + // Get a line from the bus + for i in (0..16 * (64 - n as usize)).step_by(16).rev() { + let i = i + self.screen as usize; + let line: u128 = bus.read(i); + bus.write(i - (n as usize * 16), 0u128); + bus.write(i, line); + } + } + false => { + // Get a line from the bus + for i in (0..8 * (32 - n as usize)).step_by(8).rev() { + let i = i + self.screen as usize; + let line: u64 = bus.read(i); + bus.write(i, 0u64); + bus.write(i + (n as usize * 8), line); + } + } + } + } + + /// |`00fb`| Scroll the screen right + #[inline(always)] + fn scroll_right(&mut self, bus: &mut (impl Read + Write)) { + // Get a line from the bus + for i in (0..16 * 64).step_by(16) { + //let line: u128 = bus.read(self.screen + i) >> 4; + bus.write(self.screen + i, bus.read(self.screen + i) >> 4); + } + } + /// |`00fc`| Scroll the screen right + #[inline(always)] + fn scroll_left(&mut self, bus: &mut (impl Read + Write)) { + // Get a line from the bus + for i in (0..16 * 64).step_by(16) { + let line: u128 = (bus.read(self.screen + i) & !(0xf << 124)) << 4; + bus.write(self.screen + i, line); + } + } + + /// |`Dxyn`| + /// Super-Chip extension high-resolution graphics mode + #[inline(always)] + fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + if !self.flags.quirks.draw_wait { + self.flags.draw_wait = true; + } + let (w, h) = match self.flags.draw_mode { + true => (128, 64), + false => (64, 32), + }; + let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); + match n { + 0 => self.draw_schip_sprite(x, y, w, bus), + _ => self.draw_sprite(x, y, n, w, h, bus), + } + } + /// Draws a 16x16 Super Chip sprite + #[inline(always)] + fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, bus: &mut Bus) { + self.v[0xf] = 0; + let w_bytes = w / 8; + if let Some(sprite) = bus.get(self.i as usize..(self.i + 32) as usize) { + let sprite = sprite.to_owned(); + for (line, sprite) in sprite.chunks(2).enumerate() { + let sprite = u16::from_be_bytes( + sprite + .try_into() + .expect("Chunks should only return 2 bytes"), + ); + let addr = (y + line as u16) * w_bytes + x / 8 + self.screen; + let sprite = (sprite as u32) << (16 - (x % 8)); + let screen: u32 = bus.read(addr); + bus.write(addr, screen ^ sprite); + if screen & sprite != 0 { + self.v[0xf] += 1; + } + } + } + } /// |`Fx30`| (Super-Chip) 16x16 equivalent of Fx29 + /// + /// TODO: Actually make and import the 16x font #[inline(always)] fn load_big_sprite(&mut self, x: Reg) { self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10)); @@ -1345,4 +1350,19 @@ impl CPU { self.v[reg] = *value; } } + + /// Initialize lores mode + fn init_lores(&mut self, bus: &mut Bus) { + self.flags.draw_mode = false; + let scraddr = self.screen as usize; + bus.set_region(Region::Screen, scraddr..scraddr + 256); + self.clear_screen(bus); + } + /// Initialize hires mode + fn init_hires(&mut self, bus: &mut Bus) { + self.flags.draw_mode = true; + let scraddr = self.screen as usize; + bus.set_region(Region::Screen, scraddr..scraddr + 1024); + self.clear_screen(bus); + } } diff --git a/src/cpu/disassembler.rs b/src/cpu/disassembler.rs index 9927f9e..76c2e0b 100644 --- a/src/cpu/disassembler.rs +++ b/src/cpu/disassembler.rs @@ -78,7 +78,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 }, @@ -170,7 +170,7 @@ impl Display for Insn { 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}"), -- 2.25.1 From 9bdc418a0058a63d79d21f70e474ca601f5d0221 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Mon, 3 Apr 2023 05:47:13 -0500 Subject: [PATCH 04/12] ui.rs: Implement dynamic resolution changing, revert colors for now --- src/bin/chirp-minifb/ui.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/bin/chirp-minifb/ui.rs b/src/bin/chirp-minifb/ui.rs index b0b1673..f896c93 100644 --- a/src/bin/chirp-minifb/ui.rs +++ b/src/bin/chirp-minifb/ui.rs @@ -80,13 +80,9 @@ pub struct FrameBufferFormat { impl Default for FrameBufferFormat { fn default() -> Self { - // FrameBufferFormat { - // fg: 0x0011a434, - // bg: 0x001E2431, - // } FrameBufferFormat { - fg: 0xc4c4c4, - bg: 0x000000, + fg: 0x0011a434, + bg: 0x001E2431, } } } @@ -110,6 +106,21 @@ 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 => { + self.buffer.resize(64 * 32, 0); + (64, 32) + } + 1024 => { + self.buffer.resize(128 * 64, 0); + (128, 64) + } + _ => { + unimplemented!("Screen must be 64*32 or 128*64"); + } + }; 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 { @@ -129,7 +140,7 @@ impl FrameBuffer { impl Default for FrameBuffer { fn default() -> Self { - Self::new(128, 64) + Self::new(64, 32) } } -- 2.25.1 From 946a6031fb1e552b4fb626a18b85b9f4f92fe8a6 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Mon, 3 Apr 2023 05:47:42 -0500 Subject: [PATCH 05/12] tests: Update test to match new expected behavior --- justfile | 2 +- tests/integration.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 496979f..aa9d205 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,7 @@ debug rom: cargo run --bin chirp-minifb -- -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 -Ps10 -S2100000 -m xochip + cargo run --bin chirp-minifb --release -- chip8Archive/roms/1dcell.ch8 -Ps10 -S21000000 -m xochip flame rom: CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10 diff --git a/tests/integration.rs b/tests/integration.rs index 2258eca..f90e684 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -227,7 +227,7 @@ mod quirks { shift: true, draw_wait: true, dma_inc: true, - stupid_jumps: false, + stupid_jumps: true, } ) } -- 2.25.1 From 78a5a9790ae45698af0bd37284ebeee8505e2bcb Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 14 Apr 2023 16:33:54 -0500 Subject: [PATCH 06/12] Misc: clean up in preparation for merge --- justfile | 2 +- src/bin/chirp-minifb/main.rs | 34 +++++++++++++++++----------------- src/cpu/disassembler.rs | 4 ++-- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/justfile b/justfile index aa9d205..496979f 100644 --- a/justfile +++ b/justfile @@ -14,7 +14,7 @@ debug rom: cargo run --bin chirp-minifb -- -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 -Ps10 -S21000000 -m xochip + cargo run --bin chirp-minifb --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 diff --git a/src/bin/chirp-minifb/main.rs b/src/bin/chirp-minifb/main.rs index 7038682..56f613e 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)] @@ -208,20 +225,3 @@ impl Iterator for State { Some(Ok(())) } } - -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) -} diff --git a/src/cpu/disassembler.rs b/src/cpu/disassembler.rs index 76c2e0b..e8b5092 100644 --- a/src/cpu/disassembler.rs +++ b/src/cpu/disassembler.rs @@ -154,7 +154,7 @@ impl Display for Insn { 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}"), @@ -166,7 +166,7 @@ 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}"), -- 2.25.1 From d12f3fe7108476d12229a337a07dd27672f73d6c Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 14 Apr 2023 16:50:32 -0500 Subject: [PATCH 07/12] cpu.rs: Fiddle with the alignment, to help me feel whole. --- src/cpu.rs | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/cpu.rs b/src/cpu.rs index ad4250b..3c4b174 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -754,13 +754,13 @@ impl CPU { // 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 { B, x } => self.skip_equals_immediate(x, B), - Insn::sneb { B, x } => self.skip_not_equals_immediate(x, B), + 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 { B, x } => self.load_immediate(x, B), - Insn::addb { B, x } => self.add_immediate(x, B), + 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), @@ -771,21 +771,21 @@ impl CPU { 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::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), + 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), @@ -793,9 +793,9 @@ impl CPU { 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), + Insn::hfont { x } => self.load_big_sprite(x), + Insn::flgo { x } => self.store_flags(x, bus), + Insn::flgi { x } => self.load_flags(x, bus), } } } -- 2.25.1 From 4f6f91b69b17b318a5e327192804499cac77d177 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 14 Apr 2023 20:58:28 -0500 Subject: [PATCH 08/12] bus.rs: Debug print screen with drawille, if enabled --- Cargo.toml | 15 +++++++++------ src/bus.rs | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 75e6eb6..05145dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,8 +6,9 @@ authors = ["John Breaux"] license = "MIT" [features] -default = ["unstable"] +default = ["unstable", "drawille"] unstable = [] +drawille = ["dep:drawille"] iced = ["dep:iced"] rhexdump = ["dep:rhexdump"] serde = ["dep:serde"] @@ -26,12 +27,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 } + +gumdrop = "^0.8.1" +imperative-rs = "0.3.1" +minifb = { version = "^0.24.0" } +owo-colors = "^3" +rand = "^0.8.5" thiserror = "^1.0.39" diff --git a/src/bus.rs b/src/bus.rs index 2228ddd..8efb185 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -324,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 % 16 == 0 { + if index % width as usize == 0 { print!("{index:03x}|"); } print!( "{}", format!("{byte:08b}").replace('0', " ").replace('1', "█") ); - if index % 16 == 15 { + if index % width as usize == width as usize - 1 { println!("|"); } } -- 2.25.1 From 973811ee8d8b63fba851de8cf3f6850ef94b13e2 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 14 Apr 2023 21:00:08 -0500 Subject: [PATCH 09/12] csv: print multiple shots at a time --- src/bin/chirp-minifb/ui.rs | 11 +++-------- src/bin/chirp-shot-viewer/main.rs | 7 +++++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/bin/chirp-minifb/ui.rs b/src/bin/chirp-minifb/ui.rs index f896c93..b623721 100644 --- a/src/bin/chirp-minifb/ui.rs +++ b/src/bin/chirp-minifb/ui.rs @@ -109,18 +109,13 @@ impl FrameBuffer { // 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 => { - self.buffer.resize(64 * 32, 0); - (64, 32) - } - 1024 => { - self.buffer.resize(128 * 64, 0); - (128, 64) - } + 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 { 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(()) } -- 2.25.1 From ae32d77757c3feb0a9054146448e9b3d89172103 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 14 Apr 2023 21:00:27 -0500 Subject: [PATCH 10/12] readme.md: Update readme to indicate partial schip support --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 24aa5f1..0414ba4 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 -- 2.25.1 From 674af62465d8129e5e9ba0c9011eee7e375516e4 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 14 Apr 2023 21:25:41 -0500 Subject: [PATCH 11/12] cpu.rs: Break into submodules --- src/bin/chirp-minifb/main.rs | 2 +- src/cpu.rs | 782 +------------------------------- src/cpu/flags.rs | 58 +++ src/cpu/instruction.rs | 844 +++++++++++++++++++++++++---------- src/cpu/mode.rs | 31 ++ src/cpu/quirks.rs | 60 +++ src/cpu/tests.rs | 4 +- src/lib.rs | 2 +- tests/chip8_test_suite.rs | 2 +- tests/integration.rs | 22 +- 10 files changed, 796 insertions(+), 1011 deletions(-) create mode 100644 src/cpu/flags.rs create mode 100644 src/cpu/mode.rs create mode 100644 src/cpu/quirks.rs diff --git a/src/bin/chirp-minifb/main.rs b/src/bin/chirp-minifb/main.rs index 56f613e..ff61e92 100644 --- a/src/bin/chirp-minifb/main.rs +++ b/src/bin/chirp-minifb/main.rs @@ -139,7 +139,7 @@ impl State { 0xefe, Dis::default(), options.breakpoints, - ControlFlags { + Flags { quirks: options.mode.unwrap_or_default().into(), debug: options.debug, pause: options.pause, diff --git a/src/cpu.rs b/src/cpu.rs index 3c4b174..bd0f905 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -13,8 +13,17 @@ pub trait Disassembler { } pub mod disassembler; +pub mod flags; +pub mod instruction; +pub mod mode; +pub mod quirks; -use self::disassembler::{Dis, Insn}; +use self::{ + disassembler::{Dis, Insn}, + flags::Flags, + mode::Mode, + quirks::Quirks, +}; use crate::{ bus::{Bus, Read, Region, Write}, error::{Error, Result}, @@ -22,153 +31,12 @@ use crate::{ use imperative_rs::InstructionSet; use owo_colors::OwoColorize; use rand::random; -use std::{str::FromStr, time::Instant}; +use std::time::Instant; type Reg = usize; type Adr = u16; type Nib = u8; -/// Selects the memory behavior of the interpreter -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Mode { - /// VIP emulation mode - #[default] - Chip8, - /// Chip-48 emulation mode - SChip, - /// XO-Chip emulation mode - XOChip, -} - -impl FromStr for Mode { - type Err = Error; - - fn from_str(s: &str) -> std::result::Result { - match s.to_lowercase().as_str() { - "chip8" | "chip-8" => Ok(Mode::Chip8), - "schip" | "superchip" => Ok(Mode::SChip), - "xo-chip" | "xochip" => Ok(Mode::XOChip), - _ => Err(Error::InvalidMode { - mode: s.to_string(), - }), - } - } -} - -/// Controls the authenticity behavior of the CPU on a granular level. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Quirks { - /// Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0 - pub bin_ops: bool, - /// Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX - pub shift: bool, - /// Draw operations shouldn't pause execution until the next timer tick - pub draw_wait: bool, - /// DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1 - pub dma_inc: bool, - /// Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`. - pub stupid_jumps: bool, -} - -impl From for Quirks { - fn from(value: bool) -> Self { - if value { - Quirks { - bin_ops: true, - shift: true, - draw_wait: true, - dma_inc: true, - stupid_jumps: true, - } - } else { - Quirks { - bin_ops: false, - shift: false, - draw_wait: false, - dma_inc: false, - stupid_jumps: false, - } - } - } -} - -impl From for Quirks { - fn from(value: Mode) -> Self { - match value { - Mode::Chip8 => false.into(), - Mode::SChip => true.into(), - Mode::XOChip => Self { - bin_ops: true, - shift: false, - draw_wait: true, - dma_inc: false, - stupid_jumps: false, - }, - } - } -} - -impl Default for Quirks { - fn default() -> Self { - Self::from(false) - } -} - -/// Represents flags that aid in operation, but aren't inherent to the CPU -#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ControlFlags { - /// Set when debug (live disassembly) mode enabled - pub debug: bool, - /// Set when the emulator is paused by the user and should not update - pub pause: bool, - /// Set when the emulator is waiting for a keypress - pub keypause: bool, - /// Set when the emulator is waiting for a frame to be drawn - pub draw_wait: bool, - /// Set when the emulator is in high-res mode - pub draw_mode: bool, - /// Set to the last key that's been *released* after a keypause - pub lastkey: Option, - /// Represents the current emulator [Mode] - pub mode: Mode, - /// Represents the set of emulator [Quirks] to enable, independent of the [Mode] - pub quirks: Quirks, - /// Represents the number of instructions to run per tick of the internal timer - pub monotonic: Option, -} - -impl ControlFlags { - /// Toggles debug mode - /// - /// # Examples - /// ```rust - /// # use chirp::*; - /// let mut cpu = CPU::default(); - /// assert_eq!(true, cpu.flags.debug); - /// // Toggle debug mode - /// cpu.flags.debug(); - /// assert_eq!(false, cpu.flags.debug); - /// ``` - pub fn debug(&mut self) { - self.debug = !self.debug - } - - /// Toggles pause - /// - /// # Examples - /// ```rust - /// # use chirp::*; - /// let mut cpu = CPU::default(); - /// assert_eq!(false, cpu.flags.pause); - /// // Pause the cpu - /// cpu.flags.pause(); - /// assert_eq!(true, cpu.flags.pause); - /// ``` - pub fn pause(&mut self) { - self.pause = !self.pause - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct Timers { frame: Instant, @@ -190,7 +58,7 @@ impl Default for Timers { pub struct CPU { /// Flags that control how the CPU behaves, but which aren't inherent to the /// implementation. Includes [Quirks], target IPF, etc. - pub flags: ControlFlags, + pub flags: Flags, // memory map info screen: Adr, font: Adr, @@ -235,7 +103,7 @@ impl CPU { sp: Adr, disassembler: Dis, breakpoints: Vec, - flags: ControlFlags, + flags: Flags, ) -> Self { CPU { disassembler, @@ -734,7 +602,7 @@ impl Default for CPU { sound: 0.0, cycle: 0, keys: [false; 16], - flags: ControlFlags { + flags: Flags { debug: true, ..Default::default() }, @@ -744,625 +612,3 @@ impl Default for CPU { } } } - -impl CPU { - /// Executes a single [Insn] - #[inline(always)] - #[rustfmt::skip] - fn execute(&mut self, bus: &mut Bus, instruction: Insn) { - match instruction { - // Core Chip-8 instructions - Insn::cls => self.clear_screen(bus), - Insn::ret => self.ret(bus), - Insn::jmp { A } => self.jump(A), - Insn::call { A } => self.call(A, bus), - Insn::seb { x, B } => self.skip_equals_immediate(x, B), - Insn::sneb { x, B } => self.skip_not_equals_immediate(x, B), - Insn::se { y, x } => self.skip_equals(x, y), - Insn::movb { x, B } => self.load_immediate(x, B), - Insn::addb { x, B } => self.add_immediate(x, B), - Insn::mov { y, x } => self.load(x, y), - Insn::or { y, x } => self.or(x, y), - Insn::and { y, x } => self.and(x, y), - Insn::xor { y, x } => self.xor(x, y), - Insn::add { y, x } => self.add(x, y), - Insn::sub { y, x } => self.sub(x, y), - Insn::shr { y, x } => self.shift_right(x, y), - Insn::bsub { y, x } => self.backwards_sub(x, y), - Insn::shl { y, x } => self.shift_left(x, y), - Insn::sne { y, x } => self.skip_not_equals(x, y), - Insn::movI { A } => self.load_i_immediate(A), - Insn::jmpr { A } => self.jump_indexed(A), - Insn::rand { x, B } => self.rand(x, B), - Insn::draw { y, x, n } => self.draw(x, y, n, bus), - Insn::sek { x } => self.skip_key_equals(x), - Insn::snek { x } => self.skip_key_not_equals(x), - Insn::getdt { x } => self.load_delay_timer(x), - Insn::waitk { x } => self.wait_for_key(x), - Insn::setdt { x } => self.store_delay_timer(x), - Insn::movst { x } => self.store_sound_timer(x), - Insn::addI { x } => self.add_i(x), - Insn::font { x } => self.load_sprite(x), - Insn::bcd { x } => self.bcd_convert(x, bus), - Insn::dmao { x } => self.store_dma(x, bus), - Insn::dmai { x } => self.load_dma(x, bus), - // Super-Chip extensions - Insn::scd { n } => self.scroll_down(n, bus), - Insn::scr => self.scroll_right(bus), - Insn::scl => self.scroll_left(bus), - Insn::halt => self.flags.pause(), - Insn::lores => self.init_lores(bus), - Insn::hires => self.init_hires(bus), - Insn::hfont { x } => self.load_big_sprite(x), - Insn::flgo { x } => self.store_flags(x, bus), - Insn::flgi { x } => self.load_flags(x, bus), - } - } -} - -// Below this point, comments may be duplicated per impl' block, -// since some opcodes handle multiple instructions. - -// |`0aaa`| Issues a "System call" (ML routine) -// -// |opcode| effect | -// |------|------------------------------------| -// |`00e0`| Clear screen memory to all 0 | -// |`00ee`| Return from subroutine | -impl CPU { - /// |`00e0`| Clears the screen memory to 0 - #[inline(always)] - fn clear_screen(&mut self, bus: &mut Bus) { - bus.clear_region(Region::Screen); - } - /// |`00ee`| Returns from subroutine - #[inline(always)] - fn ret(&mut self, bus: &impl Read) { - self.sp = self.sp.wrapping_add(2); - self.pc = bus.read(self.sp); - } -} - -// |`1aaa`| Sets pc to an absolute address -impl CPU { - /// |`1aaa`| Sets the program counter to an absolute address - #[inline(always)] - fn jump(&mut self, a: Adr) { - // jump to self == halt - if a.wrapping_add(2) == self.pc { - self.flags.pause = true; - } - self.pc = a; - } -} - -// |`2aaa`| Pushes pc onto the stack, then jumps to a -impl CPU { - /// |`2aaa`| Pushes pc onto the stack, then jumps to a - #[inline(always)] - fn call(&mut self, a: Adr, bus: &mut impl Write) { - bus.write(self.sp, self.pc); - self.sp = self.sp.wrapping_sub(2); - self.pc = a; - } -} - -// |`3xbb`| Skips next instruction if register X == b -impl CPU { - /// |`3xbb`| Skips the next instruction if register X == b - #[inline(always)] - fn skip_equals_immediate(&mut self, x: Reg, b: u8) { - if self.v[x] == b { - self.pc = self.pc.wrapping_add(2); - } - } -} - -// |`4xbb`| Skips next instruction if register X != b -impl CPU { - /// |`4xbb`| Skips the next instruction if register X != b - #[inline(always)] - fn skip_not_equals_immediate(&mut self, x: Reg, b: u8) { - if self.v[x] != b { - self.pc = self.pc.wrapping_add(2); - } - } -} - -// |`5xyn`| Performs a register-register comparison -// -// |opcode| effect | -// |------|------------------------------------| -// |`5XY0`| Skip next instruction if vX == vY | -impl CPU { - /// |`5xy0`| Skips the next instruction if register X != register Y - #[inline(always)] - fn skip_equals(&mut self, x: Reg, y: Reg) { - if self.v[x] == self.v[y] { - self.pc = self.pc.wrapping_add(2); - } - } -} - -// |`6xbb`| Loads immediate byte b into register vX -impl CPU { - /// |`6xbb`| Loads immediate byte b into register vX - #[inline(always)] - fn load_immediate(&mut self, x: Reg, b: u8) { - self.v[x] = b; - } -} - -// |`7xbb`| Adds immediate byte b to register vX -impl CPU { - /// |`7xbb`| Adds immediate byte b to register vX - #[inline(always)] - fn add_immediate(&mut self, x: Reg, b: u8) { - self.v[x] = self.v[x].wrapping_add(b); - } -} - -// |`8xyn`| Performs ALU operation -// -// |opcode| effect | -// |------|------------------------------------| -// |`8xy0`| Y = X | -// |`8xy1`| X = X | Y | -// |`8xy2`| X = X & Y | -// |`8xy3`| X = X ^ Y | -// |`8xy4`| X = X + Y; Set vF=carry | -// |`8xy5`| X = X - Y; Set vF=carry | -// |`8xy6`| X = X >> 1 | -// |`8xy7`| X = Y - X; Set vF=carry | -// |`8xyE`| X = X << 1 | -impl CPU { - /// |`8xy0`| Loads the value of y into x - #[inline(always)] - fn load(&mut self, x: Reg, y: Reg) { - self.v[x] = self.v[y]; - } - /// |`8xy1`| Performs bitwise or of vX and vY, and stores the result in vX - /// - /// # Quirk - /// The original chip-8 interpreter will clobber vF for any 8-series instruction - #[inline(always)] - fn or(&mut self, x: Reg, y: Reg) { - self.v[x] |= self.v[y]; - if !self.flags.quirks.bin_ops { - self.v[0xf] = 0; - } - } - /// |`8xy2`| Performs bitwise and of vX and vY, and stores the result in vX - /// - /// # Quirk - /// The original chip-8 interpreter will clobber vF for any 8-series instruction - #[inline(always)] - fn and(&mut self, x: Reg, y: Reg) { - self.v[x] &= self.v[y]; - if !self.flags.quirks.bin_ops { - self.v[0xf] = 0; - } - } - /// |`8xy3`| Performs bitwise xor of vX and vY, and stores the result in vX - /// - /// # Quirk - /// The original chip-8 interpreter will clobber vF for any 8-series instruction - #[inline(always)] - fn xor(&mut self, x: Reg, y: Reg) { - self.v[x] ^= self.v[y]; - if !self.flags.quirks.bin_ops { - self.v[0xf] = 0; - } - } - /// |`8xy4`| Performs addition of vX and vY, and stores the result in vX - #[inline(always)] - fn add(&mut self, x: Reg, y: Reg) { - let carry; - (self.v[x], carry) = self.v[x].overflowing_add(self.v[y]); - self.v[0xf] = carry.into(); - } - /// |`8xy5`| Performs subtraction of vX and vY, and stores the result in vX - #[inline(always)] - fn sub(&mut self, x: Reg, y: Reg) { - let carry; - (self.v[x], carry) = self.v[x].overflowing_sub(self.v[y]); - self.v[0xf] = (!carry).into(); - } - /// |`8xy6`| Performs bitwise right shift of vX - /// - /// # Quirk - /// On the original chip-8 interpreter, this shifts vY and stores the result in vX - #[inline(always)] - fn shift_right(&mut self, x: Reg, y: Reg) { - let src: Reg = if self.flags.quirks.shift { x } else { y }; - let shift_out = self.v[src] & 1; - self.v[x] = self.v[src] >> 1; - self.v[0xf] = shift_out; - } - /// |`8xy7`| Performs subtraction of vY and vX, and stores the result in vX - #[inline(always)] - fn backwards_sub(&mut self, x: Reg, y: Reg) { - let carry; - (self.v[x], carry) = self.v[y].overflowing_sub(self.v[x]); - self.v[0xf] = (!carry).into(); - } - /// 8X_E: Performs bitwise left shift of vX - /// - /// # Quirk - /// On the original chip-8 interpreter, this would perform the operation on vY - /// and store the result in vX. This behavior was left out, for now. - #[inline(always)] - fn shift_left(&mut self, x: Reg, y: Reg) { - let src: Reg = if self.flags.quirks.shift { x } else { y }; - let shift_out: u8 = self.v[src] >> 7; - self.v[x] = self.v[src] << 1; - self.v[0xf] = shift_out; - } -} - -// |`9xyn`| Performs a register-register comparison -// -// |opcode| effect | -// |------|------------------------------------| -// |`9XY0`| Skip next instruction if vX != vY | -impl CPU { - /// |`9xy0`| Skip next instruction if X != y - #[inline(always)] - fn skip_not_equals(&mut self, x: Reg, y: Reg) { - if self.v[x] != self.v[y] { - self.pc = self.pc.wrapping_add(2); - } - } -} - -// |`Aaaa`| Load address #a into register I -impl CPU { - /// |`Aadr`| Load address #adr into register I - #[inline(always)] - fn load_i_immediate(&mut self, a: Adr) { - self.i = a; - } -} - -// |`Baaa`| Jump to &adr + v0 -impl CPU { - /// |`Badr`| Jump to &adr + v0 - /// - /// Quirk: - /// On the Super-Chip, this does stupid shit - #[inline(always)] - fn jump_indexed(&mut self, a: Adr) { - let reg = if self.flags.quirks.stupid_jumps { - a as usize >> 8 - } else { - 0 - }; - self.pc = a.wrapping_add(self.v[reg] as Adr); - } -} - -// |`Cxbb`| Stores a random number & the provided byte into vX -impl CPU { - /// |`Cxbb`| Stores a random number & the provided byte into vX - #[inline(always)] - fn rand(&mut self, x: Reg, b: u8) { - self.v[x] = random::() & b; - } -} - -// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY) -impl CPU { - /// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY) - /// - /// # Quirk - /// On the original chip-8 interpreter, this will wait for a VBI - #[inline(always)] - fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { - if !self.flags.quirks.draw_wait { - self.flags.draw_wait = true; - } - // self.draw_hires handles both hi-res mode and drawing 16x16 sprites - if self.flags.draw_mode || n == 0 { - self.draw_hires(x, y, n, bus); - } else { - self.draw_lores(x, y, n, bus); - } - } - - #[inline(always)] - fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { - self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, bus); - } - - #[inline(always)] - fn draw_sprite(&mut self, x: u16, y: u16, n: Nib, w: u16, h: u16, bus: &mut Bus) { - let w_bytes = w / 8; - self.v[0xf] = 0; - if let Some(sprite) = bus.get(self.i as usize..(self.i + n as u16) as usize) { - let sprite = sprite.to_vec(); - for (line, &sprite) in sprite.iter().enumerate() { - let line = line as u16; - if y + line >= h { - break; - } - let sprite = (sprite as u16) << (8 - (x % 8)) - & if (x % w) >= (w - 8) { 0xff00 } else { 0xffff }; - let addr = |x, y| -> u16 { (y + line) * w_bytes + (x / 8) + self.screen }; - let screen: u16 = bus.read(addr(x, y)); - bus.write(addr(x, y), screen ^ sprite); - if screen & sprite != 0 { - self.v[0xf] = 1; - } - } - } - } -} - -// |`Exbb`| Skips instruction on value of keypress -// -// |opcode| effect | -// |------|------------------------------------| -// |`eX9e`| Skip next instruction if key == vX | -// |`eXa1`| Skip next instruction if key != vX | -impl CPU { - /// |`Ex9E`| Skip next instruction if key == vX - #[inline(always)] - fn skip_key_equals(&mut self, x: Reg) { - if self.keys[self.v[x] as usize & 0xf] { - self.pc += 2; - } - } - /// |`ExaE`| Skip next instruction if key != vX - #[inline(always)] - fn skip_key_not_equals(&mut self, x: Reg) { - if !self.keys[self.v[x] as usize & 0xf] { - self.pc += 2; - } - } -} - -// |`Fxbb`| Performs IO -// -// |opcode| effect | -// |------|------------------------------------| -// |`fX07`| Set vX to value in delay timer | -// |`fX0a`| Wait for input, store key in vX | -// |`fX15`| Set sound timer to the value in vX | -// |`fX18`| set delay timer to the value in vX | -// |`fX1e`| Add vX to I | -// |`fX29`| Load sprite for character x into I | -// |`fX33`| BCD convert X into I[0..3] | -// |`fX55`| DMA Stor from I to registers 0..=X | -// |`fX65`| DMA Load from I to registers 0..=X | -impl CPU { - /// |`Fx07`| Get the current DT, and put it in vX - /// ```py - /// vX = DT - /// ``` - #[inline(always)] - fn load_delay_timer(&mut self, x: Reg) { - self.v[x] = self.delay as u8; - } - /// |`Fx0A`| Wait for key, then vX = K - #[inline(always)] - fn wait_for_key(&mut self, x: Reg) { - if let Some(key) = self.flags.lastkey { - self.v[x] = key as u8; - self.flags.lastkey = None; - } else { - self.pc = self.pc.wrapping_sub(2); - self.flags.keypause = true; - } - } - /// |`Fx15`| Load vX into DT - /// ```py - /// DT = vX - /// ``` - #[inline(always)] - fn store_delay_timer(&mut self, x: Reg) { - self.delay = self.v[x] as f64; - } - /// |`Fx18`| Load vX into ST - /// ```py - /// ST = vX; - /// ``` - #[inline(always)] - fn store_sound_timer(&mut self, x: Reg) { - self.sound = self.v[x] as f64; - } - /// |`Fx1e`| Add vX to I, - /// ```py - /// I += vX; - /// ``` - #[inline(always)] - fn add_i(&mut self, x: Reg) { - self.i += self.v[x] as u16; - } - /// |`Fx29`| Load sprite for character x into I - /// ```py - /// I = sprite(X); - /// ``` - #[inline(always)] - fn load_sprite(&mut self, x: Reg) { - self.i = self.font + (5 * (self.v[x] as Adr % 0x10)); - } - /// |`Fx33`| BCD convert X into I`[0..3]` - #[inline(always)] - fn bcd_convert(&mut self, x: Reg, bus: &mut Bus) { - let x = self.v[x]; - bus.write(self.i.wrapping_add(2), x % 10); - bus.write(self.i.wrapping_add(1), x / 10 % 10); - bus.write(self.i, x / 100 % 10); - } - /// |`Fx55`| DMA Stor from I to registers 0..=X - /// - /// # Quirk - /// The original chip-8 interpreter uses I to directly index memory, - /// with the side effect of leaving I as I+X+1 after the transfer is done. - #[inline(always)] - fn store_dma(&mut self, x: Reg, bus: &mut Bus) { - let i = self.i as usize; - for (reg, value) in bus - .get_mut(i..=i + x) - .unwrap_or_default() - .iter_mut() - .enumerate() - { - *value = self.v[reg] - } - if !self.flags.quirks.dma_inc { - self.i += x as Adr + 1; - } - } - /// |`Fx65`| DMA Load from I to registers 0..=X - /// - /// # Quirk - /// The original chip-8 interpreter uses I to directly index memory, - /// with the side effect of leaving I as I+X+1 after the transfer is done. - #[inline(always)] - fn load_dma(&mut self, x: Reg, bus: &mut Bus) { - let i = self.i as usize; - for (reg, value) in bus.get(i..=i + x).unwrap_or_default().iter().enumerate() { - self.v[reg] = *value; - } - if !self.flags.quirks.dma_inc { - self.i += x as Adr + 1; - } - } -} - -//////////////// SUPER CHIP //////////////// - -impl CPU { - /// |`00cN`| Scroll the screen down N lines - #[inline(always)] - fn scroll_down(&mut self, n: Nib, bus: &mut Bus) { - match self.flags.draw_mode { - true => { - // Get a line from the bus - for i in (0..16 * (64 - n as usize)).step_by(16).rev() { - let i = i + self.screen as usize; - let line: u128 = bus.read(i); - bus.write(i - (n as usize * 16), 0u128); - bus.write(i, line); - } - } - false => { - // Get a line from the bus - for i in (0..8 * (32 - n as usize)).step_by(8).rev() { - let i = i + self.screen as usize; - let line: u64 = bus.read(i); - bus.write(i, 0u64); - bus.write(i + (n as usize * 8), line); - } - } - } - } - - /// |`00fb`| Scroll the screen right - #[inline(always)] - fn scroll_right(&mut self, bus: &mut (impl Read + Write)) { - // Get a line from the bus - for i in (0..16 * 64).step_by(16) { - //let line: u128 = bus.read(self.screen + i) >> 4; - bus.write(self.screen + i, bus.read(self.screen + i) >> 4); - } - } - /// |`00fc`| Scroll the screen right - #[inline(always)] - fn scroll_left(&mut self, bus: &mut (impl Read + Write)) { - // Get a line from the bus - for i in (0..16 * 64).step_by(16) { - let line: u128 = (bus.read(self.screen + i) & !(0xf << 124)) << 4; - bus.write(self.screen + i, line); - } - } - - /// |`Dxyn`| - /// Super-Chip extension high-resolution graphics mode - #[inline(always)] - fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { - if !self.flags.quirks.draw_wait { - self.flags.draw_wait = true; - } - let (w, h) = match self.flags.draw_mode { - true => (128, 64), - false => (64, 32), - }; - let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); - match n { - 0 => self.draw_schip_sprite(x, y, w, bus), - _ => self.draw_sprite(x, y, n, w, h, bus), - } - } - /// Draws a 16x16 Super Chip sprite - #[inline(always)] - fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, bus: &mut Bus) { - self.v[0xf] = 0; - let w_bytes = w / 8; - if let Some(sprite) = bus.get(self.i as usize..(self.i + 32) as usize) { - let sprite = sprite.to_owned(); - for (line, sprite) in sprite.chunks(2).enumerate() { - let sprite = u16::from_be_bytes( - sprite - .try_into() - .expect("Chunks should only return 2 bytes"), - ); - let addr = (y + line as u16) * w_bytes + x / 8 + self.screen; - let sprite = (sprite as u32) << (16 - (x % 8)); - let screen: u32 = bus.read(addr); - bus.write(addr, screen ^ sprite); - if screen & sprite != 0 { - self.v[0xf] += 1; - } - } - } - } - - /// |`Fx30`| (Super-Chip) 16x16 equivalent of Fx29 - /// - /// TODO: Actually make and import the 16x font - #[inline(always)] - fn load_big_sprite(&mut self, x: Reg) { - self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10)); - } - - /// |`Fx75`| (Super-Chip) Save to "flag registers" - /// I just chuck it in 0x0..0xf. Screw it. - #[inline(always)] - fn store_flags(&mut self, x: Reg, bus: &mut Bus) { - // TODO: Save these, maybe - for (reg, value) in bus - .get_mut(0..=x) - .unwrap_or_default() - .iter_mut() - .enumerate() - { - *value = self.v[reg] - } - } - - /// |`Fx85`| (Super-Chip) Load from "flag registers" - /// I just chuck it in 0x0..0xf. Screw it. - #[inline(always)] - fn load_flags(&mut self, x: Reg, bus: &mut Bus) { - for (reg, value) in bus.get(0..=x).unwrap_or_default().iter().enumerate() { - self.v[reg] = *value; - } - } - - /// Initialize lores mode - fn init_lores(&mut self, bus: &mut Bus) { - self.flags.draw_mode = false; - let scraddr = self.screen as usize; - bus.set_region(Region::Screen, scraddr..scraddr + 256); - self.clear_screen(bus); - } - /// Initialize hires mode - fn init_hires(&mut self, bus: &mut Bus) { - self.flags.draw_mode = true; - let scraddr = self.screen as usize; - bus.set_region(Region::Screen, scraddr..scraddr + 1024); - self.clear_screen(bus); - } -} diff --git a/src/cpu/flags.rs b/src/cpu/flags.rs new file mode 100644 index 0000000..4acd8f0 --- /dev/null +++ b/src/cpu/flags.rs @@ -0,0 +1,58 @@ +//! Represents flags that aid in implementation but aren't a part of the Chip-8 spec + +use super::{Mode, Quirks}; + +/// Represents flags that aid in operation, but aren't inherent to the CPU +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Flags { + /// Set when debug (live disassembly) mode enabled + pub debug: bool, + /// Set when the emulator is paused by the user and should not update + pub pause: bool, + /// Set when the emulator is waiting for a keypress + pub keypause: bool, + /// Set when the emulator is waiting for a frame to be drawn + pub draw_wait: bool, + /// Set when the emulator is in high-res mode + pub draw_mode: bool, + /// Set to the last key that's been *released* after a keypause + pub lastkey: Option, + /// Represents the current emulator [Mode] + pub mode: Mode, + /// Represents the set of emulator [Quirks] to enable, independent of the [Mode] + pub quirks: Quirks, + /// Represents the number of instructions to run per tick of the internal timer + pub monotonic: Option, +} + +impl Flags { + /// Toggles debug mode + /// + /// # Examples + /// ```rust + /// # use chirp::*; + /// let mut cpu = CPU::default(); + /// assert_eq!(true, cpu.flags.debug); + /// // Toggle debug mode + /// cpu.flags.debug(); + /// assert_eq!(false, cpu.flags.debug); + /// ``` + pub fn debug(&mut self) { + self.debug = !self.debug + } + + /// Toggles pause + /// + /// # Examples + /// ```rust + /// # use chirp::*; + /// let mut cpu = CPU::default(); + /// assert_eq!(false, cpu.flags.pause); + /// // Pause the cpu + /// cpu.flags.pause(); + /// assert_eq!(true, cpu.flags.pause); + /// ``` + pub fn pause(&mut self) { + self.pause = !self.pause + } +} diff --git a/src/cpu/instruction.rs b/src/cpu/instruction.rs index 33b13b0..5db5019 100644 --- a/src/cpu/instruction.rs +++ b/src/cpu/instruction.rs @@ -1,235 +1,625 @@ // (c) 2023 John A. Breaux // This code is licensed under MIT license (see LICENSE.txt for details) -//! Represents a chip-8 instruction as a Rust enum +//! Contains implementations for each instruction defined in [super::disassembler] -use super::{Adr, Nib, Reg}; -type Word = Adr; -type Byte = u8; -type Ins = Nib; +use super::*; -/// Extract the instruction nibble from a word -#[inline] -pub fn i(ins: Word) -> Ins { - (ins >> 12) as Ins & 0xf -} -/// Extracts the X-register nibble from a word -#[inline] -pub fn x(ins: Word) -> Reg { - ins as Reg >> 8 & 0xf -} -/// Extracts the Y-register nibble from a word -#[inline] -pub fn y(ins: u16) -> Reg { - ins as Reg >> 4 & 0xf -} -/// Extracts the nibble-sized immediate from a word -#[inline] -pub fn n(ins: Word) -> Nib { - ins as Nib & 0xf -} -/// Extracts the byte-sized immediate from a word -#[inline] -pub fn b(ins: Word) -> Byte { - ins as Byte -} -/// Extracts the address-sized immediate from a word -#[inline] -pub fn a(ins: Word) -> Adr { - ins & 0x0fff -} -/// Restores the instruction nibble into a word -#[inline] -pub fn ii(i: Ins) -> u16 { - (i as Word & 0xf) << 12 -} -/// Restores the X-register nibble into a word -#[inline] -pub fn xi(x: Reg) -> Word { - (x as Word & 0xf) << 8 -} -/// Restores the Y-register nibble into a word -#[inline] -pub fn yi(y: Reg) -> Word { - (y as Word & 0xf) << 4 -} -/// Restores the nibble-sized immediate into a word -#[inline] -pub fn ni(n: Nib) -> Word { - n as Word & 0xf -} -/// Restores the byte-sized immediate into a word -#[inline] -pub fn bi(b: Byte) -> Word { - b as Word -} -/// Captures the operand and type of a Chip-8 instruction -pub enum Chip8Instruction { - Unimplemented(Word), - Clear, - Return, - Sys(Adr), - Jump(Adr), - Call(Adr), - SkipEqualsByte(Reg, Byte), - SkipNotEqualsByte(Reg, Byte), - SkipEquals(Reg, Reg), - LoadImmediate(Reg, Byte), - AddImmediate(Reg, Byte), - Copy(Reg, Reg), - Or(Reg, Reg), - And(Reg, Reg), - Xor(Reg, Reg), - Add(Reg, Reg), - Sub(Reg, Reg), - ShiftRight(Reg, Reg), - BackwardsSub(Reg, Reg), - ShiftLeft(Reg, Reg), - SkipNotEquals(Reg, Reg), - LoadIndirect(Adr), - JumpIndexed(Adr), - Rand(Reg, Byte), - Draw(Reg, Reg, Nib), - SkipEqualsKey(Reg), - SkipNotEqualsKey(Reg), - StoreDelay(Reg), - WaitForKey(Reg), - LoadDelay(Reg), - LoadSound(Reg), - AddIndirect(Reg), - LoadSprite(Reg), - BcdConvert(Reg), - DmaStore(Reg), - DmaLoad(Reg), -} - -impl TryFrom for Chip8Instruction { - type Error = crate::error::Error; - /// Converts a 16-bit word into a Chip8Instruction, when possible. - fn try_from(opcode: Word) -> Result { - use crate::error::Error::*; - let (i, x, y, n, b, a) = ( - i(opcode), - x(opcode), - y(opcode), - n(opcode), - b(opcode), - a(opcode), - ); - if i > 0xf { - return Err(FunkyMath { - word: opcode, - explanation: "Instruction nibble greater than 0xf".into(), - }); +impl CPU { + /// Executes a single [Insn] + #[inline(always)] + #[rustfmt::skip] + pub(super) fn execute(&mut self, bus: &mut Bus, instruction: Insn) { + match instruction { + // Core Chip-8 instructions + Insn::cls => self.clear_screen(bus), + Insn::ret => self.ret(bus), + Insn::jmp { A } => self.jump(A), + Insn::call { A } => self.call(A, bus), + Insn::seb { x, B } => self.skip_equals_immediate(x, B), + Insn::sneb { x, B } => self.skip_not_equals_immediate(x, B), + Insn::se { y, x } => self.skip_equals(x, y), + Insn::movb { x, B } => self.load_immediate(x, B), + Insn::addb { x, B } => self.add_immediate(x, B), + Insn::mov { y, x } => self.load(x, y), + Insn::or { y, x } => self.or(x, y), + Insn::and { y, x } => self.and(x, y), + Insn::xor { y, x } => self.xor(x, y), + Insn::add { y, x } => self.add(x, y), + Insn::sub { y, x } => self.sub(x, y), + Insn::shr { y, x } => self.shift_right(x, y), + Insn::bsub { y, x } => self.backwards_sub(x, y), + Insn::shl { y, x } => self.shift_left(x, y), + Insn::sne { y, x } => self.skip_not_equals(x, y), + Insn::movI { A } => self.load_i_immediate(A), + Insn::jmpr { A } => self.jump_indexed(A), + Insn::rand { x, B } => self.rand(x, B), + Insn::draw { y, x, n } => self.draw(x, y, n, bus), + Insn::sek { x } => self.skip_key_equals(x), + Insn::snek { x } => self.skip_key_not_equals(x), + Insn::getdt { x } => self.load_delay_timer(x), + Insn::waitk { x } => self.wait_for_key(x), + Insn::setdt { x } => self.store_delay_timer(x), + Insn::movst { x } => self.store_sound_timer(x), + Insn::addI { x } => self.add_i(x), + Insn::font { x } => self.load_sprite(x), + Insn::bcd { x } => self.bcd_convert(x, bus), + Insn::dmao { x } => self.store_dma(x, bus), + Insn::dmai { x } => self.load_dma(x, bus), + // Super-Chip extensions + Insn::scd { n } => self.scroll_down(n, bus), + Insn::scr => self.scroll_right(bus), + Insn::scl => self.scroll_left(bus), + Insn::halt => self.flags.pause(), + Insn::lores => self.init_lores(bus), + Insn::hires => self.init_hires(bus), + Insn::hfont { x } => self.load_big_sprite(x), + Insn::flgo { x } => self.store_flags(x, bus), + Insn::flgi { x } => self.load_flags(x, bus), } - Ok(match i { - // # Issue a system call - // |opcode| effect | - // |------|------------------------------------| - // | 00e0 | Clear screen memory to all 0 | - // | 00ee | Return from subroutine | - 0x0 => match a { - 0xe0 => Self::Clear, - 0xee => Self::Return, - _ => Self::Sys(a), - }, - // | 1aaa | Sets pc to an absolute address - 0x1 => Self::Jump(a), - // | 2aaa | Pushes pc onto the stack, then jumps to a - 0x2 => Self::Call(a), - // | 3xbb | Skips next instruction if register X == b - 0x3 => Self::SkipEqualsByte(x, b), - // | 4xbb | Skips next instruction if register X != b - 0x4 => Self::SkipNotEqualsByte(x, b), - // # Performs a register-register comparison - // |opcode| effect | - // |------|------------------------------------| - // | 9XY0 | Skip next instruction if vX == vY | - 0x5 => match n { - 0x0 => Self::SkipEquals(x, y), - _ => Self::Unimplemented(opcode), - }, - // 6xbb: Loads immediate byte b into register vX - 0x6 => Self::LoadImmediate(x, b), - // 7xbb: Adds immediate byte b to register vX - 0x7 => Self::AddImmediate(x, b), - // # Performs ALU operation - // |opcode| effect | - // |------|------------------------------------| - // | 8xy0 | X = Y | - // | 8xy1 | X = X | Y | - // | 8xy2 | X = X & Y | - // | 8xy3 | X = X ^ Y | - // | 8xy4 | X = X + Y; Set vF=carry | - // | 8xy5 | X = X - Y; Set vF=carry | - // | 8xy6 | X = X >> 1 | - // | 8xy7 | X = Y - X; Set vF=carry | - // | 8xyE | X = X << 1 | - 0x8 => match n { - 0x0 => Self::Copy(x, y), - 0x1 => Self::Or(x, y), - 0x2 => Self::And(x, y), - 0x3 => Self::Xor(x, y), - 0x4 => Self::Add(x, y), - 0x5 => Self::Sub(x, y), - 0x6 => Self::ShiftRight(x, y), - 0x7 => Self::BackwardsSub(x, y), - 0xE => Self::ShiftLeft(x, y), - _ => Self::Unimplemented(opcode), - }, - // # Performs a register-register comparison - // |opcode| effect | - // |------|------------------------------------| - // | 9XY0 | Skip next instruction if vX != vY | - 0x9 => match n { - 0 => Self::SkipNotEquals(x, y), - _ => Self::Unimplemented(opcode), - }, - // Aaaa: Load address #a into register I - 0xa => Self::LoadIndirect(a), - // Baaa: Jump to &adr + v0 - 0xb => Self::JumpIndexed(a), - // Cxbb: Stores a random number & the provided byte into vX - 0xc => Self::Rand(x, b), - // Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY) - 0xd => Self::Draw(x, y, n), - - // # Skips instruction on value of keypress - // |opcode| effect | - // |------|------------------------------------| - // | eX9e | Skip next instruction if key == #X | - // | eXa1 | Skip next instruction if key != #X | - 0xe => match b { - 0x9e => Self::SkipEqualsKey(x), - 0xa1 => Self::SkipNotEqualsKey(x), - _ => Self::Unimplemented(opcode), - }, - - // # Performs IO - // |opcode| effect | - // |------|------------------------------------| - // | fX07 | Set vX to value in delay timer | - // | fX0a | Wait for input, store in vX m | - // | fX15 | Set sound timer to the value in vX | - // | fX18 | set delay timer to the value in vX | - // | fX1e | Add x to I | - // | fX29 | Load sprite for character x into I | - // | fX33 | BCD convert X into I[0..3] | - // | fX55 | DMA Stor from I to registers 0..X | - // | fX65 | DMA Load from I to registers 0..X | - 0xf => match b { - 0x07 => Self::StoreDelay(x), - 0x0A => Self::WaitForKey(x), - 0x15 => Self::LoadDelay(x), - 0x18 => Self::LoadSound(x), - 0x1E => Self::AddIndirect(x), - 0x29 => Self::LoadSprite(x), - 0x33 => Self::BcdConvert(x), - 0x55 => Self::DmaStore(x), - 0x65 => Self::DmaLoad(x), - _ => Self::Unimplemented(opcode), - }, - _ => unreachable!("i somehow mutated from <= 0xf to > 0xf"), - }) + } +} + +// |`0aaa`| Issues a "System call" (ML routine) +// +// |opcode| effect | +// |------|------------------------------------| +// |`00e0`| Clear screen memory to all 0 | +// |`00ee`| Return from subroutine | +impl CPU { + /// |`00e0`| Clears the screen memory to 0 + #[inline(always)] + pub(super) fn clear_screen(&mut self, bus: &mut Bus) { + bus.clear_region(Region::Screen); + } + /// |`00ee`| Returns from subroutine + #[inline(always)] + pub(super) fn ret(&mut self, bus: &impl Read) { + self.sp = self.sp.wrapping_add(2); + self.pc = bus.read(self.sp); + } +} + +// |`1aaa`| Sets pc to an absolute address +impl CPU { + /// |`1aaa`| Sets the program counter to an absolute address + #[inline(always)] + pub(super) fn jump(&mut self, a: Adr) { + // jump to self == halt + if a.wrapping_add(2) == self.pc { + self.flags.pause = true; + } + self.pc = a; + } +} + +// |`2aaa`| Pushes pc onto the stack, then jumps to a +impl CPU { + /// |`2aaa`| Pushes pc onto the stack, then jumps to a + #[inline(always)] + pub(super) fn call(&mut self, a: Adr, bus: &mut impl Write) { + bus.write(self.sp, self.pc); + self.sp = self.sp.wrapping_sub(2); + self.pc = a; + } +} + +// |`3xbb`| Skips next instruction if register X == b +impl CPU { + /// |`3xbb`| Skips the next instruction if register X == b + #[inline(always)] + pub(super) fn skip_equals_immediate(&mut self, x: Reg, b: u8) { + if self.v[x] == b { + self.pc = self.pc.wrapping_add(2); + } + } +} + +// |`4xbb`| Skips next instruction if register X != b +impl CPU { + /// |`4xbb`| Skips the next instruction if register X != b + #[inline(always)] + pub(super) fn skip_not_equals_immediate(&mut self, x: Reg, b: u8) { + if self.v[x] != b { + self.pc = self.pc.wrapping_add(2); + } + } +} + +// |`5xyn`| Performs a register-register comparison +// +// |opcode| effect | +// |------|------------------------------------| +// |`5XY0`| Skip next instruction if vX == vY | +impl CPU { + /// |`5xy0`| Skips the next instruction if register X != register Y + #[inline(always)] + pub(super) fn skip_equals(&mut self, x: Reg, y: Reg) { + if self.v[x] == self.v[y] { + self.pc = self.pc.wrapping_add(2); + } + } +} + +// |`6xbb`| Loads immediate byte b into register vX +impl CPU { + /// |`6xbb`| Loads immediate byte b into register vX + #[inline(always)] + pub(super) fn load_immediate(&mut self, x: Reg, b: u8) { + self.v[x] = b; + } +} + +// |`7xbb`| Adds immediate byte b to register vX +impl CPU { + /// |`7xbb`| Adds immediate byte b to register vX + #[inline(always)] + pub(super) fn add_immediate(&mut self, x: Reg, b: u8) { + self.v[x] = self.v[x].wrapping_add(b); + } +} + +// |`8xyn`| Performs ALU operation +// +// |opcode| effect | +// |------|------------------------------------| +// |`8xy0`| Y = X | +// |`8xy1`| X = X | Y | +// |`8xy2`| X = X & Y | +// |`8xy3`| X = X ^ Y | +// |`8xy4`| X = X + Y; Set vF=carry | +// |`8xy5`| X = X - Y; Set vF=carry | +// |`8xy6`| X = X >> 1 | +// |`8xy7`| X = Y - X; Set vF=carry | +// |`8xyE`| X = X << 1 | +impl CPU { + /// |`8xy0`| Loads the value of y into x + #[inline(always)] + pub(super) fn load(&mut self, x: Reg, y: Reg) { + self.v[x] = self.v[y]; + } + /// |`8xy1`| Performs bitwise or of vX and vY, and stores the result in vX + /// + /// # Quirk + /// The original chip-8 interpreter will clobber vF for any 8-series instruction + #[inline(always)] + pub(super) fn or(&mut self, x: Reg, y: Reg) { + self.v[x] |= self.v[y]; + if !self.flags.quirks.bin_ops { + self.v[0xf] = 0; + } + } + /// |`8xy2`| Performs bitwise and of vX and vY, and stores the result in vX + /// + /// # Quirk + /// The original chip-8 interpreter will clobber vF for any 8-series instruction + #[inline(always)] + pub(super) fn and(&mut self, x: Reg, y: Reg) { + self.v[x] &= self.v[y]; + if !self.flags.quirks.bin_ops { + self.v[0xf] = 0; + } + } + /// |`8xy3`| Performs bitwise xor of vX and vY, and stores the result in vX + /// + /// # Quirk + /// The original chip-8 interpreter will clobber vF for any 8-series instruction + #[inline(always)] + pub(super) fn xor(&mut self, x: Reg, y: Reg) { + self.v[x] ^= self.v[y]; + if !self.flags.quirks.bin_ops { + self.v[0xf] = 0; + } + } + /// |`8xy4`| Performs addition of vX and vY, and stores the result in vX + #[inline(always)] + pub(super) fn add(&mut self, x: Reg, y: Reg) { + let carry; + (self.v[x], carry) = self.v[x].overflowing_add(self.v[y]); + self.v[0xf] = carry.into(); + } + /// |`8xy5`| Performs subtraction of vX and vY, and stores the result in vX + #[inline(always)] + pub(super) fn sub(&mut self, x: Reg, y: Reg) { + let carry; + (self.v[x], carry) = self.v[x].overflowing_sub(self.v[y]); + self.v[0xf] = (!carry).into(); + } + /// |`8xy6`| Performs bitwise right shift of vX + /// + /// # Quirk + /// On the original chip-8 interpreter, this shifts vY and stores the result in vX + #[inline(always)] + pub(super) fn shift_right(&mut self, x: Reg, y: Reg) { + let src: Reg = if self.flags.quirks.shift { x } else { y }; + let shift_out = self.v[src] & 1; + self.v[x] = self.v[src] >> 1; + self.v[0xf] = shift_out; + } + /// |`8xy7`| Performs subtraction of vY and vX, and stores the result in vX + #[inline(always)] + pub(super) fn backwards_sub(&mut self, x: Reg, y: Reg) { + let carry; + (self.v[x], carry) = self.v[y].overflowing_sub(self.v[x]); + self.v[0xf] = (!carry).into(); + } + /// 8X_E: Performs bitwise left shift of vX + /// + /// # Quirk + /// On the original chip-8 interpreter, this would perform the operation on vY + /// and store the result in vX. This behavior was left out, for now. + #[inline(always)] + pub(super) fn shift_left(&mut self, x: Reg, y: Reg) { + let src: Reg = if self.flags.quirks.shift { x } else { y }; + let shift_out: u8 = self.v[src] >> 7; + self.v[x] = self.v[src] << 1; + self.v[0xf] = shift_out; + } +} + +// |`9xyn`| Performs a register-register comparison +// +// |opcode| effect | +// |------|------------------------------------| +// |`9XY0`| Skip next instruction if vX != vY | +impl CPU { + /// |`9xy0`| Skip next instruction if X != y + #[inline(always)] + pub(super) fn skip_not_equals(&mut self, x: Reg, y: Reg) { + if self.v[x] != self.v[y] { + self.pc = self.pc.wrapping_add(2); + } + } +} + +// |`Aaaa`| Load address #a into register I +impl CPU { + /// |`Aadr`| Load address #adr into register I + #[inline(always)] + pub(super) fn load_i_immediate(&mut self, a: Adr) { + self.i = a; + } +} + +// |`Baaa`| Jump to &adr + v0 +impl CPU { + /// |`Badr`| Jump to &adr + v0 + /// + /// Quirk: + /// On the Super-Chip, this does stupid shit + #[inline(always)] + pub(super) fn jump_indexed(&mut self, a: Adr) { + let reg = if self.flags.quirks.stupid_jumps { + a as usize >> 8 + } else { + 0 + }; + self.pc = a.wrapping_add(self.v[reg] as Adr); + } +} + +// |`Cxbb`| Stores a random number & the provided byte into vX +impl CPU { + /// |`Cxbb`| Stores a random number & the provided byte into vX + #[inline(always)] + pub(super) fn rand(&mut self, x: Reg, b: u8) { + self.v[x] = random::() & b; + } +} + +// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY) +impl CPU { + /// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY) + /// + /// # Quirk + /// On the original chip-8 interpreter, this will wait for a VBI + #[inline(always)] + pub(super) fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + if !self.flags.quirks.draw_wait { + self.flags.draw_wait = true; + } + // self.draw_hires handles both hi-res mode and drawing 16x16 sprites + if self.flags.draw_mode || n == 0 { + self.draw_hires(x, y, n, bus); + } else { + self.draw_lores(x, y, n, bus); + } + } + + #[inline(always)] + pub(super) fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, bus); + } + + #[inline(always)] + pub(super) fn draw_sprite(&mut self, x: u16, y: u16, n: Nib, w: u16, h: u16, bus: &mut Bus) { + let w_bytes = w / 8; + self.v[0xf] = 0; + if let Some(sprite) = bus.get(self.i as usize..(self.i + n as u16) as usize) { + let sprite = sprite.to_vec(); + for (line, &sprite) in sprite.iter().enumerate() { + let line = line as u16; + if y + line >= h { + break; + } + let sprite = (sprite as u16) << (8 - (x % 8)) + & if (x % w) >= (w - 8) { 0xff00 } else { 0xffff }; + let addr = |x, y| -> u16 { (y + line) * w_bytes + (x / 8) + self.screen }; + let screen: u16 = bus.read(addr(x, y)); + bus.write(addr(x, y), screen ^ sprite); + if screen & sprite != 0 { + self.v[0xf] = 1; + } + } + } + } +} + +// |`Exbb`| Skips instruction on value of keypress +// +// |opcode| effect | +// |------|------------------------------------| +// |`eX9e`| Skip next instruction if key == vX | +// |`eXa1`| Skip next instruction if key != vX | +impl CPU { + /// |`Ex9E`| Skip next instruction if key == vX + #[inline(always)] + pub(super) fn skip_key_equals(&mut self, x: Reg) { + if self.keys[self.v[x] as usize & 0xf] { + self.pc += 2; + } + } + /// |`ExaE`| Skip next instruction if key != vX + #[inline(always)] + pub(super) fn skip_key_not_equals(&mut self, x: Reg) { + if !self.keys[self.v[x] as usize & 0xf] { + self.pc += 2; + } + } +} + +// |`Fxbb`| Performs IO +// +// |opcode| effect | +// |------|------------------------------------| +// |`fX07`| Set vX to value in delay timer | +// |`fX0a`| Wait for input, store key in vX | +// |`fX15`| Set sound timer to the value in vX | +// |`fX18`| set delay timer to the value in vX | +// |`fX1e`| Add vX to I | +// |`fX29`| Load sprite for character x into I | +// |`fX33`| BCD convert X into I[0..3] | +// |`fX55`| DMA Stor from I to registers 0..=X | +// |`fX65`| DMA Load from I to registers 0..=X | +impl CPU { + /// |`Fx07`| Get the current DT, and put it in vX + /// ```py + /// vX = DT + /// ``` + #[inline(always)] + pub(super) fn load_delay_timer(&mut self, x: Reg) { + self.v[x] = self.delay as u8; + } + /// |`Fx0A`| Wait for key, then vX = K + #[inline(always)] + pub(super) fn wait_for_key(&mut self, x: Reg) { + if let Some(key) = self.flags.lastkey { + self.v[x] = key as u8; + self.flags.lastkey = None; + } else { + self.pc = self.pc.wrapping_sub(2); + self.flags.keypause = true; + } + } + /// |`Fx15`| Load vX into DT + /// ```py + /// DT = vX + /// ``` + #[inline(always)] + pub(super) fn store_delay_timer(&mut self, x: Reg) { + self.delay = self.v[x] as f64; + } + /// |`Fx18`| Load vX into ST + /// ```py + /// ST = vX; + /// ``` + #[inline(always)] + pub(super) fn store_sound_timer(&mut self, x: Reg) { + self.sound = self.v[x] as f64; + } + /// |`Fx1e`| Add vX to I, + /// ```py + /// I += vX; + /// ``` + #[inline(always)] + pub(super) fn add_i(&mut self, x: Reg) { + self.i += self.v[x] as u16; + } + /// |`Fx29`| Load sprite for character x into I + /// ```py + /// I = sprite(X); + /// ``` + #[inline(always)] + pub(super) fn load_sprite(&mut self, x: Reg) { + self.i = self.font + (5 * (self.v[x] as Adr % 0x10)); + } + /// |`Fx33`| BCD convert X into I`[0..3]` + #[inline(always)] + pub(super) fn bcd_convert(&mut self, x: Reg, bus: &mut Bus) { + let x = self.v[x]; + bus.write(self.i.wrapping_add(2), x % 10); + bus.write(self.i.wrapping_add(1), x / 10 % 10); + bus.write(self.i, x / 100 % 10); + } + /// |`Fx55`| DMA Stor from I to registers 0..=X + /// + /// # Quirk + /// The original chip-8 interpreter uses I to directly index memory, + /// with the side effect of leaving I as I+X+1 after the transfer is done. + #[inline(always)] + pub(super) fn store_dma(&mut self, x: Reg, bus: &mut Bus) { + let i = self.i as usize; + for (reg, value) in bus + .get_mut(i..=i + x) + .unwrap_or_default() + .iter_mut() + .enumerate() + { + *value = self.v[reg] + } + if !self.flags.quirks.dma_inc { + self.i += x as Adr + 1; + } + } + /// |`Fx65`| DMA Load from I to registers 0..=X + /// + /// # Quirk + /// The original chip-8 interpreter uses I to directly index memory, + /// with the side effect of leaving I as I+X+1 after the transfer is done. + #[inline(always)] + pub(super) fn load_dma(&mut self, x: Reg, bus: &mut Bus) { + let i = self.i as usize; + for (reg, value) in bus.get(i..=i + x).unwrap_or_default().iter().enumerate() { + self.v[reg] = *value; + } + if !self.flags.quirks.dma_inc { + self.i += x as Adr + 1; + } + } +} + +//////////////// SUPER CHIP //////////////// + +impl CPU { + /// |`00cN`| Scroll the screen down N lines + #[inline(always)] + pub(super) fn scroll_down(&mut self, n: Nib, bus: &mut Bus) { + match self.flags.draw_mode { + true => { + // Get a line from the bus + for i in (0..16 * (64 - n as usize)).step_by(16).rev() { + let i = i + self.screen as usize; + let line: u128 = bus.read(i); + bus.write(i - (n as usize * 16), 0u128); + bus.write(i, line); + } + } + false => { + // Get a line from the bus + for i in (0..8 * (32 - n as usize)).step_by(8).rev() { + let i = i + self.screen as usize; + let line: u64 = bus.read(i); + bus.write(i, 0u64); + bus.write(i + (n as usize * 8), line); + } + } + } + } + + /// |`00fb`| Scroll the screen right + #[inline(always)] + pub(super) fn scroll_right(&mut self, bus: &mut (impl Read + Write)) { + // Get a line from the bus + for i in (0..16 * 64).step_by(16) { + //let line: u128 = bus.read(self.screen + i) >> 4; + bus.write(self.screen + i, bus.read(self.screen + i) >> 4); + } + } + /// |`00fc`| Scroll the screen right + #[inline(always)] + pub(super) fn scroll_left(&mut self, bus: &mut (impl Read + Write)) { + // Get a line from the bus + for i in (0..16 * 64).step_by(16) { + let line: u128 = (bus.read(self.screen + i) & !(0xf << 124)) << 4; + bus.write(self.screen + i, line); + } + } + + /// |`Dxyn`| + /// Super-Chip extension high-resolution graphics mode + #[inline(always)] + pub(super) fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) { + if !self.flags.quirks.draw_wait { + self.flags.draw_wait = true; + } + let (w, h) = match self.flags.draw_mode { + true => (128, 64), + false => (64, 32), + }; + let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h); + match n { + 0 => self.draw_schip_sprite(x, y, w, bus), + _ => self.draw_sprite(x, y, n, w, h, bus), + } + } + /// Draws a 16x16 Super Chip sprite + #[inline(always)] + pub(super) fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, bus: &mut Bus) { + self.v[0xf] = 0; + let w_bytes = w / 8; + if let Some(sprite) = bus.get(self.i as usize..(self.i + 32) as usize) { + let sprite = sprite.to_owned(); + for (line, sprite) in sprite.chunks(2).enumerate() { + let sprite = u16::from_be_bytes( + sprite + .try_into() + .expect("Chunks should only return 2 bytes"), + ); + let addr = (y + line as u16) * w_bytes + x / 8 + self.screen; + let sprite = (sprite as u32) << (16 - (x % 8)); + let screen: u32 = bus.read(addr); + bus.write(addr, screen ^ sprite); + if screen & sprite != 0 { + self.v[0xf] += 1; + } + } + } + } + + /// |`Fx30`| (Super-Chip) 16x16 equivalent of Fx29 + /// + /// TODO: Actually make and import the 16x font + #[inline(always)] + pub(super) fn load_big_sprite(&mut self, x: Reg) { + self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10)); + } + + /// |`Fx75`| (Super-Chip) Save to "flag registers" + /// I just chuck it in 0x0..0xf. Screw it. + #[inline(always)] + pub(super) fn store_flags(&mut self, x: Reg, bus: &mut Bus) { + // TODO: Save these, maybe + for (reg, value) in bus + .get_mut(0..=x) + .unwrap_or_default() + .iter_mut() + .enumerate() + { + *value = self.v[reg] + } + } + + /// |`Fx85`| (Super-Chip) Load from "flag registers" + /// I just chuck it in 0x0..0xf. Screw it. + #[inline(always)] + pub(super) fn load_flags(&mut self, x: Reg, bus: &mut Bus) { + for (reg, value) in bus.get(0..=x).unwrap_or_default().iter().enumerate() { + self.v[reg] = *value; + } + } + + /// Initialize lores mode + pub(super) fn init_lores(&mut self, bus: &mut Bus) { + self.flags.draw_mode = false; + let scraddr = self.screen as usize; + bus.set_region(Region::Screen, scraddr..scraddr + 256); + self.clear_screen(bus); + } + /// Initialize hires mode + pub(super) fn init_hires(&mut self, bus: &mut Bus) { + self.flags.draw_mode = true; + let scraddr = self.screen as usize; + bus.set_region(Region::Screen, scraddr..scraddr + 1024); + self.clear_screen(bus); } } diff --git a/src/cpu/mode.rs b/src/cpu/mode.rs new file mode 100644 index 0000000..5e42e87 --- /dev/null +++ b/src/cpu/mode.rs @@ -0,0 +1,31 @@ +//! Selects the memory behavior of the [super::CPU] + +use crate::error::Error; +use std::str::FromStr; + +/// Selects the memory behavior of the interpreter +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Mode { + /// VIP emulation mode + #[default] + Chip8, + /// Chip-48 emulation mode + SChip, + /// XO-Chip emulation mode + XOChip, +} + +impl FromStr for Mode { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + match s.to_lowercase().as_str() { + "chip8" | "chip-8" => Ok(Mode::Chip8), + "schip" | "superchip" => Ok(Mode::SChip), + "xo-chip" | "xochip" => Ok(Mode::XOChip), + _ => Err(Error::InvalidMode { + mode: s.to_string(), + }), + } + } +} diff --git a/src/cpu/quirks.rs b/src/cpu/quirks.rs new file mode 100644 index 0000000..90a5af2 --- /dev/null +++ b/src/cpu/quirks.rs @@ -0,0 +1,60 @@ +//! Controls the authenticity behavior of the CPU on a granular level. +use super::Mode; +/// Controls the authenticity behavior of the CPU on a granular level. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Quirks { + /// Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0 + pub bin_ops: bool, + /// Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX + pub shift: bool, + /// Draw operations shouldn't pause execution until the next timer tick + pub draw_wait: bool, + /// DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1 + pub dma_inc: bool, + /// Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`. + pub stupid_jumps: bool, +} + +impl From for Quirks { + fn from(value: bool) -> Self { + if value { + Quirks { + bin_ops: true, + shift: true, + draw_wait: true, + dma_inc: true, + stupid_jumps: true, + } + } else { + Quirks { + bin_ops: false, + shift: false, + draw_wait: false, + dma_inc: false, + stupid_jumps: false, + } + } + } +} + +impl From for Quirks { + fn from(value: Mode) -> Self { + match value { + Mode::Chip8 => false.into(), + Mode::SChip => true.into(), + Mode::XOChip => Self { + bin_ops: true, + shift: false, + draw_wait: true, + dma_inc: false, + stupid_jumps: false, + }, + } + } +} + +impl Default for Quirks { + fn default() -> Self { + Self::from(false) + } +} diff --git a/src/cpu/tests.rs b/src/cpu/tests.rs index ee5ceb7..45f2163 100644 --- a/src/cpu/tests.rs +++ b/src/cpu/tests.rs @@ -1,7 +1,7 @@ // (c) 2023 John A. Breaux // This code is licensed under MIT license (see LICENSE.txt for details) -//! Tests for cpu.rs +//! Unit tests for [super::CPU] //! //! These run instructions, and ensure their output is consistent with previous builds //! @@ -23,7 +23,7 @@ mod decode; fn setup_environment() -> (CPU, Bus) { ( CPU { - flags: ControlFlags { + flags: Flags { debug: true, pause: false, monotonic: Some(8), diff --git a/src/lib.rs b/src/lib.rs index 6ff4e71..3ef8738 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ pub mod error; // Common imports for Chirp pub use bus::{Bus, Read, Region::*, Write}; -pub use cpu::{disassembler::Dis, ControlFlags, Mode, CPU}; +pub use cpu::{disassembler::Dis, flags::Flags, mode::Mode, quirks::Quirks, CPU}; pub use error::Result; /// Holds the state of a Chip-8 diff --git a/tests/chip8_test_suite.rs b/tests/chip8_test_suite.rs index 0d27b25..5cd8fe3 100644 --- a/tests/chip8_test_suite.rs +++ b/tests/chip8_test_suite.rs @@ -4,7 +4,7 @@ pub use chirp::*; fn setup_environment() -> (CPU, Bus) { let mut cpu = CPU::default(); - cpu.flags = ControlFlags { + cpu.flags = Flags { debug: true, pause: false, monotonic: Some(8), diff --git a/tests/integration.rs b/tests/integration.rs index f90e684..e8b7248 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -126,7 +126,7 @@ mod cpu { //#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[test] fn clone() { - let cf1 = ControlFlags { + let cf1 = Flags { debug: false, pause: false, keypause: false, @@ -140,13 +140,13 @@ mod cpu { } #[test] fn debug() { - println!("{:?}", ControlFlags::default()); + println!("{:?}", Flags::default()); } #[test] fn default() { assert_eq!( - ControlFlags::default(), - ControlFlags { + Flags::default(), + Flags { debug: false, pause: false, keypause: false, @@ -157,8 +157,8 @@ mod cpu { } #[test] fn eq() { - let cf1 = ControlFlags::default(); - let cf2 = ControlFlags { + let cf1 = Flags::default(); + let cf2 = Flags { debug: true, pause: true, keypause: true, @@ -169,8 +169,8 @@ mod cpu { } #[test] fn ord() { - let cf1 = ControlFlags::default(); - let cf2 = ControlFlags { + let cf1 = Flags::default(); + let cf2 = Flags { debug: true, pause: true, keypause: true, @@ -178,12 +178,12 @@ mod cpu { ..Default::default() }; assert!(cf1 < cf2); - assert_eq!(ControlFlags::default(), cf1.min(cf2)); + assert_eq!(Flags::default(), cf1.min(cf2)); } #[test] fn hash() { let mut hasher = DefaultHasher::new(); - ControlFlags::default().hash(&mut hasher); + Flags::default().hash(&mut hasher); println!("{:?}", hasher); } } @@ -215,7 +215,7 @@ fn error() { mod quirks { use super::*; - use chirp::cpu::Quirks; + use chirp::cpu::quirks::Quirks; #[test] fn from_true() { -- 2.25.1 From 43fa623da39c1e546dda685ec5c254cfea94281b Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 14 Apr 2023 22:20:30 -0500 Subject: [PATCH 12/12] Improve workflow and docs somewhat, make minifb optional --- Cargo.toml | 28 +++++++++++++++++++++++++--- justfile | 8 ++++---- readme.md | 1 + src/bin/chirp-disasm/main.rs | 34 +++++++++++++++++----------------- src/cpu.rs | 12 +++--------- src/cpu/disassembler.rs | 7 ++++++- src/cpu/flags.rs | 9 +++++++-- src/cpu/instruction.rs | 2 +- src/cpu/mode.rs | 3 +++ src/cpu/quirks.rs | 2 +- src/error.rs | 1 + src/lib.rs | 10 ++++++++-- 12 files changed, 77 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 05145dc..038b6f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +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", "drawille"] +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 @@ -31,10 +53,10 @@ 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" imperative-rs = "0.3.1" -minifb = { version = "^0.24.0" } owo-colors = "^3" rand = "^0.8.5" thiserror = "^1.0.39" diff --git a/justfile b/justfile index 496979f..ea37bb5 100644 --- a/justfile +++ b/justfile @@ -8,19 +8,19 @@ test: cargo nextest run run rom: - cargo run --bin chirp-minifb -- '{{rom}}' + cargo run -- '{{rom}}' debug rom: - cargo run --bin chirp-minifb -- -d '{{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 -Ps10 -S2100000 -m xochip + 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 diff --git a/readme.md b/readme.md index 0414ba4..791b83b 100644 --- a/readme.md +++ b/readme.md @@ -66,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/cpu.rs b/src/cpu.rs index bd0f905..23749ae 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -6,12 +6,6 @@ #[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; @@ -19,7 +13,7 @@ pub mod mode; pub mod quirks; use self::{ - disassembler::{Dis, Insn}, + disassembler::{Dis, Disassembler, Insn}, flags::Flags, mode::Mode, quirks::Quirks, @@ -57,7 +51,7 @@ 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. + /// chip-8. Includes [Quirks], target IPF, etc. pub flags: Flags, // memory map info screen: Adr, @@ -478,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; diff --git a/src/cpu/disassembler.rs b/src/cpu/disassembler.rs index e8b5092..94ec4ad 100644 --- a/src/cpu/disassembler.rs +++ b/src/cpu/disassembler.rs @@ -1,10 +1,15 @@ //! 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 diff --git a/src/cpu/flags.rs b/src/cpu/flags.rs index 4acd8f0..b9c05b7 100644 --- a/src/cpu/flags.rs +++ b/src/cpu/flags.rs @@ -1,8 +1,8 @@ -//! Represents flags that aid in implementation but aren't a part of the Chip-8 spec +//! Represents [Flags] that aid in implementation but aren't a part of the Chip-8 spec use super::{Mode, Quirks}; -/// Represents flags that aid in operation, but aren't inherent to the CPU +/// 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 @@ -55,4 +55,9 @@ impl Flags { 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 5db5019..99386fc 100644 --- a/src/cpu/instruction.rs +++ b/src/cpu/instruction.rs @@ -1,7 +1,7 @@ // (c) 2023 John A. Breaux // This code is licensed under MIT license (see LICENSE.txt for details) -//! Contains implementations for each instruction defined in [super::disassembler] +//! Contains implementations for each [Insn] as private member functions of [CPU] use super::*; diff --git a/src/cpu/mode.rs b/src/cpu/mode.rs index 5e42e87..3397d76 100644 --- a/src/cpu/mode.rs +++ b/src/cpu/mode.rs @@ -1,4 +1,7 @@ //! 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; diff --git a/src/cpu/quirks.rs b/src/cpu/quirks.rs index 90a5af2..4f2e283 100644 --- a/src/cpu/quirks.rs +++ b/src/cpu/quirks.rs @@ -1,4 +1,4 @@ -//! Controls the authenticity behavior of the CPU on a granular level. +//! 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)] diff --git a/src/error.rs b/src/error.rs index fc3e995..2212e89 100644 --- a/src/error.rs +++ b/src/error.rs @@ -64,6 +64,7 @@ pub enum 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 3ef8738..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, flags::Flags, mode::Mode, quirks::Quirks, 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)] -- 2.25.1