Chirp: Bus Schism: Split into Mem (internal) and Screen (external)

This commit is contained in:
John 2023-04-29 23:32:14 -05:00
parent f4d7e514bc
commit 96b6038bbe
18 changed files with 356 additions and 347 deletions

View File

@ -1,7 +1,7 @@
//! The emulator's state, including the screen data //! The emulator's state, including the screen data
use super::{error, BACKGROUND, FOREGROUND}; use super::{error, BACKGROUND, FOREGROUND};
use chirp::{bus, Bus, Screen, CPU}; use chirp::{Grab, Screen, CPU};
use pixels::Pixels; use pixels::Pixels;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use winit::event::VirtualKeyCode; use winit::event::VirtualKeyCode;
@ -10,7 +10,7 @@ use winit_input_helper::WinitInputHelper;
/// The state of the application /// The state of the application
#[derive(Debug)] #[derive(Debug)]
pub struct Emulator { pub struct Emulator {
screen: Bus, screen: Screen,
cpu: CPU, cpu: CPU,
pub ipf: usize, pub ipf: usize,
pub rom: PathBuf, pub rom: PathBuf,
@ -23,9 +23,7 @@ impl Emulator {
/// # Panics /// # Panics
/// Panics if the provided ROM does not exist /// Panics if the provided ROM does not exist
pub fn new(ipf: usize, rom: impl AsRef<Path>) -> Self { pub fn new(ipf: usize, rom: impl AsRef<Path>) -> Self {
let screen = bus! { let screen = Screen::default();
Screen [0x000..0x100],
};
let mut cpu = CPU::default(); let mut cpu = CPU::default();
cpu.load_program(&rom).expect("Loaded file MUST exist."); cpu.load_program(&rom).expect("Loaded file MUST exist.");
Self { Self {
@ -43,7 +41,7 @@ impl Emulator {
} }
/// Rasterizes the screen into a [Pixels] buffer /// Rasterizes the screen into a [Pixels] buffer
pub fn draw(&mut self, pixels: &mut Pixels) -> Result<(), error::Error> { pub fn draw(&mut self, pixels: &mut Pixels) -> Result<(), error::Error> {
if let Some(screen) = self.screen.get_region(Screen) { if let Some(screen) = self.screen.grab(..) {
let len_log2 = screen.len().ilog2() / 2; let len_log2 = screen.len().ilog2() / 2;
#[allow(unused_variables)] #[allow(unused_variables)]
let (width, height) = (2u32.pow(len_log2 + 2), 2u32.pow(len_log2 + 1)); let (width, height) = (2u32.pow(len_log2 + 2), 2u32.pow(len_log2 + 1));
@ -102,7 +100,7 @@ impl Emulator {
} }
/// Prints the screen (using the highest resolution available printer) to stdout /// Prints the screen (using the highest resolution available printer) to stdout
pub fn print_screen(&self) -> Result<(), error::Error> { pub fn print_screen(&self) -> Result<(), error::Error> {
self.screen.print_screen()?; self.screen.print_screen();
Ok(()) Ok(())
} }
/// Dumps the raw screen bytes to a file named `{rom}_{cycle}.bin`, /// Dumps the raw screen bytes to a file named `{rom}_{cycle}.bin`,
@ -114,23 +112,9 @@ impl Emulator {
self.cpu.cycle() self.cpu.cycle()
)); ));
path.set_extension("bin"); path.set_extension("bin");
if std::fs::write( if std::fs::write(&path, self.screen.as_slice()).is_ok() {
&path,
self.screen
.get_region(Screen)
.expect("Region::Screen should exist"),
)
.is_ok()
{
eprintln!("Saved to {}", &path.display()); eprintln!("Saved to {}", &path.display());
} else if std::fs::write( } else if std::fs::write("screen_dump.bin", self.screen.as_slice()).is_ok() {
"screen_dump.bin",
self.screen
.get_region(Screen)
.expect("Region::Screen should exist"),
)
.is_ok()
{
eprintln!("Saved to screen_dump.bin"); eprintln!("Saved to screen_dump.bin");
} else { } else {
eprintln!("Failed to dump screen to file.") eprintln!("Failed to dump screen to file.")
@ -171,14 +155,14 @@ impl Emulator {
/// Soft-resets the CPU, keeping the program in memory /// Soft-resets the CPU, keeping the program in memory
pub fn soft_reset(&mut self) { pub fn soft_reset(&mut self) {
self.cpu.reset(); self.cpu.reset();
self.screen.clear_region(Screen); self.screen.clear();
eprintln!("Soft Reset"); eprintln!("Soft Reset");
} }
/// Creates a new CPU with the current CPU's flags /// Creates a new CPU with the current CPU's flags
pub fn hard_reset(&mut self) { pub fn hard_reset(&mut self) {
self.cpu.reset(); self.cpu.reset();
self.screen.clear_region(Screen); self.screen.clear();
// keep the flags // keep the flags
let flags = self.cpu.flags.clone(); let flags = self.cpu.flags.clone();
// instantiate a completely new CPU, and reload the ROM from disk // instantiate a completely new CPU, and reload the ROM from disk

View File

@ -8,7 +8,6 @@ mod gui;
use crate::emu::*; use crate::emu::*;
use crate::gui::*; use crate::gui::*;
use core::panic;
use pixels::{Pixels, SurfaceTexture}; use pixels::{Pixels, SurfaceTexture};
use std::result::Result; use std::result::Result;
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;

View File

@ -12,7 +12,6 @@ use chirp::error::Error::BreakpointHit;
use chirp::{error::Result, *}; use chirp::{error::Result, *};
use gumdrop::*; use gumdrop::*;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use std::fs::read;
use std::{ use std::{
path::PathBuf, path::PathBuf,
time::{Duration, Instant}, time::{Duration, Instant},
@ -108,6 +107,11 @@ struct Arguments {
pub frame_rate: u64, pub frame_rate: u64,
} }
#[derive(Debug)]
pub struct Chip8 {
pub cpu: CPU,
pub screen: Screen,
}
#[derive(Debug)] #[derive(Debug)]
struct State { struct State {
pub speed: usize, pub speed: usize,
@ -127,16 +131,8 @@ impl State {
rate: options.frame_rate, rate: options.frame_rate,
perf: options.perf, perf: options.perf,
ch8: Chip8 { ch8: Chip8 {
bus: bus! {
// Load the charset into ROM
Charset [0x0050..0x00A0] = include_bytes!("../../mem/charset.bin"),
// Load the ROM file into RAM
Program [0x0200..0x1000] = &read(&options.file)?,
// Create a screen
Screen [0x1000..0x1100],
},
cpu: CPU::new( cpu: CPU::new(
&options.file, Some(&options.file),
0x50, 0x50,
0x200, 0x200,
Dis::default(), Dis::default(),
@ -148,6 +144,7 @@ impl State {
..Default::default() ..Default::default()
}, },
)?, )?,
screen: Screen::default(),
}, },
ui: UIBuilder::new(128, 64, &options.file).build()?, ui: UIBuilder::new(128, 64, &options.file).build()?,
ft: Instant::now(), ft: Instant::now(),
@ -158,7 +155,7 @@ impl State {
state.ch8.cpu.flags.quirks.draw_wait ^= options.drawsync; state.ch8.cpu.flags.quirks.draw_wait ^= options.drawsync;
state.ch8.cpu.flags.quirks.shift ^= options.shift; state.ch8.cpu.flags.quirks.shift ^= options.shift;
state.ch8.cpu.flags.quirks.stupid_jumps ^= options.jumping; state.ch8.cpu.flags.quirks.stupid_jumps ^= options.jumping;
state.ch8.bus.write(0x1feu16, options.data); state.ch8.screen.write(0x1feu16, options.data);
Ok(state) Ok(state)
} }
fn keys(&mut self) -> Result<bool> { fn keys(&mut self) -> Result<bool> {
@ -173,7 +170,7 @@ impl State {
match self.step { match self.step {
Some(ticks) => { Some(ticks) => {
let time = Instant::now(); let time = Instant::now();
self.ch8.cpu.multistep(&mut self.ch8.bus, ticks)?; self.ch8.cpu.multistep(&mut self.ch8.screen, ticks)?;
if self.perf { if self.perf {
let time = time.elapsed(); let time = time.elapsed();
let nspt = time.as_secs_f64() / ticks as f64; let nspt = time.as_secs_f64() / ticks as f64;
@ -186,7 +183,7 @@ impl State {
} }
} }
None => { None => {
self.ch8.cpu.multistep(&mut self.ch8.bus, rate)?; self.ch8.cpu.multistep(&mut self.ch8.screen, rate)?;
} }
} }
} }
@ -195,7 +192,7 @@ impl State {
fn wait_for_next_frame(&mut self) { fn wait_for_next_frame(&mut self) {
let rate = Duration::from_nanos(1_000_000_000 / self.rate + 1); let rate = Duration::from_nanos(1_000_000_000 / self.rate + 1);
std::thread::sleep(rate.saturating_sub(self.ft.elapsed())); std::thread::sleep(rate.saturating_sub(self.ft.elapsed()));
self.ft = self.ft + rate; self.ft += rate;
} }
} }

View File

@ -1,6 +1,8 @@
//! Tests for chirp-minifb //! Tests for chirp-minifb
#![allow(clippy::redundant_clone)]
use super::ui::*; use super::ui::*;
use super::Chip8;
use chirp::*; use chirp::*;
use std::{collections::hash_map::DefaultHasher, hash::Hash}; use std::{collections::hash_map::DefaultHasher, hash::Hash};
@ -29,7 +31,7 @@ mod ui {
fn new_chip8() -> Chip8 { fn new_chip8() -> Chip8 {
Chip8 { Chip8 {
cpu: CPU::default(), cpu: CPU::default(),
bus: bus! {}, screen: Screen::default(),
} }
} }
#[test] #[test]

View File

@ -4,18 +4,14 @@
//! Platform-specific IO/UI code, and some debug functionality. //! Platform-specific IO/UI code, and some debug functionality.
//! TODO: Destroy this all. //! TODO: Destroy this all.
use super::Chip8;
use chirp::{error::Result, screen::Screen};
use minifb::*;
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Instant, time::Instant,
}; };
use chirp::{
cpu::bus::{Bus, Region},
error::Result,
Chip8,
};
use minifb::*;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct UIBuilder { pub struct UIBuilder {
pub width: usize, pub width: usize,
@ -105,27 +101,25 @@ impl FrameBuffer {
format: Default::default(), format: Default::default(),
} }
} }
pub fn render(&mut self, window: &mut Window, bus: &Bus) -> Result<()> { pub fn render(&mut self, window: &mut Window, screen: &Screen) -> Result<()> {
if let Some(screen) = bus.get_region(Region::Screen) { // Resizing the buffer does not unmap memory.
// Resizing the buffer does not unmap memory. // After the first use of high-res mode, this is pretty cheap
// After the first use of high-res mode, this is pretty cheap (self.width, self.height) = match screen.len() {
(self.width, self.height) = match screen.len() { 256 => (64, 32),
256 => (64, 32), 1024 => (128, 64),
1024 => (128, 64), _ => {
_ => { unimplemented!("Screen must be 64*32 or 128*64");
unimplemented!("Screen must be 64*32 or 128*64"); }
} };
}; self.buffer.resize(self.width * self.height, 0);
self.buffer.resize(self.width * self.height, 0); for (idx, byte) in screen.as_slice().iter().enumerate() {
for (idx, byte) in screen.iter().enumerate() { for bit in 0..8 {
for bit in 0..8 { self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 {
self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 { self.format.fg
self.format.fg } else {
} else { self.format.bg
self.format.bg // .wrapping_add(0x001104 * (idx / self.width) as u32)
// .wrapping_add(0x001104 * (idx / self.width) as u32) // .wrapping_add(0x141000 * (idx & 3) as u32)
// .wrapping_add(0x141000 * (idx & 3) as u32)
}
} }
} }
} }
@ -164,7 +158,7 @@ impl UI {
} }
self.time = Instant::now(); self.time = Instant::now();
// update framebuffer // update framebuffer
self.fb.render(&mut self.window, &ch8.bus)?; self.fb.render(&mut self.window, &ch8.screen)?;
Ok(true) Ok(true)
} }
@ -182,7 +176,6 @@ impl UI {
.into_iter() .into_iter()
.filter(|key| !self.window.get_keys().contains(key)) .filter(|key| !self.window.get_keys().contains(key))
}; };
use crate::ui::Region::*;
for key in get_keys_released() { for key in get_keys_released() {
if let Some(key) = identify_key(key) { if let Some(key) = identify_key(key) {
ch8.cpu.release(key)?; ch8.cpu.release(key)?;
@ -193,7 +186,7 @@ impl UI {
use Key::*; use Key::*;
match key { match key {
F1 | Comma => ch8.cpu.dump(), F1 | Comma => ch8.cpu.dump(),
F2 | Period => ch8.bus.print_screen()?, F2 | Period => ch8.screen.print_screen(),
F3 => { F3 => {
debug_dump_screen(ch8, &self.rom).expect("Unable to write debug screen dump"); debug_dump_screen(ch8, &self.rom).expect("Unable to write debug screen dump");
} }
@ -217,7 +210,7 @@ impl UI {
}), }),
F6 | Enter => { F6 | Enter => {
eprintln!("Step"); eprintln!("Step");
ch8.cpu.singlestep(&mut ch8.bus)?; ch8.cpu.singlestep(&mut ch8.screen)?;
} }
F7 => { F7 => {
eprintln!("Set breakpoint {:03x}.", ch8.cpu.pc()); eprintln!("Set breakpoint {:03x}.", ch8.cpu.pc());
@ -230,7 +223,7 @@ impl UI {
F9 | Delete => { F9 | Delete => {
eprintln!("Soft reset state.cpu {:03x}", ch8.cpu.pc()); eprintln!("Soft reset state.cpu {:03x}", ch8.cpu.pc());
ch8.cpu.soft_reset(); ch8.cpu.soft_reset();
ch8.bus.clear_region(Screen); ch8.screen.clear();
} }
Escape => return Ok(false), Escape => return Ok(false),
key => { key => {
@ -274,19 +267,9 @@ pub fn debug_dump_screen(ch8: &Chip8, rom: &Path) -> Result<()> {
ch8.cpu.cycle() ch8.cpu.cycle()
)); ));
path.set_extension("bin"); path.set_extension("bin");
if let Ok(_) = std::fs::write( if std::fs::write(&path, ch8.screen.as_slice()).is_ok() {
&path,
ch8.bus
.get_region(Region::Screen)
.expect("Region::Screen should exist"),
) {
eprintln!("Saved to {}", &path.display()); eprintln!("Saved to {}", &path.display());
} else if let Ok(_) = std::fs::write( } else if std::fs::write("screen_dump.bin", ch8.screen.as_slice()).is_ok() {
"screen_dump.bin",
ch8.bus
.get_region(Region::Screen)
.expect("Region::Screen should exist"),
) {
eprintln!("Saved to screen_dump.bin"); eprintln!("Saved to screen_dump.bin");
} else { } else {
eprintln!("Failed to dump screen to file.") eprintln!("Failed to dump screen to file.")

View File

@ -4,7 +4,7 @@ use std::{env::args, fs::read};
fn main() -> Result<()> { fn main() -> Result<()> {
for screen in args().skip(1).inspect(|screen| println!("{screen}")) { for screen in args().skip(1).inspect(|screen| println!("{screen}")) {
let screen = read(screen)?; let screen = read(screen)?;
bus! {Screen [0..screen.len()] = &screen}.print_screen()?; Screen::from(screen).print_screen();
} }
Ok(()) Ok(())
} }

View File

@ -7,26 +7,27 @@
mod tests; mod tests;
mod behavior; mod behavior;
pub mod bus;
pub mod flags; pub mod flags;
pub mod instruction; pub mod instruction;
#[macro_use]
pub mod mem;
pub mod mode; pub mod mode;
pub mod quirks; pub mod quirks;
use self::{ use self::{
bus::{Bus, Region::*},
flags::Flags, flags::Flags,
instruction::{ instruction::{
disassembler::{Dis, Disassembler}, disassembler::{Dis, Disassembler},
Insn, Insn,
}, },
mem::{Mem, Region::*},
mode::Mode, mode::Mode,
quirks::Quirks, quirks::Quirks,
}; };
use crate::{ use crate::{
bus,
error::{Error, Result}, error::{Error, Result},
traits::auto_cast::{AutoCast, Grab}, screen::Screen,
traits::{AutoCast, Grab},
}; };
use imperative_rs::InstructionSet; use imperative_rs::InstructionSet;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
@ -44,7 +45,7 @@ pub struct CPU {
/// chip-8. Includes [Quirks], target IPF, etc. /// chip-8. Includes [Quirks], target IPF, etc.
pub flags: Flags, pub flags: Flags,
// memory map info // memory map info
mem: Bus, mem: Mem,
font: Adr, font: Adr,
// memory // memory
stack: Vec<Adr>, stack: Vec<Adr>,
@ -90,7 +91,7 @@ impl CPU {
flags: Flags, flags: Flags,
) -> Result<Self> { ) -> Result<Self> {
const CHARSET: &[u8] = include_bytes!("mem/charset.bin"); const CHARSET: &[u8] = include_bytes!("mem/charset.bin");
let mem = bus! { let mem = mem! {
Charset [font as usize..font as usize + CHARSET.len()] = CHARSET, Charset [font as usize..font as usize + CHARSET.len()] = CHARSET,
Program [pc as usize..0x1000], Program [pc as usize..0x1000],
}; };
@ -133,7 +134,7 @@ impl CPU {
} }
/// Grabs a reference to the [CPU]'s memory /// Grabs a reference to the [CPU]'s memory
pub fn introspect(&mut self) -> &Bus { pub fn introspect(&mut self) -> &Mem {
&self.mem &self.mem
} }
@ -398,7 +399,7 @@ impl CPU {
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let mut cpu = CPU::default(); /// let mut cpu = CPU::default();
/// let mut screen = bus!{ /// let mut screen = mem!{
/// Screen [0x000..0x100], /// Screen [0x000..0x100],
/// }; /// };
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]); /// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
@ -406,7 +407,7 @@ impl CPU {
/// assert_eq!(0x202, cpu.pc()); /// assert_eq!(0x202, cpu.pc());
/// assert_eq!(1, cpu.cycle()); /// assert_eq!(1, cpu.cycle());
/// ``` /// ```
pub fn singlestep(&mut self, bus: &mut Bus) -> Result<&mut Self> { pub fn singlestep(&mut self, bus: &mut Screen) -> Result<&mut Self> {
self.flags.pause = false; self.flags.pause = false;
self.tick(bus)?; self.tick(bus)?;
self.flags.draw_wait = false; self.flags.draw_wait = false;
@ -421,7 +422,7 @@ impl CPU {
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let mut cpu = CPU::default(); /// let mut cpu = CPU::default();
/// let mut screen = bus!{ /// let mut screen = mem!{
/// Screen [0x000..0x100], /// Screen [0x000..0x100],
/// }; /// };
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]); /// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
@ -430,7 +431,7 @@ impl CPU {
/// assert_eq!(0x202, cpu.pc()); /// assert_eq!(0x202, cpu.pc());
/// assert_eq!(0x20, cpu.cycle()); /// assert_eq!(0x20, cpu.cycle());
/// ``` /// ```
pub fn multistep(&mut self, screen: &mut Bus, steps: usize) -> Result<&mut Self> { pub fn multistep(&mut self, screen: &mut Screen, steps: usize) -> Result<&mut Self> {
//let speed = 1.0 / steps as f64; //let speed = 1.0 / steps as f64;
for _ in 0..steps { for _ in 0..steps {
self.tick(screen)?; self.tick(screen)?;
@ -447,22 +448,21 @@ impl CPU {
/// This result contains information about the breakpoint, but can be safely ignored. /// This result contains information about the breakpoint, but can be safely ignored.
/// ///
/// Returns [Error::UnimplementedInstruction] if the instruction at `pc` is unimplemented. /// Returns [Error::UnimplementedInstruction] if the instruction at `pc` is unimplemented.
///
/// # Examples /// # Examples
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let mut cpu = CPU::default(); /// let mut cpu = CPU::default();
/// let mut bus = bus!{ /// let mut bus = mem!{
/// Program [0x0200..0x0f00] = &[ /// Screen [0x000..0x100],
/// 0x00, 0xe0, // cls
/// 0x22, 0x02, // jump 0x202 (pc)
/// ],
/// Screen [0x0f00..0x1000],
/// }; /// };
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
/// cpu.tick(&mut bus) /// cpu.tick(&mut bus)
/// .expect("0x00e0 (cls) should be a valid opcode."); /// .expect("0x00e0 (cls) should be a valid opcode.");
/// assert_eq!(0x202, cpu.pc()); /// assert_eq!(0x202, cpu.pc());
/// assert_eq!(1, cpu.cycle()); /// assert_eq!(1, cpu.cycle());
/// ``` /// ```
/// Returns [Error::UnimplementedInstruction] if the instruction is not implemented. /// Returns [Error::UnimplementedInstruction] if the instruction is not implemented.
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
@ -470,7 +470,7 @@ impl CPU {
/// let mut cpu = CPU::default(); /// let mut cpu = CPU::default();
/// # cpu.flags.debug = true; // enable live disassembly /// # cpu.flags.debug = true; // enable live disassembly
/// # cpu.flags.monotonic = true; // enable monotonic/test timing /// # cpu.flags.monotonic = true; // enable monotonic/test timing
/// let mut bus = bus!{ /// let mut bus = mem!{
/// Screen [0x0f00..0x1000], /// Screen [0x0f00..0x1000],
/// }; /// };
/// cpu.load_program_bytes(&[ /// cpu.load_program_bytes(&[
@ -480,7 +480,7 @@ impl CPU {
/// dbg!(cpu.tick(&mut bus)) /// dbg!(cpu.tick(&mut bus))
/// .expect_err("Should return Error::InvalidInstruction { 0xffff }"); /// .expect_err("Should return Error::InvalidInstruction { 0xffff }");
/// ``` /// ```
pub fn tick(&mut self, screen: &mut Bus) -> Result<&mut Self> { pub fn tick(&mut self, screen: &mut Screen) -> Result<&mut Self> {
// Do nothing if paused // Do nothing if paused
if self.flags.is_paused() { if self.flags.is_paused() {
// always tick in test mode // always tick in test mode
@ -608,7 +608,7 @@ impl Default for CPU {
fn default() -> Self { fn default() -> Self {
CPU { CPU {
stack: vec![], stack: vec![],
mem: bus! { mem: mem! {
Charset [0x0050..0x00a0] = include_bytes!("mem/charset.bin"), Charset [0x0050..0x00a0] = include_bytes!("mem/charset.bin"),
Program [0x0200..0x1000], Program [0x0200..0x1000],
}, },

View File

@ -3,14 +3,15 @@
//! Contains implementations for each Chip-8 [Insn] //! Contains implementations for each Chip-8 [Insn]
use super::{bus::Region, *}; use super::*;
use crate::traits::Grab;
use rand::random; use rand::random;
impl CPU { impl CPU {
/// Executes a single [Insn] /// Executes a single [Insn]
#[rustfmt::skip] #[rustfmt::skip]
#[inline(always)] #[inline(always)]
pub(super) fn execute(&mut self, screen: &mut Bus, instruction: Insn) { pub(super) fn execute(&mut self, screen: &mut Screen, instruction: Insn) {
match instruction { match instruction {
// Core Chip-8 instructions // Core Chip-8 instructions
Insn::cls => self.clear_screen(screen), Insn::cls => self.clear_screen(screen),
@ -73,8 +74,8 @@ impl CPU {
/// |`00e0`| Clears the screen memory to 0 /// |`00e0`| Clears the screen memory to 0
/// Corresponds to [Insn::cls] /// Corresponds to [Insn::cls]
#[inline(always)] #[inline(always)]
pub(super) fn clear_screen(&mut self, bus: &mut Bus) { pub(super) fn clear_screen(&mut self, screen: &mut Screen) {
bus.clear_region(Region::Screen); screen.clear()
} }
/// |`00ee`| Returns from subroutine /// |`00ee`| Returns from subroutine
/// Corresponds to [Insn::ret] /// Corresponds to [Insn::ret]
@ -99,7 +100,7 @@ impl CPU {
/// ///
/// Corresponds to [Insn::scd] /// Corresponds to [Insn::scd]
#[inline(always)] #[inline(always)]
pub(super) fn scroll_down(&mut self, n: Nib, screen: &mut Bus) { pub(super) fn scroll_down(&mut self, n: Nib, screen: &mut Screen) {
match self.flags.draw_mode { match self.flags.draw_mode {
true => { true => {
// Get a line from the bus // Get a line from the bus
@ -148,18 +149,18 @@ impl CPU {
/// Initialize lores mode /// Initialize lores mode
/// ///
/// Corresponds to [Insn::lores] /// Corresponds to [Insn::lores]
pub(super) fn init_lores(&mut self, screen: &mut Bus) { pub(super) fn init_lores(&mut self, screen: &mut Screen) {
self.flags.draw_mode = false; self.flags.draw_mode = false;
screen.set_region(Region::Screen, 0..256); screen.with_size(256);
self.clear_screen(screen); self.clear_screen(screen);
} }
/// # |`00ff`| /// # |`00ff`|
/// Initialize hires mode /// Initialize hires mode
/// ///
/// Corresponds to [Insn::hires] /// Corresponds to [Insn::hires]
pub(super) fn init_hires(&mut self, screen: &mut Bus) { pub(super) fn init_hires(&mut self, screen: &mut Screen) {
self.flags.draw_mode = true; self.flags.draw_mode = true;
screen.set_region(Region::Screen, 0..1024); screen.with_size(1024);
self.clear_screen(screen); self.clear_screen(screen);
} }
} }
@ -439,7 +440,7 @@ impl CPU {
/// # Quirk /// # Quirk
/// On the original chip-8 interpreter, this will wait for a VBI /// On the original chip-8 interpreter, this will wait for a VBI
#[inline(always)] #[inline(always)]
pub(super) fn draw(&mut self, x: Reg, y: Reg, n: Nib, screen: &mut Bus) { pub(super) fn draw(&mut self, x: Reg, y: Reg, n: Nib, screen: &mut Screen) {
if !self.flags.quirks.draw_wait { if !self.flags.quirks.draw_wait {
self.flags.draw_wait = true; self.flags.draw_wait = true;
} }
@ -453,12 +454,20 @@ impl CPU {
/// |`Dxyn`| Chip-8: Draws n-byte sprite to the screen at coordinates (vX, vY) /// |`Dxyn`| Chip-8: Draws n-byte sprite to the screen at coordinates (vX, vY)
#[inline(always)] #[inline(always)]
pub(super) fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, scr: &mut Bus) { pub(super) fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, scr: &mut Screen) {
self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, scr); self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, scr);
} }
#[inline(always)] #[inline(always)]
pub(super) fn draw_sprite(&mut self, x: u16, y: u16, n: Nib, w: u16, h: u16, screen: &mut Bus) { pub(super) fn draw_sprite(
&mut self,
x: u16,
y: u16,
n: Nib,
w: u16,
h: u16,
screen: &mut Screen,
) {
let w_bytes = w / 8; let w_bytes = w / 8;
self.v[0xf] = 0; self.v[0xf] = 0;
if let Some(sprite) = self.mem.grab(self.i as usize..(self.i + n as u16) as usize) { if let Some(sprite) = self.mem.grab(self.i as usize..(self.i + n as u16) as usize) {
@ -492,7 +501,7 @@ impl CPU {
impl CPU { impl CPU {
/// |`Dxyn`| Super-Chip extension high-resolution graphics mode /// |`Dxyn`| Super-Chip extension high-resolution graphics mode
#[inline(always)] #[inline(always)]
pub(super) fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, screen: &mut Bus) { pub(super) fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, screen: &mut Screen) {
if !self.flags.quirks.draw_wait { if !self.flags.quirks.draw_wait {
self.flags.draw_wait = true; self.flags.draw_wait = true;
} }
@ -508,7 +517,7 @@ impl CPU {
} }
/// Draws a 16x16 Super Chip sprite /// Draws a 16x16 Super Chip sprite
#[inline(always)] #[inline(always)]
pub(super) fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, screen: &mut Bus) { pub(super) fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, screen: &mut Screen) {
self.v[0xf] = 0; self.v[0xf] = 0;
let w_bytes = w / 8; let w_bytes = w / 8;
if let Some(sprite) = self.mem.grab(self.i as usize..(self.i + 32) as usize) { if let Some(sprite) = self.mem.grab(self.i as usize..(self.i + 32) as usize) {

View File

@ -1,45 +1,43 @@
// (c) 2023 John A. Breaux // (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details) // This code is licensed under MIT license (see LICENSE for details)
//! The Bus connects the CPU to Memory //! The Mem represents the CPU's memory
//! //!
//! This is more of a memory management unit + some utils for reading/writing //! Contains some handy utils for reading and writing
use crate::error::{Error::MissingRegion, Result}; use crate::{error::Result, traits::Grab};
use std::{ use std::{
fmt::{Debug, Display, Formatter}, fmt::{Debug, Display, Formatter},
ops::Range, ops::Range,
slice::SliceIndex, slice::SliceIndex,
}; };
/// Creates a new bus, growing the backing memory as needed /// Creates a new [Mem], growing as needed
/// # Examples /// # Examples
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let mut bus = bus! { /// let mut mem = mem! {
/// Charset [0x0000..0x0800] = b"ABCDEF", /// Charset [0x0000..0x0800] = b"ABCDEF",
/// Program [0x0800..0xf000] = include_bytes!("bus.rs"), /// Program [0x0800..0xf000] = include_bytes!("mem.rs"),
/// }; /// };
/// ``` /// ```
#[macro_export] #[macro_export]
macro_rules! bus { macro_rules! mem {
($($name:path $(:)? [$range:expr] $(= $data:expr)?) ,* $(,)?) => { ($($name:path $(:)? [$range:expr] $(= $data:expr)?) ,* $(,)?) => {
$crate::cpu::bus::Bus::default()$(.add_region_owned($name, $range)$(.load_region_owned($name, $data))?)* $crate::cpu::mem::Mem::default()$(.add_region_owned($name, $range)$(.load_region_owned($name, $data))?)*
}; };
} }
pub use crate::traits::auto_cast::{AutoCast, Grab};
// Traits Read and Write are here purely to make implementing other things more bearable // Traits Read and Write are here purely to make implementing other things more bearable
impl Grab<u8> for Bus { impl Grab for Mem {
/// Gets a slice of [Bus] memory /// Gets a slice of [Mem] memory
/// # Examples /// # Examples
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let bus = Bus::new() /// let mem = Mem::new()
/// .add_region_owned(Program, 0..10); /// .add_region_owned(Program, 0..10);
/// assert!([0;10].as_slice() == bus.get(0..10).unwrap()); /// assert!([0;10].as_slice() == mem.grab(0..10).unwrap());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
@ -51,14 +49,14 @@ impl Grab<u8> for Bus {
self.memory.get(index) self.memory.get(index)
} }
/// Gets a mutable slice of [Bus] memory /// Gets a mutable slice of [Mem] memory
/// # Examples /// # Examples
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let mut bus = Bus::new() /// let mut mem = Mem::new()
/// .add_region_owned(Program, 0..10); /// .add_region_owned(Program, 0..10);
/// assert!([0;10].as_slice() == bus.get_mut(0..10).unwrap()); /// assert!([0;10].as_slice() == mem.grab_mut(0..10).unwrap());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
@ -80,8 +78,6 @@ pub enum Region {
Charset, Charset,
/// Program memory /// Program memory
Program, Program,
/// Screen buffer
Screen,
#[doc(hidden)] #[doc(hidden)]
/// Total number of named regions /// Total number of named regions
Count, Count,
@ -95,7 +91,6 @@ impl Display for Region {
match self { match self {
Region::Charset => "Charset", Region::Charset => "Charset",
Region::Program => "Program", Region::Program => "Program",
Region::Screen => "Screen",
_ => "", _ => "",
} }
) )
@ -105,35 +100,35 @@ impl Display for Region {
/// Stores memory in a series of named regions with ranges /// Stores memory in a series of named regions with ranges
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Bus { pub struct Mem {
memory: Vec<u8>, memory: Vec<u8>,
region: [Option<Range<usize>>; Region::Count as usize], region: [Option<Range<usize>>; Region::Count as usize],
} }
impl Bus { impl Mem {
// TODO: make bus::new() give a properly set up bus with a default memory map // TODO: make mem::new() give a properly set up mem with a default memory map
/// Constructs a new bus /// Constructs a new mem
/// # Examples /// # Examples
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let bus = Bus::new(); /// let mem = Mem::new();
/// assert!(bus.is_empty()); /// assert!(mem.is_empty());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
pub fn new() -> Self { pub fn new() -> Self {
Bus::default() Mem::default()
} }
/// Gets the length of the bus' backing memory /// Gets the length of the mem' backing memory
/// # Examples /// # Examples
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let bus = Bus::new() /// let mem = Mem::new()
/// .add_region_owned(Program, 0..1234); /// .add_region_owned(Program, 0..1234);
/// assert_eq!(1234, bus.len()); /// assert_eq!(1234, mem.len());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
@ -146,47 +141,48 @@ impl Bus {
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let bus = Bus::new(); /// let mem = Mem::new();
/// assert!(bus.is_empty()); /// assert!(mem.is_empty());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.memory.is_empty() self.memory.is_empty()
} }
/// Grows the Bus backing memory to at least size bytes, but does not truncate
/// Grows the Mem backing memory to at least size bytes, but does not truncate
/// # Examples /// # Examples
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let mut bus = Bus::new(); /// let mut mem = Mem::new();
/// bus.with_size(1234); /// mem.with_size(1234);
/// assert_eq!(1234, bus.len()); /// assert_eq!(1234, mem.len());
/// bus.with_size(0); /// mem.with_size(0);
/// assert_eq!(1234, bus.len()); /// assert_eq!(1234, mem.len());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
pub fn with_size(&mut self, size: usize) { fn with_size(&mut self, size: usize) {
if self.len() < size { if self.len() < size {
self.memory.resize(size, 0); self.memory.resize(size, 0);
} }
} }
/// Adds a new names range ([Region]) to an owned [Bus] /// Adds a new names range ([Region]) to an owned [Mem]
pub fn add_region_owned(mut self, name: Region, range: Range<usize>) -> Self { pub fn add_region_owned(mut self, name: Region, range: Range<usize>) -> Self {
self.add_region(name, range); self.add_region(name, range);
self self
} }
/// Adds a new named range ([Region]) to a [Bus] /// Adds a new named range ([Region]) to a [Mem]
/// # Examples /// # Examples
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let mut bus = Bus::new(); /// let mut mem = Mem::new();
/// bus.add_region(Program, 0..1234); /// mem.add_region(Program, 0..1234);
/// assert_eq!(1234, bus.len()); /// assert_eq!(1234, mem.len());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
@ -203,9 +199,9 @@ impl Bus {
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let mut bus = Bus::new().add_region_owned(Program, 0..1234); /// let mut mem = Mem::new().add_region_owned(Program, 0..1234);
/// bus.set_region(Program, 1234..2345); /// mem.set_region(Program, 1234..2345);
/// assert_eq!(2345, bus.len()); /// assert_eq!(2345, mem.len());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
@ -217,7 +213,7 @@ impl Bus {
self self
} }
/// Loads data into a [Region] on an *owned* [Bus], for use during initialization /// Loads data into a [Region] on an *owned* [Mem], for use during initialization
pub fn load_region_owned(mut self, name: Region, data: &[u8]) -> Self { pub fn load_region_owned(mut self, name: Region, data: &[u8]) -> Self {
self.load_region(name, data).ok(); self.load_region(name, data).ok();
self self
@ -228,7 +224,7 @@ impl Bus {
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let bus = Bus::new() /// let mem = Mem::new()
/// .add_region_owned(Program, 0..1234) /// .add_region_owned(Program, 0..1234)
/// .load_region(Program, b"Hello, world!")?; /// .load_region(Program, b"Hello, world!")?;
///# // TODO: Test if region actually contains "Hello, world!" ///# // TODO: Test if region actually contains "Hello, world!"
@ -248,7 +244,7 @@ impl Bus {
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let bus = Bus::new() /// let mem = Mem::new()
/// .add_region_owned(Program, 0..1234) /// .add_region_owned(Program, 0..1234)
/// .clear_region(Program); /// .clear_region(Program);
///# // TODO: test if region actually clear ///# // TODO: test if region actually clear
@ -259,7 +255,7 @@ impl Bus {
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let bus = Bus::new() /// let mem = Mem::new()
/// .add_region_owned(Program, 0..1234) /// .add_region_owned(Program, 0..1234)
/// .clear_region(Screen); /// .clear_region(Screen);
///# // TODO: test if region actually clear ///# // TODO: test if region actually clear
@ -278,9 +274,9 @@ impl Bus {
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let bus = Bus::new() /// let mem = Mem::new()
/// .add_region_owned(Program, 0..10); /// .add_region_owned(Program, 0..10);
/// assert!([0;10].as_slice() == bus.get_region(Program).unwrap()); /// assert!([0;10].as_slice() == mem.get_region(Program).unwrap());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
@ -295,9 +291,9 @@ impl Bus {
/// ```rust /// ```rust
///# use chirp::*; ///# use chirp::*;
///# fn main() -> Result<()> { ///# fn main() -> Result<()> {
/// let mut bus = Bus::new() /// let mut mem = Mem::new()
/// .add_region_owned(Program, 0..10); /// .add_region_owned(Program, 0..10);
/// assert!([0;10].as_slice() == bus.get_region_mut(Program).unwrap()); /// assert!([0;10].as_slice() == mem.get_region_mut(Program).unwrap());
///# Ok(()) ///# Ok(())
///# } ///# }
/// ``` /// ```
@ -306,79 +302,10 @@ impl Bus {
debug_assert!(self.region.get(name as usize).is_some()); debug_assert!(self.region.get(name as usize).is_some());
self.grab_mut(self.region.get(name as usize)?.clone()?) self.grab_mut(self.region.get(name as usize)?.clone()?)
} }
/// Prints the region of memory called `Screen` at 1bpp using box characters
/// # Examples
///
/// [Bus::print_screen] will print the screen
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new()
/// .add_region_owned(Screen, 0x000..0x100);
/// bus.print_screen()?;
///# Ok(())
///# }
/// ```
/// If there is no Screen region, it will return Err([MissingRegion])
/// ```rust,should_panic
///# use chirp::*;
///# fn main() -> Result<()> {
/// let mut bus = Bus::new()
/// .add_region_owned(Program, 0..10);
/// bus.print_screen()?;
///# Ok(())
///# }
/// ```
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 + 1) - 1);
// 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).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 % width as usize == 0 {
print!("{index:03x}|");
}
print!(
"{}",
format!("{byte:08b}").replace('0', " ").replace('1', "")
);
if index % width as usize == width as usize - 1 {
println!("|");
}
}
} else {
return Err(MissingRegion { region: REGION });
}
Ok(())
}
} }
#[cfg(target_feature = "rhexdump")] #[cfg(target_feature = "rhexdump")]
impl Display for Bus { impl Display for Mem {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
use rhexdump::Rhexdump; use rhexdump::Rhexdump;
let mut rhx = Rhexdump::default(); let mut rhx = Rhexdump::default();

View File

@ -13,15 +13,12 @@
//! Some of these tests run >16M times, which is very silly //! Some of these tests run >16M times, which is very silly
use super::*; use super::*;
use crate::{ use crate::Screen;
bus,
cpu::bus::{Bus, Region::*},
};
use rand::random; use rand::random;
mod decode; mod decode;
fn setup_environment() -> (CPU, Bus) { fn setup_environment() -> (CPU, Screen) {
let mut ch8 = ( let mut ch8 = (
CPU { CPU {
flags: Flags { flags: Flags {
@ -31,10 +28,7 @@ fn setup_environment() -> (CPU, Bus) {
}, },
..CPU::default() ..CPU::default()
}, },
bus! { Screen::default(),
// Create a screen
Screen [0x000..0x100] = include_bytes!("../../chip8Archive/roms/1dcell.ch8"),
},
); );
ch8.0 ch8.0
.load_program_bytes(include_bytes!("tests/roms/jumptest.ch8")) .load_program_bytes(include_bytes!("tests/roms/jumptest.ch8"))
@ -43,9 +37,7 @@ fn setup_environment() -> (CPU, Bus) {
} }
fn print_screen(bytes: &[u8]) { fn print_screen(bytes: &[u8]) {
bus! {Screen [0..0x100] = bytes} Screen::from(bytes).print_screen()
.print_screen()
.expect("Printing screen should not fail if Screen exists.")
} }
/// Unused instructions /// Unused instructions
@ -57,9 +49,9 @@ mod unimplemented {
$pub:vis test $name:ident { $($insn:literal),+$(,)? } $pub:vis test $name:ident { $($insn:literal),+$(,)? }
);+ $(;)?) => { );+ $(;)?) => {
$( $(#[$attr])* #[test] $pub fn $name () {$( $( $(#[$attr])* #[test] $pub fn $name () {$(
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.mem.write(0x200u16, $insn as u16); cpu.mem.write(0x200u16, $insn as u16);
cpu.tick(&mut bus) cpu.tick(&mut screen)
.expect_err(stringify!($insn is not an instruction)); .expect_err(stringify!($insn is not an instruction));
)*} )+ )*} )+
}; };
@ -95,10 +87,11 @@ mod sys {
/// 00e0: Clears the screen memory to 0 /// 00e0: Clears the screen memory to 0
#[test] #[test]
fn clear_screen() { fn clear_screen() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.clear_screen(&mut bus); cpu.clear_screen(&mut screen);
bus.get_region(Screen) screen
.expect("Expected screen, got None") .grab(..)
.unwrap()
.iter() .iter()
.for_each(|byte| assert_eq!(*byte, 0)); .for_each(|byte| assert_eq!(*byte, 0));
} }
@ -751,7 +744,7 @@ mod io {
#[test] #[test]
fn draw() { fn draw() {
for test in SCREEN_TESTS { for test in SCREEN_TESTS {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.flags.quirks = test.quirks; cpu.flags.quirks = test.quirks;
// Debug mode is 5x slower // Debug mode is 5x slower
cpu.flags.debug = false; cpu.flags.debug = false;
@ -759,18 +752,13 @@ mod io {
cpu.mem.load_region(Program, test.program).unwrap(); cpu.mem.load_region(Program, test.program).unwrap();
// Run the test program for the specified number of steps // Run the test program for the specified number of steps
while cpu.cycle() < test.steps { while cpu.cycle() < test.steps {
cpu.multistep(&mut bus, 10.min(test.steps - cpu.cycle())) cpu.multistep(&mut screen, 10.min(test.steps - cpu.cycle()))
.expect("Draw tests should not contain undefined instructions"); .expect("Draw tests should not contain undefined instructions");
} }
// Compare the screen to the reference screen buffer // Compare the screen to the reference screen buffer
bus.print_screen() screen.print_screen();
.expect("Printing screen should not fail if screen exists");
print_screen(test.screen); print_screen(test.screen);
assert_eq!( assert_eq!(screen.grab(..).unwrap(), test.screen);
bus.get_region(Screen)
.expect("Getting screen should not fail if screen exists"),
test.screen
);
} }
} }
} }
@ -1028,15 +1016,15 @@ mod io {
// Perform DMA store // Perform DMA store
cpu.i = addr as u16; cpu.i = addr as u16;
cpu.store_dma(len); cpu.store_dma(len);
// Check that bus grabbed the correct data // Check that screen grabbed the correct data
let bus = cpu let screen = cpu
.mem .mem
.grab_mut(addr..addr + DATA.len()) .grab_mut(addr..addr + DATA.len())
.expect("Getting a mutable slice at addr 0x0456 should not fail"); .expect("Getting a mutable slice at addr 0x0456 should not fail");
assert_eq!(bus[0..=len], DATA[0..=len]); assert_eq!(screen[0..=len], DATA[0..=len]);
assert_eq!(bus[len + 1..], [0; 16][len + 1..]); assert_eq!(screen[len + 1..], [0; 16][len + 1..]);
// clear // clear
bus.fill(0); screen.fill(0);
} }
} }
@ -1074,11 +1062,11 @@ mod behavior {
use std::time::Duration; use std::time::Duration;
#[test] #[test]
fn delay() { fn delay() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.flags.monotonic = false; cpu.flags.monotonic = false;
cpu.delay = 10; cpu.delay = 10;
for _ in 0..2 { for _ in 0..2 {
cpu.multistep(&mut bus, 8) cpu.multistep(&mut screen, 8)
.expect("Running valid instructions should always succeed"); .expect("Running valid instructions should always succeed");
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0)); std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
} }
@ -1087,11 +1075,11 @@ mod behavior {
} }
#[test] #[test]
fn sound() { fn sound() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.flags.monotonic = false; // disable monotonic timing cpu.flags.monotonic = false; // disable monotonic timing
cpu.sound = 10; cpu.sound = 10;
for _ in 0..2 { for _ in 0..2 {
cpu.multistep(&mut bus, 8) cpu.multistep(&mut screen, 8)
.expect("Running valid instructions should always succeed"); .expect("Running valid instructions should always succeed");
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0)); std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
} }
@ -1100,11 +1088,11 @@ mod behavior {
} }
#[test] #[test]
fn vbi_wait() { fn vbi_wait() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.flags.monotonic = false; // disable monotonic timing cpu.flags.monotonic = false; // disable monotonic timing
cpu.flags.draw_wait = true; cpu.flags.draw_wait = true;
for _ in 0..2 { for _ in 0..2 {
cpu.multistep(&mut bus, 8) cpu.multistep(&mut screen, 8)
.expect("Running valid instructions should always succeed"); .expect("Running valid instructions should always succeed");
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0)); std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
} }
@ -1118,9 +1106,9 @@ mod behavior {
#[test] #[test]
#[cfg_attr(feature = "unstable", no_coverage)] #[cfg_attr(feature = "unstable", no_coverage)]
fn hit_break() { fn hit_break() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.set_break(0x202); cpu.set_break(0x202);
match cpu.multistep(&mut bus, 10) { match cpu.multistep(&mut screen, 10) {
Err(crate::error::Error::BreakpointHit { addr, next }) => { Err(crate::error::Error::BreakpointHit { addr, next }) => {
assert_eq!(0x202, addr); // current address is 202 assert_eq!(0x202, addr); // current address is 202
assert_eq!(0x1204, next); // next insn is `jmp 204` assert_eq!(0x1204, next); // next insn is `jmp 204`
@ -1133,9 +1121,9 @@ mod behavior {
#[test] #[test]
#[cfg_attr(feature = "unstable", no_coverage)] #[cfg_attr(feature = "unstable", no_coverage)]
fn hit_break_singlestep() { fn hit_break_singlestep() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.set_break(0x202); cpu.set_break(0x202);
match cpu.singlestep(&mut bus) { match cpu.singlestep(&mut screen) {
Err(crate::error::Error::BreakpointHit { addr, next }) => { Err(crate::error::Error::BreakpointHit { addr, next }) => {
assert_eq!(0x202, addr); // current address is 202 assert_eq!(0x202, addr); // current address is 202
assert_eq!(0x1204, next); // next insn is `jmp 204` assert_eq!(0x1204, next); // next insn is `jmp 204`
@ -1150,10 +1138,10 @@ mod behavior {
#[test] #[test]
#[cfg_attr(feature = "unstable", no_coverage)] #[cfg_attr(feature = "unstable", no_coverage)]
fn invalid_pc() { fn invalid_pc() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
// The bus extends from 0x0..0x1000 // The screen extends from 0x0..0x1000
cpu.pc = 0x1001; cpu.pc = 0x1001;
match cpu.tick(&mut bus) { match cpu.tick(&mut screen) {
Err(Error::InvalidAddressRange { range }) => { Err(Error::InvalidAddressRange { range }) => {
eprintln!("InvalidAddressRange {{ {range:04x?} }}") eprintln!("InvalidAddressRange {{ {range:04x?} }}")
} }

View File

@ -9,17 +9,15 @@ const INDX: &[u8; 16] = b"\0\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d
/// runs one arbitrary operation on a brand new CPU /// runs one arbitrary operation on a brand new CPU
/// returns the CPU for inspection /// returns the CPU for inspection
fn run_single_op(op: &[u8]) -> CPU { fn run_single_op(op: &[u8]) -> CPU {
let (mut cpu, mut bus) = ( let (mut cpu, mut screen) = (
CPU::default(), CPU::default(),
bus! { Screen::default(),
Screen[0x0..0x1000],
},
); );
cpu.mem cpu.mem
.load_region(Program, op).unwrap(); .load_region(Program, op).unwrap();
cpu.v = *INDX; cpu.v = *INDX;
cpu.flags.quirks = Quirks::from(false); cpu.flags.quirks = Quirks::from(false);
cpu.tick(&mut bus).unwrap(); // will panic if unimplemented cpu.tick(&mut screen).unwrap(); // will panic if unimplemented
cpu cpu
} }

View File

@ -6,7 +6,7 @@
pub mod any_range; pub mod any_range;
use any_range::AnyRange; use any_range::AnyRange;
use crate::cpu::bus::Region; use crate::cpu::mem::Region;
use thiserror::Error; use thiserror::Error;
/// Result type, equivalent to [std::result::Result]<T, [enum@Error]> /// Result type, equivalent to [std::result::Result]<T, [enum@Error]>

View File

@ -10,11 +10,11 @@
pub mod cpu; pub mod cpu;
pub mod error; pub mod error;
pub mod screen;
pub mod traits; pub mod traits;
// Common imports for Chirp // Common imports for Chirp
pub use cpu::{ pub use cpu::{
bus::{Bus, Region::*},
flags::Flags, flags::Flags,
instruction::disassembler::{Dis, Disassembler}, instruction::disassembler::{Dis, Disassembler},
mode::Mode, mode::Mode,
@ -22,8 +22,6 @@ pub use cpu::{
CPU, CPU,
}; };
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use traits::auto_cast::{AutoCast, Grab};
/// Holds the state of a Chip-8 /// Holds the state of a Chip-8
#[derive(Clone, Debug, Default, PartialEq)] #[derive(Clone, Debug, Default, PartialEq)]
pub struct Chip8 { pub struct Chip8 {
@ -32,3 +30,5 @@ pub struct Chip8 {
/// Contains the memory of a chip-8 /// Contains the memory of a chip-8
pub bus: cpu::bus::Bus, pub bus: cpu::bus::Bus,
} }
pub use screen::Screen;
pub use traits::{AutoCast, FallibleAutoCast, Grab};

132
src/screen.rs Normal file
View File

@ -0,0 +1,132 @@
//! Contains the raw screen bytes, and an iterator over individual bits of those bytes.
use crate::traits::Grab;
/// Stores the screen bytes
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Screen {
/// The screen bytes
bytes: Vec<u8>,
}
impl<T: AsRef<[u8]>> From<T> for Screen {
fn from(value: T) -> Self {
Screen {
bytes: value.as_ref().into(),
}
}
}
impl Default for Screen {
fn default() -> Self {
Self::new(256)
}
}
impl Grab for Screen {
fn grab<I>(&self, index: I) -> Option<&<I as std::slice::SliceIndex<[u8]>>::Output>
where
I: std::slice::SliceIndex<[u8]>,
{
self.bytes.get(index)
}
fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as std::slice::SliceIndex<[u8]>>::Output>
where
I: std::slice::SliceIndex<[u8]>,
{
self.bytes.get_mut(index)
}
}
impl Screen {
/// Creates a new [Screen]
pub fn new(size: usize) -> Self {
Self {
bytes: vec![0; size],
}
}
/// Returns true if the screen has 0 elements
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
/// Gets the length of the [Screen]
pub fn len(&self) -> usize {
self.bytes.len()
}
/// Gets a slice of the whole [Screen]
pub fn as_slice(&self) -> &[u8] {
self.bytes.as_slice()
}
/// Clears the [Screen] to 0
pub fn clear(&mut self) {
self.bytes.fill(0);
}
/// Grows the [Screen] memory to at least size bytes, but does not truncate
/// # Examples
/// ```rust
///# use chirp::screen::Screen;
/// let mut screen = Screen::new(256);
/// assert_eq!(1234, screen.len());
/// screen.with_size(0);
/// assert_eq!(1234, screen.len());
/// ```
pub fn with_size(&mut self, size: usize) {
if self.len() < size {
self.bytes.resize(size, 0);
}
}
/// Prints the [Screen] using either [drawille] or Box Drawing Characters
/// # Examples
/// [Screen::print_screen] will print the screen
/// ```rust
///# use chirp::screen::Screen;
/// let screen = Screen::default();
/// screen.print_screen();
/// ```
pub fn print_screen(&self) {
let len_log2 = self.bytes.len().ilog2() / 2;
#[allow(unused_variables)]
let (width, height) = (2u32.pow(len_log2 - 1), 2u32.pow(len_log2 + 1) - 1);
// 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;
self.bytes
.iter()
.enumerate()
.flat_map(|(bytei, byte)| {
(0..8).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 self.bytes.iter().enumerate() {
if index % width as usize == 0 {
print!("{index:03x}|");
}
print!(
"{}",
format!("{byte:08b}").replace('0', " ").replace('1', "")
);
if index % width as usize == width as usize - 1 {
println!("|");
}
}
}
}

View File

@ -3,4 +3,5 @@
//! Traits useful for Chirp //! Traits useful for Chirp
pub mod auto_cast; mod auto_cast;
pub use auto_cast::{AutoCast, FallibleAutoCast, Grab};

View File

@ -3,26 +3,26 @@
//! Traits for automatically serializing and deserializing Rust primitive types. //! Traits for automatically serializing and deserializing Rust primitive types.
//! //!
//! Users of this module should impl [Get]`<u8>` for their type, which notably returns `&[u8]` and `&mut [u8]` //! Users of this module should impl [Grab]`<u8>` for their type, which notably returns `&[u8]` and `&mut [u8]`
#[allow(unused_imports)] #[allow(unused_imports)]
use core::mem::size_of; use core::mem::size_of;
use std::{fmt::Debug, slice::SliceIndex}; use std::{fmt::Debug, slice::SliceIndex};
/// Gets a `&[T]` at [SliceIndex] `I`. /// Get Raw Bytes at [SliceIndex] `I`.
/// ///
/// This is similar to the [SliceIndex] method `.get(...)`, however implementing this trait /// This is similar to the [SliceIndex] method `.get(...)`, however implementing this
/// for [u8] will auto-impl [ReadWrite]<([i8], [u8], [i16], [u16] ... [i128], [u128])> /// trait will auto-impl [AutoCast]<([i8], [u8], [i16], [u16] ... [i128], [u128])>
pub trait Grab<T> { pub trait Grab {
/// Gets the slice of Self at [SliceIndex] I /// Gets the slice of Self at [SliceIndex] I
fn grab<I>(&self, index: I) -> Option<&<I as SliceIndex<[T]>>::Output> fn grab<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
where where
I: SliceIndex<[T]>; I: SliceIndex<[u8]>;
/// Gets a mutable slice of Self at [SliceIndex] I /// Gets a mutable slice of Self at [SliceIndex] I
fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[T]>>::Output> fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[u8]>>::Output>
where where
I: SliceIndex<[T]>; I: SliceIndex<[u8]>;
} }
/// Read or Write a T at address `addr` /// Read or Write a T at address `addr`
@ -36,6 +36,10 @@ pub trait AutoCast<T>: FallibleAutoCast<T> {
self.read_fallible(addr).unwrap_or_else(|e| panic!("{e:?}")) self.read_fallible(addr).unwrap_or_else(|e| panic!("{e:?}"))
} }
/// Write a T to address `addr` /// Write a T to address `addr`
///
/// # Will Panic
///
/// This will panic on error. For a non-panicking implementation, do it yourself.
fn write(&mut self, addr: impl Into<usize>, data: T) { fn write(&mut self, addr: impl Into<usize>, data: T) {
self.write_fallible(addr, data) self.write_fallible(addr, data)
.unwrap_or_else(|e| panic!("{e:?}")); .unwrap_or_else(|e| panic!("{e:?}"));
@ -43,7 +47,7 @@ pub trait AutoCast<T>: FallibleAutoCast<T> {
} }
/// Read a T from address `addr`, and return the value as a [Result] /// Read a T from address `addr`, and return the value as a [Result]
pub trait FallibleAutoCast<T>: Grab<u8> { pub trait FallibleAutoCast<T>: Grab {
/// The [Err] type /// The [Err] type
type Error: Debug; type Error: Debug;
/// Read a T from address `addr`, returning the value as a [Result] /// Read a T from address `addr`, returning the value as a [Result]
@ -59,8 +63,8 @@ pub trait FallibleAutoCast<T>: Grab<u8> {
/// - `Self::to_be_bytes` /// - `Self::to_be_bytes`
macro_rules! impl_rw {($($t:ty) ,* $(,)?) =>{ macro_rules! impl_rw {($($t:ty) ,* $(,)?) =>{
$( $(
#[doc = concat!("Read or Write [", stringify!($t), "] at address `addr`")] #[doc = concat!("Read or Write [", stringify!($t), "] at address `addr`, *discarding errors*.\n\nThis will never panic.")]
impl<T: Grab<u8> + FallibleAutoCast<$t>> AutoCast<$t> for T { impl<T: Grab + FallibleAutoCast<$t>> AutoCast<$t> for T {
#[inline(always)] #[inline(always)]
fn read(&self, addr: impl Into<usize>) -> $t { fn read(&self, addr: impl Into<usize>) -> $t {
self.read_fallible(addr).ok().unwrap_or_default() self.read_fallible(addr).ok().unwrap_or_default()
@ -70,7 +74,7 @@ macro_rules! impl_rw {($($t:ty) ,* $(,)?) =>{
self.write_fallible(addr, data).ok(); self.write_fallible(addr, data).ok();
} }
} }
impl<T: Grab<u8>> FallibleAutoCast<$t> for T { impl<T: Grab> FallibleAutoCast<$t> for T {
type Error = $crate::error::Error; type Error = $crate::error::Error;
#[inline(always)] #[inline(always)]
fn read_fallible(&self, addr: impl Into<usize>) -> $crate::error::Result<$t> { fn read_fallible(&self, addr: impl Into<usize>) -> $crate::error::Result<$t> {

View File

@ -6,20 +6,14 @@
pub use chirp::*; pub use chirp::*;
fn setup_environment() -> (CPU, Bus) { fn setup_environment() -> (CPU, Screen) {
let mut cpu = CPU::default(); let mut cpu = CPU::default();
cpu.flags = Flags { cpu.flags = Flags {
debug: true, debug: true,
pause: false, pause: false,
..Default::default() ..Default::default()
}; };
( (cpu, Screen::default())
cpu,
bus! {
// Create a screen, and fill it with garbage
Screen [0x000..0x100] = b"jsuadhgufywegrwsdyfogbbg4owgbrt",
},
)
} }
struct SuiteTest { struct SuiteTest {
@ -28,23 +22,21 @@ struct SuiteTest {
screen: &'static [u8], screen: &'static [u8],
} }
fn run_screentest(test: SuiteTest, mut cpu: CPU, mut bus: Bus) { fn run_screentest(test: SuiteTest, mut cpu: CPU, mut screen: Screen) {
// Set the test to run // Set the test to run
cpu.poke(0x1ffu16, test.test); cpu.poke(0x1ffu16, test.test);
cpu.load_program_bytes(test.data).unwrap(); cpu.load_program_bytes(test.data).unwrap();
// The test suite always initiates a keypause on test completion // The test suite always initiates a keypause on test completion
while !(cpu.flags.is_paused()) { while !(cpu.flags.is_paused()) {
cpu.multistep(&mut bus, 10).unwrap(); cpu.multistep(&mut screen, 10).unwrap();
if cpu.cycle() > 1000000 { if cpu.cycle() > 1000000 {
panic!("test {} took too long", test.test) panic!("test {} took too long", test.test)
} }
} }
// Compare the screen to the reference screen buffer // Compare the screen to the reference screen buffer
bus.print_screen().unwrap(); screen.print_screen();
bus! {crate::cpu::bus::Region::Screen [0..256] = test.screen} Screen::from(test.screen).print_screen();
.print_screen() assert_eq!(screen.grab(..).unwrap(), test.screen);
.unwrap();
assert_eq!(bus.get_region(Screen).unwrap(), test.screen);
} }
#[test] #[test]

View File

@ -1,52 +1,45 @@
//! Testing methods on Chirp's public API //! Testing methods on Chirp's public API
use chirp::cpu::mem::Region::*;
use chirp::*; use chirp::*;
use std::{collections::hash_map::DefaultHasher, hash::Hash}; use std::{collections::hash_map::DefaultHasher, hash::Hash};
#[test]
#[allow(clippy::redundant_clone)]
fn chip8() {
let ch8 = Chip8::default(); // Default
let ch82 = ch8.clone(); // Clone
assert_eq!(ch8, ch82); // PartialEq
println!("{ch8:?}"); // Debug
}
mod bus { mod bus {
use super::*; use super::*;
mod region { mod region {
use super::*; use super::*;
// #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] // #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[test] #[test]
fn copy() { fn copy() {
let r1 = Screen; let r1 = Charset;
let r2 = r1; let r2 = r1;
assert_eq!(r1, r2); assert_eq!(r1, r2);
} }
#[test] #[test]
#[allow(clippy::clone_on_copy)] #[allow(clippy::clone_on_copy)]
fn clone() { fn clone() {
let r1 = Screen; let r1 = Charset;
let r2 = r1.clone(); let r2 = r1.clone();
assert_eq!(r1, r2); assert_eq!(r1, r2);
} }
#[test] #[test]
fn display() { fn display() {
println!("{Charset}{Program}{Screen}{Count}"); println!("{Charset}{Program}{Count}");
} }
#[test] #[test]
fn debug() { fn debug() {
println!("{Charset:?}{Program:?}{Screen:?}{Count:?}"); println!("{Charset:?}{Program:?}{Count:?}");
} }
// lmao the things you do for test coverage // lmao the things you do for test coverage
#[test] #[test]
fn eq() { fn eq() {
assert_eq!(Screen, Screen); assert_eq!(Charset, Charset);
assert_ne!(Charset, Program); assert_ne!(Charset, Program);
} }
#[test] #[test]
fn ord() { fn ord() {
assert_eq!(Screen, Charset.max(Program).max(Screen)); assert_eq!(Program, Charset.max(Program));
assert!(Charset < Program && Program < Screen); assert!(Charset < Program);
} }
#[test] #[test]
fn hash() { fn hash() {
@ -59,7 +52,7 @@ mod bus {
#[should_panic] #[should_panic]
fn bus_missing_region() { fn bus_missing_region() {
// Print the screen of a bus with no screen // Print the screen of a bus with no screen
bus! {}.print_screen().unwrap() mem! {}.get_region(Charset).unwrap();
} }
} }
@ -208,7 +201,7 @@ mod dis {
#[test] #[test]
fn error() { fn error() {
let error = chirp::error::Error::MissingRegion { region: Screen }; let error = chirp::error::Error::InvalidAddressRange { range: (..).into() };
// Print it with Display and Debug // Print it with Display and Debug
println!("{error} {error:?}"); println!("{error} {error:?}");
} }