experimentation: benchmarking and alternate impl's

- Do some basic benchmarking with std::time
- Try writing bus writer based on iterator
  - Fail, because that requires mutable iterator
  - Begin rewriting bus based on simpler design instead.
    - Simpler design uses a unified memory model,
      which grows based on the maximum addresses expected in it
    - Still uses the "infallible" Read/Write traits from previous
      implementation. :( Alas, it's much faster during operation,
      even if it takes longer to instantiate.
    - Reassessed the syntax for bus macro
  - Made CPU tick generic over bus::Read and bus::Write traits
This commit is contained in:
John 2023-03-17 20:06:31 -05:00
parent 2ba807d7a8
commit ef3d765651
10 changed files with 357 additions and 91 deletions

17
Cargo.lock generated
View File

@ -7,6 +7,7 @@ name = "chumpulator"
version = "0.1.0"
dependencies = [
"owo-colors",
"rhexdump",
"serde",
"thiserror",
]
@ -36,19 +37,25 @@ dependencies = [
]
[[package]]
name = "serde"
version = "1.0.153"
name = "rhexdump"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a382c72b4ba118526e187430bb4963cd6d55051ebf13d9b25574d379cc98d20"
checksum = "c5e9af64574935e39f24d1c0313a997c8b880ca0e087c888bc6af8af31579847"
[[package]]
name = "serde"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.153"
version = "1.0.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ef476a5790f0f6decbc66726b6e5d63680ed518283e64c7df415989d880954f"
checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217"
dependencies = [
"proc-macro2",
"quote",

View File

@ -7,5 +7,6 @@ edition = "2021"
[dependencies]
owo-colors = "^3"
rhexdump = "0.1.1"
serde = { version = "^1.0", features = ["derive"] }
thiserror = "1.0.39"

View File

@ -1,11 +1,15 @@
//! 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 std::{
collections::HashMap,
fmt::{Debug, Display, Formatter, Result},
ops::Range,
slice::SliceIndex,
};
/// Creates a new bus, instantiating BusConnectable devices
@ -27,6 +31,127 @@ macro_rules! bus {
};
}
#[macro_export]
macro_rules! newbus {
($($name:literal $(:)? [$range:expr] $(= $data:expr)?) ,* $(,)?) => {
$crate::bus::NewBus::new()
$(
.add_region($name, $range)
$(
.load_region($name, $data)
)?
)*
};
}
/// Store memory in a series of named regions with ranges
#[derive(Debug, Default)]
pub struct NewBus {
memory: Vec<u8>,
region: HashMap<&'static str, Range<usize>>,
}
impl NewBus {
/// Construct a new bus
pub fn new() -> Self {
NewBus::default()
}
/// Gets the length of the bus' backing memory
pub fn len(&self) -> usize {
self.memory.len()
}
/// Returns true if the backing memory contains no elements
pub fn is_empty(&self) -> bool {
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){
if self.len() < size {
self.memory.resize(size, 0);
}
}
pub fn add_region(mut self, name: &'static str, range: Range<usize>) -> Self {
self.with_size(range.end);
self.region.insert(name, range);
self
}
pub fn load_region(mut self, name: &str, data: &[u8]) -> Self {
use std::io::Write;
if let Some(mut region) = self.get_region_mut(name) {
dbg!(region.write(data)).ok(); // TODO: THIS SUCKS
}
self
}
/// Gets a slice of bus memory
pub fn get<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
where
I: SliceIndex<[u8]>,
{
self.memory.get(index)
}
/// Gets a mutable slice of bus memory
pub fn get_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[u8]>>::Output>
where
I: SliceIndex<[u8]>,
{
self.memory.get_mut(index)
}
/// Gets a slice of a named region of memory
pub fn get_region(&self, name: &str) -> Option<&[u8]> {
self.get(self.region.get(name)?.clone())
}
/// Gets a mutable slice to a named region of memory
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)
}
}
impl Read<u16> for NewBus {
fn read(&self, addr: impl Into<usize>) -> u16 {
let addr: usize = addr.into();
if let Some(bytes) = self.memory.get(addr..addr + 2) {
u16::from_be_bytes(bytes.try_into().expect("asked for 2 bytes, got != 2 bytes"))
} else {
0xc5c5
}
//u16::from_le_bytes(self.memory.get([addr;2]))
}
}
impl Write<u8> for NewBus {
fn write(&mut self, addr: impl Into<usize>, data: u8) {
let addr: usize = addr.into();
if let Some(byte) = self.get_mut(addr) {
*byte = data;
}
}
}
impl Write<u16> for NewBus {
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
@ -34,19 +159,20 @@ macro_rules! bus {
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: u16) -> T;
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: u16, data: T);
fn write(&mut self, addr: impl Into<usize>, data: T);
}
/// The Bus connects bus readers with bus writers.
@ -71,28 +197,39 @@ impl Bus {
self.devices.push(BusDevice::new(name, range, device));
self
}
pub fn get_region_by_name(&self, name: &str) -> Option<Range<u16>> {
for item in &self.devices {
if item.name == name {
return Some(item.range.clone());
}
}
None
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> {
Some(self.read(addr))
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) {
self.write(addr, data)
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: u16) -> u8 {
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)
@ -102,18 +239,18 @@ impl Read<u8> for Bus {
}
impl Read<u16> for Bus {
fn read(&self, addr: u16) -> u16 {
fn read(&self, addr: impl Into<usize>) -> u16 {
let addr = addr.into() as u16;
let mut result = 0;
for item in &self.devices {
result |= (item.read_at(addr).unwrap_or(0) as u16) << 8;
result |= item.read_at(addr.wrapping_add(1)).unwrap_or(0) as u16;
}
result |= (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: u16, data: u8) {
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)
}
@ -121,10 +258,18 @@ impl Write<u8> for Bus {
}
impl Write<u16> for Bus {
fn write(&mut self, addr: u16, data: u16) {
for item in &mut self.devices {
item.write_to(addr, (data >> 8) as u8);
item.write_to(addr.wrapping_add(1), data as u8);
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);
}
}
}
@ -140,8 +285,11 @@ impl Display for Bus {
impl Dumpable for Bus {
fn dump(&self, range: Range<usize>) {
for index in range {
let byte: u8 = self.read(index as u16);
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);
}
}
@ -155,3 +303,13 @@ impl BinDumpable for Bus {
}
}
}
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)
}
}

