commit a721a00232c37cfabd5e43c9a2dff5560df8b29f Author: John Breaux Date: Wed Mar 8 06:07:33 2023 -0600 Initial commit: Created outline of emulator: The emulator has a Bus, which attaches a CPU to some Memory (Mapped Devices) The design isn't particularly efficient, but the interpreter only needs to run at ~500Hz or so. It's Rust. It can do that. Instructions yet to be implemented: Cxbb: "Store a random number, masked by bitmask bb, into vX" Dxyn: "Draw an 8 by n sprite to the screen at coordinates (x, y)" Fx0A: "Wait for a key, then set vX to the value of the pressed key" Fx33: "BCD convert X, storing the results in &I[0..3]" Thoughts going forward: - It's probably a good idea to parse instructions out into an enum. I had this in an earlier design, but it didn't really look that good. However, I haven't read many other emulators before, so I don't know the style people generally go for. - I haven't used a native graphics library before, and my cg class was done entirely in a web browser. That kinda sucks, honestly. Sure the skill might transfer well, but, >JS diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02135de --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +# Chip-8 test ROMs +/chip-8 + +# VS Code files +/.vscode diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..96dc036 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,93 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rumpulator" +version = "0.1.0" +dependencies = [ + "owo-colors", + "serde", + "thiserror", +] + +[[package]] +name = "serde" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a382c72b4ba118526e187430bb4963cd6d55051ebf13d9b25574d379cc98d20" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef476a5790f0f6decbc66726b6e5d63680ed518283e64c7df415989d880954f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e410414 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rumpulator" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +owo-colors = "^3" +serde = { version = "^1.0", features = ["derive"] } +thiserror = "1.0.39" diff --git a/src/bus.rs b/src/bus.rs new file mode 100644 index 0000000..83c02d0 --- /dev/null +++ b/src/bus.rs @@ -0,0 +1,157 @@ +//! The Bus connects the CPU to Memory +mod bus_device; + +use crate::dump::{BinDumpable, Dumpable}; +use bus_device::BusDevice; +use std::{ + fmt::{Debug, Display, Formatter, Result}, + ops::Range, +}; + +/// Creates a new bus, instantiating BusConnectable devices +/// # Examples +/// ```rust +/// # use rumpulator::prelude::*; +/// let mut bus = bus! { +/// "RAM" [0x0000..0x8000] Mem::new(0x8000), +/// "ROM" [0x8000..0xFFFF] Mem::new(0x8000).w(false), +/// }; +/// ``` +#[macro_export] +macro_rules! bus { + ($($name:literal $(:)? [$range:expr] $(=)? $d:expr) ,* $(,)?) => { + $crate::bus::Bus::new() + $( + .connect($name, $range, Box::new($d)) + )* + }; +} + +/// BusConnectable objects can be connected to a bus with `Bus::connect()` +/// +/// The bus performs address translation, so your object will receive +/// reads and writes relative to offset 0 +pub trait BusConnectible: Debug + Display { + fn read_at(&self, addr: u16) -> Option; + fn write_to(&mut self, addr: u16, data: u8); +} + +// Traits Read and Write are here purely to make implementing other things more bearable +/// Do whatever `Read` means to you +pub trait Read { + /// Read a T from address `addr` + fn read(&self, addr: u16) -> T; +} + +/// Write "some data" to the Bus +pub trait Write { + /// Write a T to address `addr` + fn write(&mut self, addr: u16, data: T); +} + +/// The Bus connects bus readers with bus writers. +/// The design assumes single-threaded operation. +#[derive(Debug, Default)] +pub struct Bus { + devices: Vec, +} + +impl Bus { + /// Construct a new bus + pub fn new() -> Self { + Bus::default() + } + /// Connect a BusConnectible object to the bus + pub fn connect( + mut self, + name: &str, + range: Range, + device: Box, + ) -> Self { + self.devices.push(BusDevice::new(name, range, device)); + self + } + pub fn get_region_by_name(&self, name: &str) -> Option> { + for item in &self.devices { + if item.name == name { + return Some(item.range.clone()); + } + } + None + } +} + +/// lmao +impl BusConnectible for Bus { + fn read_at(&self, addr: u16) -> Option { + Some(self.read(addr)) + } + fn write_to(&mut self, addr: u16, data: u8) { + self.write(addr, data) + } +} + +impl Read for Bus { + fn read(&self, addr: u16) -> u8 { + let mut result: u8 = 0; + for item in &self.devices { + result |= item.read_at(addr).unwrap_or(0) + } + result + } +} + +impl Read for Bus { + fn read(&self, addr: u16) -> u16 { + let mut result = 0; + for item in &self.devices { + result |= (item.read_at(addr).unwrap_or(0) as u16) << 8; + result |= item.read_at(addr.wrapping_add(1)).unwrap_or(0) as u16; + } + result + } +} + +impl Write for Bus { + fn write(&mut self, addr: u16, data: u8) { + for item in &mut self.devices { + item.write_to(addr, data) + } + } +} + +impl Write for Bus { + fn write(&mut self, addr: u16, data: u16) { + for item in &mut self.devices { + item.write_to(addr, (data >> 8) as u8); + item.write_to(addr.wrapping_add(1), data as u8); + } + } +} + +impl Display for Bus { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + for device in &self.devices { + write!(f, "{device}")?; + } + write!(f, "") + } +} + +impl Dumpable for Bus { + fn dump(&self, range: Range) { + for index in range { + let byte: u8 = self.read(index as u16); + crate::dump::as_hexdump(index, byte); + } + } +} + +impl BinDumpable for Bus { + fn bin_dump(&self, range: Range) { + for index in range { + let byte: u8 = self.read(index as u16); + crate::dump::as_bindump(index, byte) + } + } +} diff --git a/src/bus/bus_device.rs b/src/bus/bus_device.rs new file mode 100644 index 0000000..689df3e --- /dev/null +++ b/src/bus/bus_device.rs @@ -0,0 +1,51 @@ +//! Connects a BusConnectible to the Bus + +use super::BusConnectible; +use std::{ + fmt::{Display, Formatter, Result}, + ops::Range, +}; + +/// BusDevice performs address translation for BusConnectibles. +/// It is an implementation detail of Bus.connect() +#[derive(Debug)] +pub struct BusDevice { + pub name: String, + pub range: Range, + device: Box, +} + +impl BusDevice { + pub fn new(name: &str, range: Range, device: Box) -> Self { + BusDevice { + name: name.to_string(), + range, + device, + } + } + fn translate_address(&self, addr: u16) -> Option { + let addr = addr.wrapping_sub(self.range.start); + if addr < self.range.end { + Some(addr) + } else { + None + } + } +} + +impl BusConnectible for BusDevice { + fn read_at(&self, addr: u16) -> Option { + self.device.read_at(self.translate_address(addr)?) + } + fn write_to(&mut self, addr: u16, data: u8) { + if let Some(addr) = self.translate_address(addr) { + self.device.write_to(addr, data); + } + } +} + +impl Display for BusDevice { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + writeln!(f, "{} [{:04x?}]:\n{}", self.name, self.range, self.device) + } +} diff --git a/src/cpu.rs b/src/cpu.rs new file mode 100644 index 0000000..35d2cb1 --- /dev/null +++ b/src/cpu.rs @@ -0,0 +1,525 @@ +//! The CPU decodes and runs instructions + +pub mod disassemble; + +use self::disassemble::Disassemble; +use crate::bus::{Bus, Read, Write}; +use owo_colors::OwoColorize; + +type Reg = usize; +type Adr = u16; +type Nib = u8; + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct CPUBuilder { + screen: Option, + font: Option, + pc: Option, + sp: Option, +} + +impl CPUBuilder { + pub fn new() -> Self { + CPUBuilder { + screen: None, + font: None, + pc: None, + sp: None, + } + } + pub fn build(self) -> CPU { + CPU { + screen: self.screen.unwrap_or(0xF00), + font: self.font.unwrap_or(0x050), + pc: self.pc.unwrap_or(0x200), + sp: self.sp.unwrap_or(0xefe), + i: 0, + v: [0; 16], + delay: 0, + sound: 0, + cycle: 0, + keys: 0, + disassembler: Disassemble::default(), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct CPU { + // memory map info + screen: Adr, + font: Adr, + // registers + pc: Adr, + sp: Adr, + i: Adr, + v: [u8; 16], + delay: u8, + sound: u8, + // I/O + keys: usize, + // Execution data + cycle: usize, + disassembler: Disassemble, +} + +// public interface +impl CPU { + /// Press keys (where `keys` is a bitmap of the keys [F-0]) + pub fn press(mut self, keys: u16) -> Self { + self.keys = keys as usize; + self + } + /// Set a general purpose register in the CPU + /// # Examples + /// ```rust + /// # use rumpulator::prelude::*; + /// // Create a new CPU, and set v4 to 0x41 + /// let cpu = CPU::default() + /// .set_gpr(0x4, 0x41); + /// // Dump the CPU registers + /// cpu.dump(); + /// ``` + pub fn set_gpr(mut self, gpr: Reg, value: u8) -> Self { + if let Some(gpr) = self.v.get_mut(gpr) { + *gpr = value; + } + self + } + + /// Constructs a new CPU with sane defaults + /// + /// | value | default | description + /// |--------|---------|------------ + /// | screen | 0x0f00 | Location of screen memory. + /// | font | 0x0050 | Location of font memory. + /// | pc | 0x0200 | Start location. Generally 0x200 or 0x600. + /// | sp | 0x0efe | Initial top of stack. + /// # Examples + /// ```rust + /// # use rumpulator::prelude::*; + /// let mut cpu = CPU::new(0xf00, 0x50, 0x200, 0xefe, Disassemble::default()); + /// ``` + pub fn new(screen: Adr, font: Adr, pc: Adr, sp: Adr, disassembler: Disassemble) -> Self { + CPU { + disassembler, + screen, + font, + pc, + sp, + i: 0, + v: [0; 16], + delay: 0, + sound: 0, + cycle: 0, + keys: 0, + } + } + + pub fn tick(&mut self, bus: &mut Bus) { + std::print!("{:3} {:03x}: ", self.cycle.bright_black(), self.pc); + // fetch opcode + let opcode: u16 = bus.read(self.pc); + // DINC pc + self.pc = self.pc.wrapping_add(2); + // decode opcode + // Print opcode disassembly: + + std::println!("{}", self.disassembler.instruction(opcode)); + use disassemble::{a, b, i, n, x, y}; + let (i, x, y, n, b, a) = ( + i(opcode), + x(opcode), + y(opcode), + n(opcode), + b(opcode), + a(opcode), + ); + match i { + // # Issue a system call + // |opcode| effect | + // |------|------------------------------------| + // | 00e0 | Clear screen memory to all 0 | + // | 00ee | Return from subroutine | + 0x0 => match a { + 0x0e0 => self.clear_screen(bus), + 0x0ee => self.ret(bus), + _ => 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, bus), + // | 3xbb | Skips next instruction if register X == b + 0x3 => self.skip_if_x_equal_byte(x, b), + // | 4xbb | Skips next instruction if register X != b + 0x4 => self.skip_if_x_not_equal_byte(x, b), + // # Performs a register-register comparison + // |opcode| effect | + // |------|------------------------------------| + // | 9XY0 | Skip next instruction if vX == vY | + 0x5 => match n { + 0x0 => self.skip_if_x_equal_y(x, y), + _ => self.unimplemented(opcode), + }, + // 6xbb: Loads immediate byte b into register vX + 0x6 => self.load_immediate(x, b), + // 7xbb: Adds immediate byte b to register vX + 0x7 => self.add_immediate(x, b), + // # 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 | + 0x8 => match n { + 0x0 => self.load_y_into_x(x, y), + 0x1 => self.x_orequals_y(x, y), + 0x2 => self.x_andequals_y(x, y), + 0x3 => self.x_xorequals_y(x, y), + 0x4 => self.x_addequals_y(x, y), + 0x5 => self.x_subequals_y(x, y), + 0x6 => self.shift_right_x(x), + 0x7 => self.backwards_subtract(x, y), + 0xE => self.shift_left_x(x), + _ => self.unimplemented(opcode), + }, + // # Performs a register-register comparison + // |opcode| effect | + // |------|------------------------------------| + // | 9XY0 | Skip next instruction if vX != vY | + 0x9 => match n { + 0 => self.skip_if_x_not_equal_y(x, y), + _ => self.unimplemented(opcode), + }, + // Aaaa: Load address #a into register I + 0xa => self.load_indirect_register(a), + // Baaa: Jump to &adr + v0 + 0xb => self.jump_indexed(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.skip_if_key_equals_x(x), + 0xa1 => self.skip_if_key_not_x(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.get_delay_timer(x, bus), + 0x0A => self.wait_for_key(x, bus), + 0x15 => self.load_delay_timer(x, bus), + 0x18 => self.load_sound_timer(x, bus), + 0x1E => self.add_to_indirect(x, bus), + 0x29 => self.load_sprite_x(x, bus), + 0x33 => self.bcd_convert_i(x, bus), + 0x55 => self.dma_store(x, bus), + 0x65 => self.dma_load(x, bus), + _ => self.unimplemented(opcode), + }, + _ => unimplemented!("Extracted nibble from byte, got >nibble?"), + } + + self.cycle += 1; + } + + pub fn dump(&self) { + let dumpstyle = owo_colors::Style::new().bright_black(); + let mut dump = format!( + "PC: {:04x}, SP: {:04x}, I: {:04x}\n", + self.pc, self.sp, self.i + ); + for (i, gpr) in self.v.into_iter().enumerate() { + dump += &format!( + "V{i:x}: {:02x} {}", + gpr, + match i % 4 { + 3 => "\n", + _ => "", + } + ) + } + dump += &format!("DLY: {}, SND: {}", self.delay, self.sound); + + std::println!("{}", dump.style(dumpstyle)); + } +} + +impl Default for CPU { + fn default() -> Self { + CPUBuilder::new().build() + } +} + +// private implementation +impl CPU { + /// Unused instructions + #[inline] + fn unimplemented(&self, opcode: u16) { + unimplemented!("Opcode: {opcode:04x}") + } + /// 0aaa: Handles a "machine language function call" (lmao) + #[inline] + fn sys(&mut self, a: Adr) { + unimplemented!("SYS\t{a:03x}"); + } + /// 00e0: Clears the screen memory to 0 + #[inline] + fn clear_screen(&mut self, bus: &mut Bus) { + for addr in self.screen..self.screen + 0x100 { + bus.write(addr, 0u8); + } + //use dump::BinDumpable; + //bus.bin_dump(self.screen as usize..self.screen as usize + 0x100); + } + /// 00ee: Returns from subroutine + #[inline] + fn ret(&mut self, bus: &mut Bus) { + self.sp = self.sp.wrapping_add(2); + self.pc = bus.read(self.sp); + } + /// 1aaa: Sets the program counter to an absolute address + #[inline] + fn jump(&mut self, a: Adr) { + self.pc = a; + } + /// 2aaa: Pushes pc onto the stack, then jumps to a + #[inline] + fn call(&mut self, a: Adr, bus: &mut Bus) { + bus.write(self.sp, self.pc); + self.sp = self.sp.wrapping_sub(2); + self.pc = a; + } + /// 3xbb: Skips the next instruction if register X == b + #[inline] + fn skip_if_x_equal_byte(&mut self, x: Reg, b: u8) { + if self.v[x] == b { + self.pc = self.pc.wrapping_add(2); + } + } + /// 4xbb: Skips the next instruction if register X != b + #[inline] + fn skip_if_x_not_equal_byte(&mut self, x: Reg, b: u8) { + if self.v[x] != b { + self.pc = self.pc.wrapping_add(2); + } + } + /// 5xy0: Skips the next instruction if register X != register Y + #[inline] + fn skip_if_x_equal_y(&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 + #[inline] + fn load_immediate(&mut self, x: Reg, b: u8) { + self.v[x] = b; + } + /// 7xbb: Adds immediate byte b to register vX + #[inline] + fn add_immediate(&mut self, x: Reg, b: u8) { + self.v[x] = self.v[x].wrapping_add(b); + } + /// Set the carry register (vF) after math + #[inline] + fn set_carry(&mut self, x: Reg, y: Reg, f: fn(u16, u16) -> u16) -> u8 { + let sum = f(self.v[x] as u16, self.v[y] as u16); + self.v[0xf] = if sum & 0xff00 != 0 { 1 } else { 0 }; + (sum & 0xff) as u8 + } + /// 8xy0: Loads the value of y into x + #[inline] + fn load_y_into_x(&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 + #[inline] + fn x_orequals_y(&mut self, x: Reg, y: Reg) { + self.v[x] |= self.v[y]; + } + /// 8xy2: Performs bitwise and of vX and vY, and stores the result in vX + #[inline] + fn x_andequals_y(&mut self, x: Reg, y: Reg) { + self.v[x] &= self.v[y]; + } + /// 8xy3: Performs bitwise xor of vX and vY, and stores the result in vX + #[inline] + fn x_xorequals_y(&mut self, x: Reg, y: Reg) { + self.v[x] ^= self.v[y]; + } + /// 8xy4: Performs addition of vX and vY, and stores the result in vX + #[inline] + fn x_addequals_y(&mut self, x: Reg, y: Reg) { + self.v[x] = self.set_carry(x, y, u16::wrapping_add); + } + /// 8xy5: Performs subtraction of vX and vY, and stores the result in vX + #[inline] + fn x_subequals_y(&mut self, x: Reg, y: Reg) { + self.v[x] = self.set_carry(x, y, u16::wrapping_sub); + } + /// 8xy6: Performs bitwise right shift of vX + #[inline] + fn shift_right_x(&mut self, x: Reg) { + self.v[x] >>= 1; + } + /// 8xy7: Performs subtraction of vY and vX, and stores the result in vX + #[inline] + fn backwards_subtract(&mut self, x: Reg, y: Reg) { + self.v[x] = self.set_carry(y, x, u16::wrapping_sub); + } + /// 8X_E: Performs bitwise left shift of vX + #[inline] + fn shift_left_x(&mut self, x: Reg) { + let shift_out: u8 = self.v[x] >> 7; + self.v[x] <<= 1; + self.v[0xf] = shift_out; + } + + /// 9xy0: Skip next instruction if X != y + #[inline] + fn skip_if_x_not_equal_y(&mut self, x: Reg, y: Reg) { + if self.v[x] != self.v[y] { + self.pc = self.pc.wrapping_add(2); + } + } + /// Aadr: Load address #adr into register I + #[inline] + fn load_indirect_register(&mut self, a: Adr) { + self.i = a; + } + /// Badr: Jump to &adr + v0 + #[inline] + fn jump_indexed(&mut self, a: Adr) { + self.pc = a.wrapping_add(self.v[0] as Adr); + } + /// Cxbb: Stores a random number + the provided byte into vX + /// Pretty sure the input byte is supposed to be the seed of a LFSR or something + #[inline] + fn rand(&mut self, x: Reg, b: u8) { + // TODO: Random Number Generator + todo!("{}", format_args!("rand\t#{b:X}, v{x:x}").red()); + } + /// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY) + #[inline] + fn draw(&mut self, x: Reg, y: Reg, n: Nib) { + // TODO: Screen + todo!("{}", format_args!("draw\t#{n:x}, v{x:x}, v{y:x}").red()); + // TODO: Repeat for all N + // TODO: Calculate the lower bound address based on the X,Y position on the screen + // TODO: Read a u16 from the bus containing the two bytes which might need to be updated + } + /// Ex9E: Skip next instruction if key == #X + #[inline] + fn skip_if_key_equals_x(&mut self, x: Reg) { + std::println!("{}", format_args!("sek\tv{x:x}")); + if self.keys >> x & 1 == 1 { + std::println!("KEY == {x}"); + self.pc += 2; + } + } + /// ExaE: Skip next instruction if key != #X + #[inline] + fn skip_if_key_not_x(&mut self, x: Reg) { + std::println!("{}", format_args!("snek\tv{x:x}")); + if self.keys >> x & 1 == 0 { + std::println!("KEY != {x}"); + self.pc += 2; + } + } + /// Fx07: Get the current DT, and put it in vX + /// ```py + /// vX = DT + /// ``` + #[inline] + fn get_delay_timer(&mut self, x: Reg, _bus: &mut Bus) { + self.v[x] = self.delay; + } + /// Fx0A: Wait for key, then vX = K + #[inline] + fn wait_for_key(&mut self, x: Reg, _bus: &mut Bus) { + // TODO: I/O + + std::println!("{}", format_args!("waitk\tv{x:x}").red()); + } + /// Fx15: Load vX into DT + /// ```py + /// DT = vX + /// ``` + #[inline] + fn load_delay_timer(&mut self, x: Reg, _bus: &mut Bus) { + self.delay = self.v[x]; + } + /// Fx18: Load vX into ST + /// ```py + /// ST = vX; + /// ``` + #[inline] + fn load_sound_timer(&mut self, x: Reg, _bus: &mut Bus) { + self.sound = self.v[x]; + } + /// Fx1e: Add vX to I, + /// ```py + /// I += vX; + /// ``` + #[inline] + fn add_to_indirect(&mut self, x: Reg, _bus: &mut Bus) { + self.i += self.v[x] as u16; + } + /// Fx29: Load sprite for character x into I + /// ```py + /// I = sprite(X); + /// ``` + #[inline] + fn load_sprite_x(&mut self, x: Reg, _bus: &mut Bus) { + self.i = self.font + (5 * x as Adr); + } + /// Fx33: BCD convert X into I`[0..3]` + #[inline] + fn bcd_convert_i(&mut self, x: Reg, _bus: &mut Bus) { + // TODO: I/O + + std::println!("{}", format_args!("bcd\t{x:x}, &I").red()); + } + /// Fx55: DMA Stor from I to registers 0..X + #[inline] + fn dma_store(&mut self, x: Reg, bus: &mut Bus) { + for reg in 0..=x { + bus.write(self.i + reg as u16, self.v[reg]); + } + self.i += x as Adr + 1; + } + /// Fx65: DMA Load from I to registers 0..X + #[inline] + fn dma_load(&mut self, x: Reg, bus: &mut Bus) { + for reg in 0..=x { + self.v[reg] = bus.read(self.i + reg as u16); + } + self.i += x as Adr + 1; + } +} diff --git a/src/cpu/disassemble.rs b/src/cpu/disassemble.rs new file mode 100644 index 0000000..6562fc8 --- /dev/null +++ b/src/cpu/disassemble.rs @@ -0,0 +1,400 @@ +//! A disassembler for Chip-8 opcodes + +use owo_colors::{OwoColorize, Style}; + +use super::{Adr, Nib, Reg}; +type Ins = Nib; + +#[inline] +pub fn i(ins: u16) -> Ins { + (ins >> 12 & 0xf) as Ins +} +#[inline] +pub fn x(ins: u16) -> Reg { + (ins >> 8 & 0xf) as Reg +} +#[inline] +pub fn y(ins: u16) -> Reg { + (ins >> 4 & 0xf) as Reg +} +#[inline] +pub fn n(ins: u16) -> Nib { + (ins & 0xf) as Nib +} +#[inline] +pub fn b(ins: u16) -> u8 { + (ins & 0xff) as u8 +} +#[inline] +pub fn a(ins: u16) -> Adr { + ins & 0x0fff +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Disassemble { + invalid: Style, + normal: Style, +} + +impl Default for Disassemble { + fn default() -> Self { + Disassemble::builder().build() + } +} + +// Public API +impl Disassemble { + // Returns a new Disassemble with the provided Styles + pub fn new(invalid: Style, normal: Style) -> Disassemble { + Disassemble { invalid, normal } + } + // + pub fn builder() -> DisassembleBuilder { + DisassembleBuilder::default() + } + // Disassemble a single instruction + pub fn instruction(&self, opcode: u16) -> String { + let (i, x, y, n, b, a) = ( + i(opcode), + x(opcode), + y(opcode), + n(opcode), + b(opcode), + a(opcode), + ); + match i { + // # Issue a system call + // |opcode| effect | + // |------|------------------------------------| + // | 00e0 | Clear screen memory to all 0 | + // | 00ee | Return from subroutine | + 0x0 => match a { + 0x0e0 => self.clear_screen(), + 0x0ee => self.ret(), + _ => 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.skip_if_x_equal_byte(x, b), + // | 4xbb | Skips next instruction if register X != b + 0x4 => self.skip_if_x_not_equal_byte(x, b), + // # Performs a register-register comparison + // |opcode| effect | + // |------|------------------------------------| + // | 9XY0 | Skip next instruction if vX == vY | + 0x5 => match n { + 0x0 => self.skip_if_x_equal_y(x, y), + _ => self.unimplemented(opcode), + }, + // 6xbb: Loads immediate byte b into register vX + 0x6 => self.load_immediate(x, b), + // 7xbb: Adds immediate byte b to register vX + 0x7 => self.add_immediate(x, b), + // # 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 | + 0x8 => match n { + 0x0 => self.load_y_into_x(x, y), + 0x1 => self.x_orequals_y(x, y), + 0x2 => self.x_andequals_y(x, y), + 0x3 => self.x_xorequals_y(x, y), + 0x4 => self.x_addequals_y(x, y), + 0x5 => self.x_subequals_y(x, y), + 0x6 => self.shift_right_x(x), + 0x7 => self.backwards_subtract(x, y), + 0xE => self.shift_left_x(x), + _ => self.unimplemented(opcode), + }, + // # Performs a register-register comparison + // |opcode| effect | + // |------|------------------------------------| + // | 9XY0 | Skip next instruction if vX != vY | + 0x9 => match n { + 0 => self.skip_if_x_not_equal_y(x, y), + _ => self.unimplemented(opcode), + }, + // Aaaa: Load address #a into register I + 0xa => self.load_indirect_register(a), + // Baaa: Jump to &adr + v0 + 0xb => self.jump_indexed(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.skip_if_key_equals_x(x), + 0xa1 => self.skip_if_key_not_x(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.get_delay_timer(x), + 0x0A => self.wait_for_key(x), + 0x15 => self.load_delay_timer(x), + 0x18 => self.load_sound_timer(x), + 0x1E => self.add_to_indirect(x), + 0x29 => self.load_sprite_x(x), + 0x33 => self.bcd_convert_i(x), + 0x55 => self.dma_store(x), + 0x65 => self.dma_load(x), + _ => self.unimplemented(opcode), + }, + _ => unimplemented!("Extracted nibble from byte, got >nibble?"), + } + } +} + +// Private api +impl Disassemble { + /// Unused instructions + fn unimplemented(&self, opcode: u16) -> String { + format!("inval\t{opcode:04x}") + .style(self.invalid) + .to_string() + } + /// `0aaa`: Handles a "machine language function call" (lmao) + pub fn sys(&self, a: Adr) -> String { + format!("sysc\t{a:03x}").style(self.invalid).to_string() + } + /// `00e0`: Clears the screen memory to 0 + pub fn clear_screen(&self) -> String { + "cls".style(self.normal).to_string() + } + /// `00ee`: Returns from subroutine + pub fn ret(&self) -> String { + "ret".style(self.normal).to_string() + } + /// `1aaa`: Sets the program counter to an absolute address + pub fn jump(&self, a: Adr) -> String { + format!("jmp\t{a:03x}").style(self.normal).to_string() + } + /// `2aaa`: Pushes pc onto the stack, then jumps to a + pub fn call(&self, a: Adr) -> String { + format!("call\t{a:03x}").style(self.normal).to_string() + } + /// `3xbb`: Skips the next instruction if register X == b + pub fn skip_if_x_equal_byte(&self, x: Reg, b: u8) -> String { + format!("se\t#{b:02x}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `4xbb`: Skips the next instruction if register X != b + pub fn skip_if_x_not_equal_byte(&self, x: Reg, b: u8) -> String { + format!("sne\t#{b:02x}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `5xy0`: Skips the next instruction if register X != register Y + pub fn skip_if_x_equal_y(&self, x: Reg, y: Reg) -> String { + format!("se\tv{x:X}, v{y:X}").style(self.normal).to_string() + } + + /// `6xbb`: Loads immediate byte b into register vX + pub fn load_immediate(&self, x: Reg, b: u8) -> String { + format!("mov\t#{b:02x}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `7xbb`: Adds immediate byte b to register vX + pub fn add_immediate(&self, x: Reg, b: u8) -> String { + format!("add\t#{b:02x}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `8xy0`: Loads the value of y into x + pub fn load_y_into_x(&self, x: Reg, y: Reg) -> String { + format!("mov\tv{y:X}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `8xy1`: Performs bitwise or of vX and vY, and stores the result in vX + pub fn x_orequals_y(&self, x: Reg, y: Reg) -> String { + format!("or\tv{y:X}, v{x:X}").style(self.normal).to_string() + } + /// `8xy2`: Performs bitwise and of vX and vY, and stores the result in vX + pub fn x_andequals_y(&self, x: Reg, y: Reg) -> String { + format!("and\tv{y:X}, v{x:X}") + .style(self.normal) + .to_string() + } + + /// `8xy3`: Performs bitwise xor of vX and vY, and stores the result in vX + pub fn x_xorequals_y(&self, x: Reg, y: Reg) -> String { + format!("xor\tv{y:X}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `8xy4`: Performs addition of vX and vY, and stores the result in vX + pub fn x_addequals_y(&self, x: Reg, y: Reg) -> String { + format!("add\tv{y:X}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `8xy5`: Performs subtraction of vX and vY, and stores the result in vX + pub fn x_subequals_y(&self, x: Reg, y: Reg) -> String { + format!("sub\tv{y:X}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `8xy6`: Performs bitwise right shift of vX + pub fn shift_right_x(&self, x: Reg) -> String { + format!("shr\tv{x:X}").style(self.normal).to_string() + } + /// `8xy7`: Performs subtraction of vY and vX, and stores the result in vX + pub fn backwards_subtract(&self, x: Reg, y: Reg) -> String { + format!("bsub\tv{y:X}, v{x:X}") + .style(self.normal) + .to_string() + } + /// 8X_E: Performs bitwise left shift of vX + pub fn shift_left_x(&self, x: Reg) -> String { + format!("shl\tv{x:X}").style(self.normal).to_string() + } + /// `9xy0`: Skip next instruction if X != y + pub fn skip_if_x_not_equal_y(&self, x: Reg, y: Reg) -> String { + format!("sn\tv{x:X}, v{y:X}").style(self.normal).to_string() + } + /// Aadr: Load address #adr into register I + pub fn load_indirect_register(&self, a: Adr) -> String { + format!("mov\t${a:03x}, I").style(self.normal).to_string() + } + /// Badr: Jump to &adr + v0 + pub fn jump_indexed(&self, a: Adr) -> String { + format!("jmp\t${a:03x}+v0").style(self.normal).to_string() + } + /// `Cxbb`: Stores a random number + the provided byte into vX + /// Pretty sure the input byte is supposed to be the seed of a LFSR or something + pub fn rand(&self, x: Reg, b: u8) -> String { + format!("rand\t#{b:X}, v{x:X}") + .style(self.normal) + .to_string() + } + /// `Dxyn`: Draws n-byte sprite to the screen at coordinates (vX, vY) + pub fn draw(&self, x: Reg, y: Reg, n: Nib) -> String { + #[rustfmt::skip] + format!("draw\t#{n:x}, v{x:X}, v{y:X}").style(self.normal).to_string() + } + /// `Ex9E`: Skip next instruction if key == #X + pub fn skip_if_key_equals_x(&self, x: Reg) -> String { + format!("sek\tv{x:X}").style(self.normal).to_string() + } + /// `ExaE`: Skip next instruction if key != #X + pub fn skip_if_key_not_x(&self, x: Reg) -> String { + format!("snek\tv{x:X}").style(self.normal).to_string() + } + /// `Fx07`: Get the current DT, and put it in vX + /// ```py + /// vX = DT + /// ``` + pub fn get_delay_timer(&self, x: Reg) -> String { + format!("mov\tDT, v{x:X}").style(self.normal).to_string() + } + /// `Fx0A`: Wait for key, then vX = K + pub fn wait_for_key(&self, x: Reg) -> String { + format!("waitk\tv{x:X}").style(self.normal).to_string() + } + /// `Fx15`: Load vX into DT + /// ```py + /// DT = vX + /// ``` + pub fn load_delay_timer(&self, x: Reg) -> String { + format!("ld\tv{x:X}, DT").style(self.normal).to_string() + } + /// `Fx18`: Load vX into ST + /// ```py + /// ST = vX; + /// ``` + pub fn load_sound_timer(&self, x: Reg) -> String { + format!("ld\tv{x:X}, ST").style(self.normal).to_string() + } + /// `Fx1e`: Add vX to I, + /// ```py + /// I += vX; + /// ``` + pub fn add_to_indirect(&self, x: Reg) -> String { + format!("add\tv{x:X}, I").style(self.normal).to_string() + } + /// `Fx29`: Load sprite for character x into I + /// ```py + /// I = sprite(X); + /// ``` + pub fn load_sprite_x(&self, x: Reg) -> String { + format!("font\t#{x:X}, I").style(self.normal).to_string() + } + /// `Fx33`: BCD convert X into I`[0..3]` + pub fn bcd_convert_i(&self, x: Reg) -> String { + format!("bcd\t{x:X}, &I").style(self.normal).to_string() + } + /// `Fx55`: DMA Stor from I to registers 0..X + pub fn dma_store(&self, x: Reg) -> String { + format!("dmao\t{x:X}").style(self.normal).to_string() + } + /// `Fx65`: DMA Load from I to registers 0..X + pub fn dma_load(&self, x: Reg) -> String { + format!("dmai\t{x:X}").style(self.normal).to_string() + } +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct DisassembleBuilder { + invalid: Option