I/O: KISS the bus, attach a screen, plug in a controller
Chip-8 has no ROM, nor memory management. - It's much easier to just use contiguous memory. - Then we can return references to slices of that memory - ~3x speed increase Screen exists now, uses 24-bit framebuffer - We have a 1-bit framebuffer - I chose colors that look good to me Controller exists as well, has 16 buttons - Mapped "0 123 456 789 ab cdef" to (QWERTY) "X 123 QWE ASD zC 4RFV" - Other chip-8 interpreters may use a different layout - This is good enough for now. - F1-F9 map to control functions - F1, F2: Dump CPU registers/screen contents - F3, F4: Toggle disassembly/pause - F5: Single-step the CPU, pausing after - F6, F7: Set/Unset breakpoint - F8, F9: Soft/Hard Reset CPU
This commit is contained in:
274
src/bus.rs
274
src/bus.rs
@@ -1,13 +1,9 @@
|
||||
//! The Bus connects the CPU to Memory
|
||||
mod bus_device;
|
||||
mod iterator;
|
||||
|
||||
use crate::dump::{BinDumpable, Dumpable};
|
||||
use bus_device::BusDevice;
|
||||
use iterator::BusIterator;
|
||||
use crate::error::Result;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{Debug, Display, Formatter, Result},
|
||||
fmt::{Debug, Display, Formatter},
|
||||
ops::Range,
|
||||
slice::SliceIndex,
|
||||
};
|
||||
@@ -17,24 +13,14 @@ use std::{
|
||||
/// ```rust
|
||||
/// # use chumpulator::prelude::*;
|
||||
/// let mut bus = bus! {
|
||||
/// "RAM" [0x0000..0x8000] Mem::new(0x8000),
|
||||
/// "ROM" [0x8000..0xFFFF] Mem::new(0x8000).w(false),
|
||||
/// "RAM" [0x0000..0x8000],
|
||||
/// "ROM" [0x8000..0xFFFF],
|
||||
/// };
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! bus {
|
||||
($($name:literal $(:)? [$range:expr] $(=)? $d:expr) ,* $(,)?) => {
|
||||
$crate::bus::Bus::new()
|
||||
$(
|
||||
.connect($name, $range, Box::new($d))
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! newbus {
|
||||
($($name:literal $(:)? [$range:expr] $(= $data:expr)?) ,* $(,)?) => {
|
||||
$crate::bus::NewBus::new()
|
||||
$crate::bus::Bus::new()
|
||||
$(
|
||||
.add_region($name, $range)
|
||||
$(
|
||||
@@ -44,17 +30,33 @@ macro_rules! newbus {
|
||||
};
|
||||
}
|
||||
|
||||
// Traits Read and Write are here purely to make implementing other things more bearable
|
||||
/// Do whatever `Read` means to you
|
||||
pub trait Read<T> {
|
||||
/// Read a T from address `addr`
|
||||
fn read(&self, addr: impl Into<usize>) -> T;
|
||||
}
|
||||
|
||||
/// Write "some data" to the Bus
|
||||
pub trait Write<T> {
|
||||
/// Write a T to address `addr`
|
||||
fn write(&mut self, addr: impl Into<usize>, data: T);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Region {}
|
||||
|
||||
/// Store memory in a series of named regions with ranges
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NewBus {
|
||||
pub struct Bus {
|
||||
memory: Vec<u8>,
|
||||
region: HashMap<&'static str, Range<usize>>,
|
||||
}
|
||||
|
||||
impl NewBus {
|
||||
impl Bus {
|
||||
/// Construct a new bus
|
||||
pub fn new() -> Self {
|
||||
NewBus::default()
|
||||
Bus::default()
|
||||
}
|
||||
/// Gets the length of the bus' backing memory
|
||||
pub fn len(&self) -> usize {
|
||||
@@ -65,7 +67,7 @@ impl NewBus {
|
||||
self.memory.is_empty()
|
||||
}
|
||||
/// Grows the NewBus backing memory to at least size bytes, but does not truncate
|
||||
pub fn with_size(&mut self, size: usize){
|
||||
pub fn with_size(&mut self, size: usize) {
|
||||
if self.len() < size {
|
||||
self.memory.resize(size, 0);
|
||||
}
|
||||
@@ -82,6 +84,12 @@ impl NewBus {
|
||||
}
|
||||
self
|
||||
}
|
||||
pub fn clear_region(&mut self, name: &str) -> &mut Self {
|
||||
if let Some(region) = self.get_region_mut(name) {
|
||||
region.fill(0)
|
||||
}
|
||||
self
|
||||
}
|
||||
/// Gets a slice of bus memory
|
||||
pub fn get<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
|
||||
where
|
||||
@@ -104,15 +112,38 @@ impl NewBus {
|
||||
pub fn get_region_mut(&mut self, name: &str) -> Option<&mut [u8]> {
|
||||
self.get_mut(self.region.get(name)?.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Read<u8> for NewBus {
|
||||
fn read(&self, addr: impl Into<usize>) -> u8 {
|
||||
*self.memory.get(addr.into()).unwrap_or(&0xc5)
|
||||
pub fn print_screen(&self) -> Result<()> {
|
||||
const REGION: &str = "screen";
|
||||
if let Some(screen) = self.get_region(REGION) {
|
||||
for (index, byte) in screen.iter().enumerate() {
|
||||
if index % 8 == 0 {
|
||||
print!("|");
|
||||
}
|
||||
print!(
|
||||
"{}",
|
||||
format!("{byte:08b}").replace('0', " ").replace('1', "██")
|
||||
);
|
||||
if index % 8 == 7 {
|
||||
println!("|");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(crate::error::Error::MissingRegion {
|
||||
region: REGION.to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Read<u16> for NewBus {
|
||||
impl Read<u8> for Bus {
|
||||
fn read(&self, addr: impl Into<usize>) -> u8 {
|
||||
let addr: usize = addr.into();
|
||||
*self.memory.get(addr).unwrap_or(&0xc5)
|
||||
}
|
||||
}
|
||||
|
||||
impl Read<u16> for Bus {
|
||||
fn read(&self, addr: impl Into<usize>) -> u16 {
|
||||
let addr: usize = addr.into();
|
||||
if let Some(bytes) = self.memory.get(addr..addr + 2) {
|
||||
@@ -120,11 +151,10 @@ impl Read<u16> for NewBus {
|
||||
} else {
|
||||
0xc5c5
|
||||
}
|
||||
//u16::from_le_bytes(self.memory.get([addr;2]))
|
||||
}
|
||||
}
|
||||
|
||||
impl Write<u8> for NewBus {
|
||||
impl Write<u8> for Bus {
|
||||
fn write(&mut self, addr: impl Into<usize>, data: u8) {
|
||||
let addr: usize = addr.into();
|
||||
if let Some(byte) = self.get_mut(addr) {
|
||||
@@ -133,183 +163,29 @@ impl Write<u8> for NewBus {
|
||||
}
|
||||
}
|
||||
|
||||
impl Write<u16> for NewBus {
|
||||
impl Write<u16> for Bus {
|
||||
fn write(&mut self, addr: impl Into<usize>, data: u16) {
|
||||
let addr: usize = addr.into();
|
||||
if let Some(slice) = self.get_mut(addr..addr + 2) {
|
||||
slice.swap_with_slice(data.to_be_bytes().as_mut())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for NewBus {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
use rhexdump::Rhexdump;
|
||||
let mut rhx = Rhexdump::default();
|
||||
rhx.set_bytes_per_group(2).expect("2 <= MAX_BYTES_PER_GROUP (8)");
|
||||
rhx.display_duplicate_lines(false);
|
||||
write!(f, "{}", rhx.hexdump(&self.memory))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<u8>;
|
||||
fn write_to(&mut self, addr: u16, data: u8);
|
||||
fn get_mut(&mut self, addr: u16) -> Option<&mut u8>;
|
||||
}
|
||||
|
||||
// Traits Read and Write are here purely to make implementing other things more bearable
|
||||
/// Do whatever `Read` means to you
|
||||
pub trait Read<T> {
|
||||
/// Read a T from address `addr`
|
||||
fn read(&self, addr: impl Into<usize>) -> T;
|
||||
}
|
||||
|
||||
/// Write "some data" to the Bus
|
||||
pub trait Write<T> {
|
||||
/// Write a T to address `addr`
|
||||
fn write(&mut self, addr: impl Into<usize>, data: T);
|
||||
}
|
||||
|
||||
/// The Bus connects bus readers with bus writers.
|
||||
/// The design assumes single-threaded operation.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Bus {
|
||||
devices: Vec<BusDevice>,
|
||||
}
|
||||
|
||||
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<u16>,
|
||||
device: Box<dyn BusConnectible>,
|
||||
) -> Self {
|
||||
self.devices.push(BusDevice::new(name, range, device));
|
||||
self
|
||||
}
|
||||
pub fn get_region_by_name(&mut self, name: &str) -> Option<&mut BusDevice> {
|
||||
self.devices.iter_mut().find(|item| item.name == name)
|
||||
}
|
||||
}
|
||||
|
||||
/// lmao
|
||||
impl BusConnectible for Bus {
|
||||
fn read_at(&self, addr: u16) -> Option<u8> {
|
||||
let mut result: u8 = 0;
|
||||
for item in &self.devices {
|
||||
result |= item.read_at(addr).unwrap_or(0)
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
fn write_to(&mut self, addr: u16, data: u8) {
|
||||
for item in &mut self.devices {
|
||||
item.write_to(addr, data)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mut(&mut self, addr: u16) -> Option<&mut u8> {
|
||||
for item in &mut self.devices {
|
||||
if let Some(mutable) = item.get_mut(addr) {
|
||||
return Some(mutable);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Read<u8> for Bus {
|
||||
fn read(&self, addr: impl Into<usize>) -> u8 {
|
||||
let addr = addr.into() as u16;
|
||||
let mut result: u8 = 0;
|
||||
for item in &self.devices {
|
||||
result |= item.read_at(addr).unwrap_or(0)
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Read<u16> for Bus {
|
||||
fn read(&self, addr: impl Into<usize>) -> u16 {
|
||||
let addr = addr.into() as u16;
|
||||
let mut result = 0;
|
||||
result |= (self.read_at(addr).unwrap_or(0) as u16) << 8;
|
||||
result |= self.read_at(addr.wrapping_add(1)).unwrap_or(0) as u16;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Write<u8> for Bus {
|
||||
fn write(&mut self, addr: impl Into<usize>, data: u8) {
|
||||
let addr = addr.into() as u16;
|
||||
for item in &mut self.devices {
|
||||
item.write_to(addr, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write<u16> for Bus {
|
||||
fn write(&mut self, addr: impl Into<usize>, data: u16) {
|
||||
let addr = addr.into() as u16;
|
||||
self.write_to(addr, (data >> 8) as u8);
|
||||
self.write_to(addr.wrapping_add(1), data as u8);
|
||||
}
|
||||
}
|
||||
|
||||
impl Write<u32> for Bus {
|
||||
fn write(&mut self, addr: impl Into<usize>, data: u32) {
|
||||
let addr = addr.into() as u16;
|
||||
for i in 0..4 {
|
||||
self.write_to(addr.wrapping_add(i), (data >> (3 - i * 8)) as u8);
|
||||
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Bus {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
for device in &self.devices {
|
||||
write!(f, "{device}")?;
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
use rhexdump::Rhexdump;
|
||||
let mut rhx = Rhexdump::default();
|
||||
rhx.set_bytes_per_group(2)
|
||||
.expect("2 <= MAX_BYTES_PER_GROUP (8)");
|
||||
rhx.display_duplicate_lines(false);
|
||||
for (&name, range) in &self.region {
|
||||
writeln!(
|
||||
f,
|
||||
"[{name}]\n{}\n",
|
||||
rhx.hexdump(&self.memory[range.clone()])
|
||||
)?
|
||||
}
|
||||
write!(f, "")
|
||||
}
|
||||
}
|
||||
|
||||
impl Dumpable for Bus {
|
||||
fn dump(&self, range: Range<usize>) {
|
||||
for (index, byte) in self
|
||||
.into_iter()
|
||||
.range(range.start as u16..range.end as u16) // this causes a truncation
|
||||
.enumerate()
|
||||
{
|
||||
crate::dump::as_hexdump(index, byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BinDumpable for Bus {
|
||||
fn bin_dump(&self, range: Range<usize>) {
|
||||
for index in range {
|
||||
let byte: u8 = self.read(index as u16);
|
||||
crate::dump::as_bindump(index, byte)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Bus {
|
||||
type Item = u8;
|
||||
|
||||
type IntoIter = iterator::BusIterator<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
BusIterator::new(0..u16::MAX, self)
|
||||
}
|
||||
}
|
||||
|
||||
304
src/cpu.rs
304
src/cpu.rs
@@ -3,47 +3,37 @@
|
||||
pub mod disassemble;
|
||||
|
||||
use self::disassemble::Disassemble;
|
||||
use crate::bus::{Read, Write};
|
||||
use crate::bus::{Bus, Read, Write};
|
||||
use owo_colors::OwoColorize;
|
||||
use rand::random;
|
||||
use std::time::Instant;
|
||||
|
||||
type Reg = usize;
|
||||
type Adr = u16;
|
||||
type Nib = u8;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct CPUBuilder {
|
||||
screen: Option<Adr>,
|
||||
font: Option<Adr>,
|
||||
pc: Option<Adr>,
|
||||
sp: Option<Adr>,
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ControlFlags {
|
||||
pub debug: bool,
|
||||
pub pause: bool,
|
||||
pub keypause: bool,
|
||||
pub authentic: bool,
|
||||
}
|
||||
|
||||
impl CPUBuilder {
|
||||
pub fn new() -> Self {
|
||||
CPUBuilder {
|
||||
screen: None,
|
||||
font: None,
|
||||
pc: None,
|
||||
sp: None,
|
||||
}
|
||||
impl ControlFlags {
|
||||
pub fn debug(&mut self) {
|
||||
self.debug = !self.debug
|
||||
}
|
||||
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(),
|
||||
}
|
||||
pub fn pause(&mut self) {
|
||||
self.pause = !self.pause
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Keys {
|
||||
keys: [bool; 16],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CPU {
|
||||
// memory map info
|
||||
@@ -57,19 +47,30 @@ pub struct CPU {
|
||||
delay: u8,
|
||||
sound: u8,
|
||||
// I/O
|
||||
keys: usize,
|
||||
pub keys: [bool; 16],
|
||||
pub flags: ControlFlags,
|
||||
// Execution data
|
||||
cycle: usize,
|
||||
breakpoints: Vec<Adr>,
|
||||
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
|
||||
pub fn press(&mut self, key: usize) {
|
||||
if (0..16).contains(&key) {
|
||||
self.keys[key] = true;
|
||||
self.flags.keypause = false;
|
||||
}
|
||||
}
|
||||
/// Release all keys
|
||||
pub fn release(&mut self) {
|
||||
for key in &mut self.keys {
|
||||
*key = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a general purpose register in the CPU
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
@@ -80,11 +81,10 @@ impl CPU {
|
||||
/// // Dump the CPU registers
|
||||
/// cpu.dump();
|
||||
/// ```
|
||||
pub fn set_gpr(mut self, gpr: Reg, value: u8) -> Self {
|
||||
pub fn set_gpr(&mut self, gpr: Reg, value: u8) {
|
||||
if let Some(gpr) = self.v.get_mut(gpr) {
|
||||
*gpr = value;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Constructs a new CPU with sane defaults
|
||||
@@ -112,23 +112,80 @@ impl CPU {
|
||||
delay: 0,
|
||||
sound: 0,
|
||||
cycle: 0,
|
||||
keys: 0,
|
||||
keys: [false; 16],
|
||||
breakpoints: vec![],
|
||||
flags: ControlFlags {
|
||||
debug: true,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick<B>(&mut self, bus: &mut B)
|
||||
where
|
||||
B: Read<u8> + Write<u8> + Read<u16> + Write<u16>
|
||||
{
|
||||
std::print!("{:3} {:03x}: ", self.cycle.bright_black(), self.pc);
|
||||
/// Get the program counter
|
||||
pub fn pc(&self) -> Adr {
|
||||
self.pc
|
||||
}
|
||||
|
||||
/// Soft resets the CPU, releasing keypause and reinitializing the program counter to 0x200
|
||||
pub fn soft_reset(&mut self) {
|
||||
self.pc = 0x200;
|
||||
self.flags.keypause = false;
|
||||
}
|
||||
|
||||
/// Set a breakpoint
|
||||
pub fn set_break(&mut self, point: Adr) {
|
||||
if !self.breakpoints.contains(&point) {
|
||||
self.breakpoints.push(point)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unset a breakpoint
|
||||
pub fn unset_break(&mut self, point: Adr) {
|
||||
fn linear_find(needle: Adr, haystack: &Vec<Adr>) -> Option<usize> {
|
||||
for (i, v) in haystack.iter().enumerate() {
|
||||
if *v == needle {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
if let Some(idx) = linear_find(point, &self.breakpoints) {
|
||||
assert_eq!(point, self.breakpoints.swap_remove(idx));
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpauses the emulator for a single tick
|
||||
/// NOTE: does not synchronize with delay timers
|
||||
pub fn singlestep(&mut self, bus: &mut Bus) {
|
||||
self.flags.pause = false;
|
||||
self.tick(bus);
|
||||
self.flags.pause = true;
|
||||
}
|
||||
|
||||
/// Ticks the delay and sound timers
|
||||
pub fn tick_timer(&mut self) {
|
||||
if self.flags.pause {
|
||||
return;
|
||||
}
|
||||
self.delay = self.delay.saturating_sub(1);
|
||||
self.sound = self.sound.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Runs a single instruction
|
||||
pub fn tick(&mut self, bus: &mut Bus) {
|
||||
// Do nothing if paused
|
||||
if self.flags.pause || self.flags.keypause {
|
||||
return;
|
||||
}
|
||||
let time = Instant::now();
|
||||
// fetch opcode
|
||||
let opcode: u16 = bus.read(self.pc);
|
||||
let pc = 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),
|
||||
@@ -246,9 +303,24 @@ impl CPU {
|
||||
_ => self.unimplemented(opcode),
|
||||
},
|
||||
_ => unimplemented!("Extracted nibble from byte, got >nibble?"),
|
||||
|
||||
}
|
||||
let elapsed = time.elapsed();
|
||||
// Print opcode disassembly:
|
||||
if self.flags.debug {
|
||||
std::println!(
|
||||
"{:3} {:03x}: {:<36}{:?}",
|
||||
self.cycle.bright_black(),
|
||||
pc,
|
||||
self.disassembler.instruction(opcode),
|
||||
elapsed.dimmed()
|
||||
);
|
||||
}
|
||||
|
||||
self.cycle += 1;
|
||||
// process breakpoints
|
||||
if self.breakpoints.contains(&self.pc) {
|
||||
self.flags.pause = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump(&self) {
|
||||
@@ -275,7 +347,24 @@ impl CPU {
|
||||
|
||||
impl Default for CPU {
|
||||
fn default() -> Self {
|
||||
CPUBuilder::new().build()
|
||||
CPU {
|
||||
screen: 0xf00,
|
||||
font: 0x050,
|
||||
pc: 0x200,
|
||||
sp: 0xefe,
|
||||
i: 0,
|
||||
v: [0; 16],
|
||||
delay: 0,
|
||||
sound: 0,
|
||||
cycle: 0,
|
||||
keys: [false; 16],
|
||||
flags: ControlFlags {
|
||||
debug: true,
|
||||
..Default::default()
|
||||
},
|
||||
breakpoints: vec![],
|
||||
disassembler: Disassemble::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,9 +382,11 @@ impl CPU {
|
||||
}
|
||||
/// 00e0: Clears the screen memory to 0
|
||||
#[inline]
|
||||
fn clear_screen(&mut self, bus: &mut impl Write<u8>) {
|
||||
for addr in self.screen..self.screen + 0x100 {
|
||||
bus.write(addr, 0u8);
|
||||
fn clear_screen(&mut self, bus: &mut Bus) {
|
||||
if let Some(screen) = bus.get_region_mut("screen") {
|
||||
for byte in screen {
|
||||
*byte = 0;
|
||||
}
|
||||
}
|
||||
//use dump::BinDumpable;
|
||||
//bus.bin_dump(self.screen as usize..self.screen as usize + 0x100);
|
||||
@@ -351,9 +442,9 @@ impl CPU {
|
||||
}
|
||||
/// Set the carry register (vF) after math
|
||||
#[inline]
|
||||
fn set_carry(&mut self, x: Reg, y: Reg, f: fn(u16, u16) -> u16) -> u8 {
|
||||
fn set_carry(&mut self, x: Reg, y: Reg, f: fn(u16, u16) -> u16, inv: bool) -> u8 {
|
||||
let sum = f(self.v[x] as u16, self.v[y] as u16);
|
||||
self.v[0xf] = if sum & 0xff00 != 0 { 1 } else { 0 };
|
||||
self.v[0xf] = if (sum & 0xff00 != 0) ^ inv { 1 } else { 0 };
|
||||
(sum & 0xff) as u8
|
||||
}
|
||||
/// 8xy0: Loads the value of y into x
|
||||
@@ -379,22 +470,24 @@ impl CPU {
|
||||
/// 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);
|
||||
self.v[x] = self.set_carry(x, y, u16::wrapping_add, false);
|
||||
}
|
||||
/// 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);
|
||||
self.v[x] = self.set_carry(x, y, u16::wrapping_sub, true);
|
||||
}
|
||||
/// 8xy6: Performs bitwise right shift of vX
|
||||
#[inline]
|
||||
fn shift_right_x(&mut self, x: Reg) {
|
||||
let shift_out = self.v[x] & 1;
|
||||
self.v[x] >>= 1;
|
||||
self.v[0xf] = shift_out;
|
||||
}
|
||||
/// 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);
|
||||
self.v[x] = self.set_carry(y, x, u16::wrapping_sub, true);
|
||||
}
|
||||
/// 8X_E: Performs bitwise left shift of vX
|
||||
#[inline]
|
||||
@@ -421,48 +514,48 @@ impl CPU {
|
||||
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
|
||||
/// Cxbb: Stores a random number & the provided byte into vX
|
||||
#[inline]
|
||||
fn rand(&mut self, x: Reg, b: u8) {
|
||||
// TODO: Random Number Generator
|
||||
todo!("{}", format_args!("rand\t#{b:X}, v{x:x}").red());
|
||||
self.v[x] = random::<u8>() & b;
|
||||
}
|
||||
/// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY)
|
||||
#[inline]
|
||||
fn draw<I>(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut I)
|
||||
where
|
||||
I: Read<u8> + Read<u16>
|
||||
{
|
||||
println!("{}", format_args!("draw\t#{n:x}, v{x:x}, v{y:x}").red());
|
||||
fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
|
||||
// println!("{}", format_args!("draw\t#{n:x}, (x: {:x}, y: {:x})", self.v[x], self.v[y]).green());
|
||||
let (x, y) = (self.v[x], self.v[y]);
|
||||
self.v[0xf] = 0;
|
||||
// TODO: Repeat for all N
|
||||
for byte in 0..n as u16 {
|
||||
// TODO: Calculate the lower bound address based on the X,Y position on the screen
|
||||
let lower_bound = ((y as u16 + byte) * 8) + x as u16 / 8;
|
||||
// TODO: Read a byte of sprite data into a u16, and shift it x % 8 bits
|
||||
let sprite_line: u8 = bus.read(self.i);
|
||||
// TODO: Read a u16 from the bus containing the two bytes which might need to be updated
|
||||
let screen_word: u16 = bus.read(self.screen + lower_bound);
|
||||
// TODO: Update the screen word by XORing the sprite byte
|
||||
todo!("{sprite_line}, {screen_word}")
|
||||
// Calculate the lower bound address based on the X,Y position on the screen
|
||||
let addr = ((y as u16 + byte) * 8) + (x / 8) as u16 + 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) << 1 + (7 - x % 8);
|
||||
// Read a u16 from the bus containing the two bytes which might need to be updated
|
||||
let mut screen: u16 = bus.read(addr);
|
||||
// Save the bits-toggled-off flag if necessary
|
||||
if screen & sprite != 0 {
|
||||
self.v[0xF] = 1
|
||||
}
|
||||
// Update the screen word by XORing the sprite byte
|
||||
screen ^= sprite;
|
||||
// Save the result back to the screen
|
||||
bus.write(addr, screen);
|
||||
}
|
||||
}
|
||||
/// 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}");
|
||||
let x = self.v[x] as usize;
|
||||
if self.keys[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}");
|
||||
let x = self.v[x] as usize;
|
||||
if !self.keys[x] {
|
||||
self.pc += 2;
|
||||
}
|
||||
}
|
||||
@@ -477,9 +570,17 @@ impl CPU {
|
||||
/// Fx0A: Wait for key, then vX = K
|
||||
#[inline]
|
||||
fn wait_for_key(&mut self, x: Reg) {
|
||||
// TODO: I/O
|
||||
|
||||
std::println!("{}", format_args!("waitk\tv{x:x}").red());
|
||||
let mut pressed = false;
|
||||
for bit in 0..16 {
|
||||
if self.keys[bit] {
|
||||
self.v[x] = bit as u8;
|
||||
pressed = true;
|
||||
}
|
||||
}
|
||||
if !pressed {
|
||||
self.pc = self.pc.wrapping_sub(2);
|
||||
self.flags.keypause = true;
|
||||
}
|
||||
}
|
||||
/// Fx15: Load vX into DT
|
||||
/// ```py
|
||||
@@ -511,29 +612,46 @@ impl CPU {
|
||||
/// ```
|
||||
#[inline]
|
||||
fn load_sprite_x(&mut self, x: Reg) {
|
||||
self.i = self.font + (5 * x as Adr);
|
||||
self.i = self.font + (5 * (self.v[x] as Adr % 0x10));
|
||||
}
|
||||
/// Fx33: BCD convert X into I`[0..3]`
|
||||
#[inline]
|
||||
fn bcd_convert_i(&mut self, x: Reg, _bus: &mut impl Write<u8>) {
|
||||
// TODO: I/O
|
||||
|
||||
std::println!("{}", format_args!("bcd\t{x:x}, &I").red());
|
||||
fn bcd_convert_i(&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
|
||||
#[inline]
|
||||
fn dma_store(&mut self, x: Reg, bus: &mut impl Write<u8>) {
|
||||
for reg in 0..=x {
|
||||
bus.write(self.i + reg as u16, self.v[reg]);
|
||||
fn dma_store(&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.authentic {
|
||||
self.i += x as Adr + 1;
|
||||
}
|
||||
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 impl Read<u8>) {
|
||||
for reg in 0..=x {
|
||||
self.v[reg] = bus.read(self.i + reg as u16);
|
||||
fn dma_load(&mut self, x: Reg, bus: &mut Bus) {
|
||||
let i = self.i as usize;
|
||||
for (reg, value) in bus
|
||||
.get(i + 0..=i + x)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
self.v[reg] = *value;
|
||||
}
|
||||
if self.flags.authentic {
|
||||
self.i += x as Adr + 1;
|
||||
}
|
||||
self.i += x as Adr + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,190 +178,190 @@ impl Disassemble {
|
||||
impl Disassemble {
|
||||
/// Unused instructions
|
||||
fn unimplemented(&self, opcode: u16) -> String {
|
||||
format!("inval\t{opcode:04x}")
|
||||
format!("inval {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()
|
||||
format!("sysc {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()
|
||||
"cls ".style(self.normal).to_string()
|
||||
}
|
||||
/// `00ee`: Returns from subroutine
|
||||
pub fn ret(&self) -> String {
|
||||
"ret".style(self.normal).to_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()
|
||||
format!("jmp {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()
|
||||
format!("call {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}")
|
||||
format!("se #{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}")
|
||||
format!("sne #{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()
|
||||
format!("se v{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}")
|
||||
format!("mov #{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}")
|
||||
format!("add #{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}")
|
||||
format!("mov v{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()
|
||||
format!("or v{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}")
|
||||
format!("and v{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}")
|
||||
format!("xor v{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}")
|
||||
format!("add v{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}")
|
||||
format!("sub v{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()
|
||||
format!("shr v{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}")
|
||||
format!("bsub v{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()
|
||||
format!("shl v{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()
|
||||
format!("sne v{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()
|
||||
format!("mov ${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()
|
||||
format!("jmp ${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}")
|
||||
format!("rand #{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()
|
||||
format!("draw #{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()
|
||||
format!("sek v{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()
|
||||
format!("snek v{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()
|
||||
format!("mov DT, 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()
|
||||
format!("waitk v{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()
|
||||
format!("ld v{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()
|
||||
format!("ld v{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()
|
||||
format!("add v{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()
|
||||
format!("font #{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()
|
||||
format!("bcd {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()
|
||||
format!("dmao {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()
|
||||
format!("dmai {x:X}").style(self.normal).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,16 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
#[derive(Clone, Debug, Error, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unrecognized opcode {word}")]
|
||||
UnimplementedInstruction { word: u16 },
|
||||
#[error("Math was funky when parsing {word}: {explanation}")]
|
||||
FunkyMath { word: u16, explanation: String },
|
||||
#[error("No {region} found on bus")]
|
||||
MissingRegion { region: String },
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
WindowError(#[from] minifb::Error)
|
||||
}
|
||||
|
||||
125
src/io.rs
Normal file
125
src/io.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
//!
|
||||
|
||||
use crate::{bus::Bus, cpu::CPU, error::Result};
|
||||
use minifb::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct WindowBuilder {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
pub name: Option<&'static str>,
|
||||
pub window_options: WindowOptions,
|
||||
}
|
||||
|
||||
impl WindowBuilder {
|
||||
pub fn new(height: usize, width: usize) -> Self {
|
||||
WindowBuilder {
|
||||
width,
|
||||
height,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn build(self) -> Result<Window> {
|
||||
Ok(Window::new(
|
||||
self.name.unwrap_or_default(),
|
||||
self.width,
|
||||
self.height,
|
||||
self.window_options,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WindowBuilder {
|
||||
fn default() -> Self {
|
||||
WindowBuilder {
|
||||
width: 64,
|
||||
height: 32,
|
||||
name: Some("Chip-8 Interpreter"),
|
||||
window_options: WindowOptions {
|
||||
title: true,
|
||||
resize: false,
|
||||
scale: Scale::X16,
|
||||
scale_mode: ScaleMode::AspectRatioStretch,
|
||||
none: true,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameBufferFormat {
|
||||
pub fg: u32,
|
||||
pub bg: u32
|
||||
}
|
||||
|
||||
impl Default for FrameBufferFormat {
|
||||
fn default() -> Self {
|
||||
FrameBufferFormat { fg: 0x0011a434, bg: 0x001E2431 }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameBuffer {
|
||||
buffer: Vec<u32>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
format: FrameBufferFormat,
|
||||
}
|
||||
|
||||
impl FrameBuffer {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
FrameBuffer {
|
||||
buffer: vec![0x00be4d; width * height],
|
||||
width,
|
||||
height,
|
||||
format: Default::default(),
|
||||
}
|
||||
}
|
||||
pub fn render(&mut self, window: &mut Window, bus: &Bus) {
|
||||
if let Some(screen) = bus.get_region("screen") {
|
||||
for (idx, byte) in screen.iter().enumerate() {
|
||||
for bit in 0..8 {
|
||||
self.buffer[8 * idx + bit] = if byte & (1 << 7 - bit) as u8 != 0 {
|
||||
self.format.fg
|
||||
} else {
|
||||
self.format.bg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: NOT THIS
|
||||
window
|
||||
.update_with_buffer(&self.buffer, self.width, self.height)
|
||||
.expect("The window manager has encountered an issue I don't want to deal with");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn identify_key(key: Key) -> usize {
|
||||
match key {
|
||||
Key::Key1 => 0x1,
|
||||
Key::Key2 => 0x2,
|
||||
Key::Key3 => 0x3,
|
||||
Key::Key4 => 0xc,
|
||||
Key::Q => 0x4,
|
||||
Key::W => 0x5,
|
||||
Key::E => 0x6,
|
||||
Key::R => 0xD,
|
||||
Key::A => 0x7,
|
||||
Key::S => 0x8,
|
||||
Key::D => 0x9,
|
||||
Key::F => 0xE,
|
||||
Key::Z => 0xA,
|
||||
Key::X => 0x0,
|
||||
Key::C => 0xB,
|
||||
Key::V => 0xF,
|
||||
_ => 0x10,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets keys from the Window, and feeds them directly to the CPU
|
||||
pub fn get_keys(window: &mut Window, cpu: &mut CPU) {
|
||||
cpu.release();
|
||||
window
|
||||
.get_keys()
|
||||
.iter()
|
||||
.for_each(|key| cpu.press(identify_key(*key)));
|
||||
}
|
||||
11
src/lib.rs
11
src/lib.rs
@@ -1,4 +1,4 @@
|
||||
#![feature(let_chains, stmt_expr_attributes)]
|
||||
#![feature(stmt_expr_attributes)]
|
||||
/*!
|
||||
This crate implements a Chip-8 interpreter as if it were a real CPU architecture,
|
||||
to the best of my current knowledge. As it's the first emulator project I've
|
||||
@@ -9,20 +9,17 @@ Hopefully, though, you'll find some use in it.
|
||||
|
||||
pub mod bus;
|
||||
pub mod cpu;
|
||||
pub mod mem;
|
||||
pub mod io;
|
||||
|
||||
pub mod dump;
|
||||
pub mod error;
|
||||
pub mod screen;
|
||||
|
||||
/// Common imports for chumpulator
|
||||
pub mod prelude {
|
||||
use super::*;
|
||||
pub use crate::bus;
|
||||
pub use crate::newbus;
|
||||
pub use bus::{Bus, BusConnectible, Read, Write};
|
||||
pub use bus::{Bus, Read, Write};
|
||||
pub use cpu::{disassemble::Disassemble, CPU};
|
||||
pub use dump::{BinDumpable, Dumpable};
|
||||
pub use mem::Mem;
|
||||
pub use screen::Screen;
|
||||
pub use io::{*, WindowBuilder};
|
||||
}
|
||||
|
||||
195
src/main.rs
195
src/main.rs
@@ -1,71 +1,154 @@
|
||||
use chumpulator::{bus::Read, prelude::*};
|
||||
//! Chirp: A chip-8 interpreter in Rust
|
||||
//! Hello, world!
|
||||
|
||||
use chirp::{error::Result, prelude::*};
|
||||
use gumdrop::*;
|
||||
use minifb::*;
|
||||
use std::fs::read;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// What I want:
|
||||
/// I want a data bus that stores as much memory as I need to implement a chip 8 emulator
|
||||
/// I want that data bus to hold named memory ranges and have a way to get a memory region
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)]
|
||||
struct Arguments {
|
||||
#[options(help = "Enable behavior incompatible with modern software")]
|
||||
pub authentic: bool,
|
||||
#[options(
|
||||
help = "Set breakpoints for the emulator to stop at",
|
||||
parse(try_from_str = "parse_hex")
|
||||
)]
|
||||
pub breakpoints: Vec<u16>,
|
||||
#[options(help = "Enable debug mode at startup")]
|
||||
pub debug: bool,
|
||||
#[options(help = "Enable pause mode at startup")]
|
||||
pub pause: bool,
|
||||
#[options(help = "Load a ROM to run on Chirp")]
|
||||
pub file: PathBuf,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
let mut now;
|
||||
println!("Building Bus...");
|
||||
let mut time = Instant::now();
|
||||
fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> {
|
||||
u16::from_str_radix(value, 16)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let options = Arguments::parse_args_default_or_exit();
|
||||
|
||||
// Create the data bus
|
||||
let mut bus = bus! {
|
||||
// Load the charset into ROM
|
||||
"charset" [0x0050..0x00a0] = Mem::new(0x50).load_charset(0).w(false),
|
||||
"charset" [0x0050..0x00A0] = include_bytes!("mem/charset.bin"),
|
||||
// Load the ROM file into RAM
|
||||
"userram" [0x0200..0x0F00] = Mem::new(0xF00 - 0x200).load(0, &read("chip-8/Fishie.ch8")?),
|
||||
// Create a screen
|
||||
"screen" [0x0F00..0x1000] = Mem::new(32*64/8),
|
||||
// Create some stack memory
|
||||
"stack" [0xF000..0xF800] = Mem::new(0x800).r(true).w(true),
|
||||
};
|
||||
now = time.elapsed();
|
||||
println!("Elapsed: {:?}\nBuilding NewBus...", now);
|
||||
time = Instant::now();
|
||||
let mut newbus = newbus! {
|
||||
// Load the charset into ROM
|
||||
"charset" [0x0050..0x00a0] = include_bytes!("mem/charset.bin"),
|
||||
// Load the ROM file into RAM
|
||||
"userram" [0x0200..0x0F00] = &read("chip-8/Fishie.ch8")?,
|
||||
"userram" [0x0200..0x0F00] = &read(options.file)?,
|
||||
// Create a screen
|
||||
"screen" [0x0F00..0x1000],
|
||||
// Create some stack memory
|
||||
"stack" [0x2000..0x2800],
|
||||
//"stack" [0x2000..0x2100],
|
||||
};
|
||||
now = time.elapsed();
|
||||
println!("Elapsed: {:?}", now);
|
||||
println!("{newbus}");
|
||||
|
||||
let disassembler = Disassemble::default();
|
||||
if false {
|
||||
for addr in 0x200..0x290 {
|
||||
if addr % 2 == 0 {
|
||||
println!(
|
||||
"{addr:03x}: {}",
|
||||
disassembler.instruction(bus.read(addr as usize))
|
||||
);
|
||||
// let disassembler = Disassemble::default();
|
||||
// if false {
|
||||
// for addr in 0x200..0x290 {
|
||||
// if addr % 2 == 0 {
|
||||
// println!(
|
||||
// "{addr:03x}: {}",
|
||||
// disassembler.instruction(bus.read(addr as usize))
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
let mut cpu = CPU::new(0xf00, 0x50, 0x200, 0x20fe, Disassemble::default());
|
||||
for point in options.breakpoints {
|
||||
cpu.set_break(point);
|
||||
}
|
||||
cpu.flags.authentic = options.authentic;
|
||||
cpu.flags.debug = options.debug;
|
||||
cpu.flags.pause = options.pause;
|
||||
let mut framebuffer = FrameBuffer::new(64, 32);
|
||||
let mut window = WindowBuilder::default().build()?;
|
||||
let mut frame_time = Instant::now();
|
||||
let mut step_time = Instant::now();
|
||||
|
||||
framebuffer.render(&mut window, &mut bus);
|
||||
|
||||
cpu.flags.pause = false;
|
||||
cpu.flags.debug = true;
|
||||
|
||||
loop {
|
||||
if !cpu.flags.pause {
|
||||
cpu.tick(&mut bus);
|
||||
}
|
||||
while frame_time.elapsed() > Duration::from_micros(16000) {
|
||||
if cpu.flags.pause {
|
||||
window.set_title("Chirp ⏸")
|
||||
} else {
|
||||
window.set_title("Chirp ▶")
|
||||
}
|
||||
frame_time += Duration::from_micros(16000);
|
||||
// tick sound and delay timers
|
||||
cpu.tick_timer();
|
||||
// update framebuffer
|
||||
framebuffer.render(&mut window, &mut bus);
|
||||
// get key input (has to happen after framebuffer)
|
||||
get_keys(&mut window, &mut cpu);
|
||||
// handle keys at the
|
||||
for key in window.get_keys_pressed(KeyRepeat::No) {
|
||||
use Key::*;
|
||||
match key {
|
||||
F1 => cpu.dump(),
|
||||
F2 => bus
|
||||
.print_screen()
|
||||
.expect("The 'screen' memory region exists"),
|
||||
F3 => {
|
||||
println!(
|
||||
"{}",
|
||||
endis("Debug", {
|
||||
cpu.flags.debug();
|
||||
cpu.flags.debug
|
||||
})
|
||||
)
|
||||
}
|
||||
F4 => println!(
|
||||
"{}",
|
||||
endis("Pause", {
|
||||
cpu.flags.pause();
|
||||
cpu.flags.pause
|
||||
})
|
||||
),
|
||||
F5 => {
|
||||
println!("Step");
|
||||
cpu.singlestep(&mut bus)
|
||||
}
|
||||
F6 => {
|
||||
println!("Set breakpoint {:x}", cpu.pc());
|
||||
cpu.set_break(cpu.pc())
|
||||
}
|
||||
F7 => {
|
||||
println!("Unset breakpoint {:x}", cpu.pc());
|
||||
cpu.unset_break(cpu.pc())
|
||||
}
|
||||
F8 => {
|
||||
println!("Soft reset CPU {:x}", cpu.pc());
|
||||
cpu.soft_reset();
|
||||
bus.clear_region("screen");
|
||||
}
|
||||
F9 => {
|
||||
println!("Hard reset CPU");
|
||||
cpu = CPU::default();
|
||||
bus.clear_region("screen");
|
||||
}
|
||||
Escape => return Ok(()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_micros(1666).saturating_sub(step_time.elapsed()));
|
||||
step_time = Instant::now();
|
||||
}
|
||||
|
||||
let mut cpu = CPU::new(0xf00, 0x50, 0x200, 0xf7fe, disassembler);
|
||||
let mut cpu2 = cpu.clone();
|
||||
println!("Old Bus:");
|
||||
for _instruction in 0..6 {
|
||||
time = Instant::now();
|
||||
cpu.tick(&mut bus);
|
||||
now = time.elapsed();
|
||||
println!(" Elapsed: {:?}", now);
|
||||
std::thread::sleep(Duration::from_micros(2000).saturating_sub(time.elapsed()));
|
||||
}
|
||||
println!("New Bus:");
|
||||
for _instruction in 0..6 {
|
||||
time = Instant::now();
|
||||
cpu2.tick(&mut newbus);
|
||||
now = time.elapsed();
|
||||
println!(" Elapsed: {:?}", now);
|
||||
std::thread::sleep(Duration::from_micros(2000).saturating_sub(time.elapsed()));
|
||||
}
|
||||
Ok(())
|
||||
//Ok(())
|
||||
}
|
||||
|
||||
fn endis(name: &str, state: bool) -> String {
|
||||
format!("{name} {}", if state { "enabled" } else { "disabled" })
|
||||
}
|
||||
|
||||
153
src/mem.rs
153
src/mem.rs
@@ -1,153 +0,0 @@
|
||||
//! Mem covers WOM, ROM, and RAM
|
||||
|
||||
use crate::{bus::BusConnectible, dump::Dumpable};
|
||||
use owo_colors::{OwoColorize, Style};
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
const MSIZE: usize = 0x1000;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Attr {
|
||||
pub r: bool,
|
||||
pub w: bool,
|
||||
}
|
||||
|
||||
pub struct MemWindow<'a> {
|
||||
mem: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> Display for MemWindow<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
// Green phosphor style formatting, for taste
|
||||
let term: Style = Style::new().bold().green().on_black();
|
||||
for (index, byte) in self.mem.iter().enumerate() {
|
||||
if index % 16 == 0 {
|
||||
write!(f, "{:>03x}{} ", index.style(term), ":".style(term))?
|
||||
}
|
||||
write!(f, "{byte:02x}")?;
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match index % 16 {
|
||||
0xf => "\n",
|
||||
0x7 => " ",
|
||||
_ if index % 2 == 1 => " ",
|
||||
_ => "",
|
||||
}
|
||||
)?
|
||||
}
|
||||
write!(f, "")
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents some kind of abstract memory chip
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Mem {
|
||||
mem: Vec<u8>,
|
||||
attr: Attr,
|
||||
}
|
||||
|
||||
impl Mem {
|
||||
pub fn r(mut self, readable: bool) -> Self {
|
||||
self.attr.r = readable;
|
||||
self
|
||||
}
|
||||
pub fn w(mut self, writable: bool) -> Self {
|
||||
self.attr.w = writable;
|
||||
self
|
||||
}
|
||||
/// Returns the number of bytes in the `Mem`, also referred to as its length.
|
||||
///
|
||||
/// # Examples
|
||||
/// ``` rust
|
||||
/// # use chumpulator::prelude::*;
|
||||
/// let mem = Mem::new(0x100);
|
||||
/// assert_eq!(mem.len(), 0x100)
|
||||
/// ```
|
||||
pub fn len(&self) -> usize {
|
||||
self.mem.len()
|
||||
}
|
||||
|
||||
/// Because clippy is so kind:
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.mem.is_empty()
|
||||
}
|
||||
|
||||
/// Loads data into a `Mem`
|
||||
pub fn load(mut self, addr: u16, bytes: &[u8]) -> Self {
|
||||
let addr = addr as usize;
|
||||
let end = self.mem.len().min(addr + bytes.len());
|
||||
self.mem[addr..end].copy_from_slice(&bytes[0..bytes.len()]);
|
||||
self
|
||||
}
|
||||
|
||||
/// Load a character set from chumpulator/src/mem/charset.bin into this memory section
|
||||
pub fn load_charset(self, addr: u16) -> Self {
|
||||
let charset = include_bytes!("mem/charset.bin");
|
||||
self.load(addr, charset)
|
||||
}
|
||||
|
||||
/// Creates a new `mem` with the specified length
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use chumpulator::prelude::*;
|
||||
/// let length = 0x100;
|
||||
/// let mem = Mem::new(length);
|
||||
/// ```
|
||||
pub fn new(len: usize) -> Self {
|
||||
Mem {
|
||||
mem: vec![0; len],
|
||||
attr: Attr { r: true, w: true },
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a window into the Mem which implements Display
|
||||
pub fn window(&self, range: Range<usize>) -> MemWindow {
|
||||
MemWindow {
|
||||
mem: &self.mem[range],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mem {
|
||||
fn default() -> Self {
|
||||
Self::new(MSIZE)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mem {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}", self.window(0..self.len()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Dumpable for Mem {
|
||||
fn dump(&self, range: Range<usize>) {
|
||||
print!("Mem {range:2x?}: ");
|
||||
print!("{}", self.window(range));
|
||||
}
|
||||
}
|
||||
|
||||
impl BusConnectible for Mem {
|
||||
fn read_at(&self, addr: u16) -> Option<u8> {
|
||||
if !self.attr.r {
|
||||
return None;
|
||||
}
|
||||
self.mem.get(addr as usize).copied()
|
||||
}
|
||||
fn write_to(&mut self, addr: u16, data: u8) {
|
||||
if self.attr.w && let Some(value) = self.mem.get_mut(addr as usize) {
|
||||
*value = data
|
||||
}
|
||||
}
|
||||
fn get_mut(&mut self, addr: u16) -> Option<&mut u8> {
|
||||
if !self.attr.w {
|
||||
return None;
|
||||
}
|
||||
self.mem.get_mut(addr as usize)
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
//! Stores and displays the Chip-8's screen memory
|
||||
|
||||
#![allow(unused_imports)]
|
||||
|
||||
use crate::{bus::BusConnectible, dump::Dumpable, mem::Mem};
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Screen {
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
pub fn new(width: usize, height: usize) -> Screen {
|
||||
Screen { width, height }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Screen {
|
||||
fn default() -> Self {
|
||||
Screen {
|
||||
width: 64,
|
||||
height: 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user