View File

@ -42,6 +42,9 @@ impl BusConnectible for BusDevice {
self.device.write_to(addr, data);
}
}
fn get_mut(&mut self, addr: u16) -> Option<&mut u8> {
return self.device.get_mut(addr);
}
}
impl Display for BusDevice {

52
src/bus/iterator.rs Normal file
View File

@ -0,0 +1,52 @@
//! Iterators for working with Busses
use super::{Bus, Read};
use std::ops::Range;
pub trait IterMut<'a> {
type Item;
fn next(&'a mut self) -> Option<&'a mut Self::Item>;
}
pub trait IntoIterMut<'a> {
type Item;
type IntoIter;
fn into_iter(self) -> Self::IntoIter;
}
#[derive(Debug)]
pub struct BusIterator<'a> {
range: Range<u16>,
addr: u16,
bus: &'a Bus,
}
impl<'a> BusIterator<'a> {
/// Creates a new BusIterator with a specified range
pub fn new(range: Range<u16>, bus: &'a Bus) -> BusIterator<'a> {
BusIterator {
addr: range.start,
range,
bus,
}
}
pub fn range(mut self, range: Range<u16>) -> Self {
self.range = range;
self
}
}
impl<'a> Iterator for BusIterator<'a> {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
let mut res = None;
if self.range.contains(&self.addr) {
res = Some(self.bus.read(self.addr));
self.addr += 1;
}
res
}
}

View File

@ -3,7 +3,7 @@
pub mod disassemble;
use self::disassemble::Disassemble;
use crate::bus::{Bus, Read, Write};
use crate::bus::{Read, Write};
use owo_colors::OwoColorize;
type Reg = usize;
@ -116,7 +116,10 @@ impl CPU {
}
}
pub fn tick(&mut self, bus: &mut Bus) {
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);
// fetch opcode
let opcode: u16 = bus.read(self.pc);
@ -205,7 +208,7 @@ impl CPU {
// Cxbb: Stores a random number + the provided byte into vX
0xc => self.rand(x, b),
// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY)
0xd => self.draw(x, y, n),
0xd => self.draw(x, y, n, bus),
// # Skips instruction on value of keypress
// |opcode| effect |
@ -231,12 +234,12 @@ impl CPU {
// | fX55 | DMA Stor from I to registers 0..X |
// | fX65 | DMA Load from I to registers 0..X |
0xf => match b {
0x07 => self.get_delay_timer(x, bus),
0x0A => self.wait_for_key(x, bus),
0x15 => self.load_delay_timer(x, bus),
0x18 => self.load_sound_timer(x, bus),
0x1E => self.add_to_indirect(x, bus),
0x29 => self.load_sprite_x(x, bus),
0x07 => self.get_delay_timer(x),
0x0A => self.wait_for_key(x),
0x15 => self.load_delay_timer(x),
0x18 => self.load_sound_timer(x),
0x1E => self.add_to_indirect(x),
0x29 => self.load_sprite_x(x),
0x33 => self.bcd_convert_i(x, bus),
0x55 => self.dma_store(x, bus),
0x65 => self.dma_load(x, bus),
@ -290,7 +293,7 @@ impl CPU {
}
/// 00e0: Clears the screen memory to 0
#[inline]
fn clear_screen(&mut self, bus: &mut Bus) {
fn clear_screen(&mut self, bus: &mut impl Write<u8>) {
for addr in self.screen..self.screen + 0x100 {
bus.write(addr, 0u8);
}
@ -299,7 +302,7 @@ impl CPU {
}
/// 00ee: Returns from subroutine
#[inline]
fn ret(&mut self, bus: &mut Bus) {
fn ret(&mut self, bus: &impl Read<u16>) {
self.sp = self.sp.wrapping_add(2);
self.pc = bus.read(self.sp);
}
@ -310,7 +313,7 @@ impl CPU {
}
/// 2aaa: Pushes pc onto the stack, then jumps to a
#[inline]
fn call(&mut self, a: Adr, bus: &mut Bus) {
fn call(&mut self, a: Adr, bus: &mut impl Write<u16>) {
bus.write(self.sp, self.pc);
self.sp = self.sp.wrapping_sub(2);
self.pc = a;
@ -427,12 +430,23 @@ impl CPU {
}
/// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY)
#[inline]
fn draw(&mut self, x: Reg, y: Reg, n: Nib) {
// TODO: Screen
todo!("{}", format_args!("draw\t#{n:x}, v{x:x}, v{y:x}").red());
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());
self.v[0xf] = 0;
// TODO: Repeat for all N
// TODO: Calculate the lower bound address based on the X,Y position on the screen
// TODO: Read a u16 from the bus containing the two bytes which might need to be updated
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}")
}
}
/// Ex9E: Skip next instruction if key == #X
#[inline]
@ -457,12 +471,12 @@ impl CPU {
/// vX = DT
/// ```
#[inline]
fn get_delay_timer(&mut self, x: Reg, _bus: &mut Bus) {
fn get_delay_timer(&mut self, x: Reg) {
self.v[x] = self.delay;
}
/// Fx0A: Wait for key, then vX = K
#[inline]
fn wait_for_key(&mut self, x: Reg, _bus: &mut Bus) {
fn wait_for_key(&mut self, x: Reg) {
// TODO: I/O
std::println!("{}", format_args!("waitk\tv{x:x}").red());
@ -472,7 +486,7 @@ impl CPU {
/// DT = vX
/// ```
#[inline]
fn load_delay_timer(&mut self, x: Reg, _bus: &mut Bus) {
fn load_delay_timer(&mut self, x: Reg) {
self.delay = self.v[x];
}
/// Fx18: Load vX into ST
@ -480,7 +494,7 @@ impl CPU {
/// ST = vX;
/// ```
#[inline]
fn load_sound_timer(&mut self, x: Reg, _bus: &mut Bus) {
fn load_sound_timer(&mut self, x: Reg) {
self.sound = self.v[x];
}
/// Fx1e: Add vX to I,
@ -488,7 +502,7 @@ impl CPU {
/// I += vX;
/// ```
#[inline]
fn add_to_indirect(&mut self, x: Reg, _bus: &mut Bus) {
fn add_to_indirect(&mut self, x: Reg) {
self.i += self.v[x] as u16;
}
/// Fx29: Load sprite for character x into I
@ -496,19 +510,19 @@ impl CPU {
/// I = sprite(X);
/// ```
#[inline]
fn load_sprite_x(&mut self, x: Reg, _bus: &mut Bus) {
fn load_sprite_x(&mut self, x: Reg) {
self.i = self.font + (5 * x as Adr);
}
/// Fx33: BCD convert X into I`[0..3]`
#[inline]
fn bcd_convert_i(&mut self, x: Reg, _bus: &mut Bus) {
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());
}
/// Fx55: DMA Stor from I to registers 0..X
#[inline]
fn dma_store(&mut self, x: Reg, bus: &mut Bus) {
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]);
}
@ -516,7 +530,7 @@ impl CPU {
}
/// Fx65: DMA Load from I to registers 0..X
#[inline]
fn dma_load(&mut self, x: Reg, bus: &mut Bus) {
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);
}

View File

@ -19,7 +19,8 @@ pub mod screen;
pub mod prelude {
use super::*;
pub use crate::bus;
pub use bus::{Bus, BusConnectible};
pub use crate::newbus;
pub use bus::{Bus, BusConnectible, Read, Write};
pub use cpu::{disassemble::Disassemble, CPU};
pub use dump::{BinDumpable, Dumpable};
pub use mem::Mem;

View File

@ -1,7 +1,15 @@
use chumpulator::{bus::Read, prelude::*};
use std::fs::read;
use std::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
fn main() -> Result<(), std::io::Error> {
let mut now;
println!("Building Bus...");
let mut time = Instant::now();
let mut bus = bus! {
// Load the charset into ROM
"charset" [0x0050..0x00a0] = Mem::new(0x50).load_charset(0).w(false),
@ -12,20 +20,52 @@ fn main() -> Result<(), std::io::Error> {
// Create some stack memory
"stack" [0xF000..0xF800] = Mem::new(0x800).r(true).w(true),
};
println!("{bus}");
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")?,
// Create a screen
"screen" [0x0F00..0x1000],
// Create some stack memory
"stack" [0x2000..0x2800],
};
now = time.elapsed();
println!("Elapsed: {:?}", now);
println!("{newbus}");
let disassembler = Disassemble::default();
for addr in 0x200..0x290 {
if addr % 2 == 0 {
println!("{addr:03x}: {}", disassembler.instruction(bus.read(addr)));
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, 0xf7fe, disassembler);
for _instruction in 0..100 {
let mut cpu2 = cpu.clone();
println!("Old Bus:");
for _instruction in 0..6 {
time = Instant::now();
cpu.tick(&mut bus);
//bus.dump(0xF7e0..0xf800);
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(())
}

View File

@ -144,4 +144,10 @@ impl BusConnectible for Mem {
*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)
}
}

View File

@ -1,5 +1,7 @@
//! 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},
@ -8,39 +10,21 @@ use std::{
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Screen {
mem: Mem,
width: usize,
height: usize,
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 {
mem: Mem::new(width * height / 8),
width,
height,
width: 64,
height: 32,
}
}
}
impl BusConnectible for Screen {
fn read_at(&self, addr: u16) -> Option<u8> {
self.mem.read_at(addr)
}
fn write_to(&mut self, addr: u16, data: u8) {
self.mem.write_to(addr, data)
}
}
impl Dumpable for Screen {
fn dump(&self, range: Range<usize>) {
self.mem.dump(range)
}
}
impl Display for Screen {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", self.mem.window(0..self.width * self.height / 8))
}
}