From ef3d7656519e1ddb7092c21903222ffe8a8e20e6 Mon Sep 17 00:00:00 2001 From: John Breaux Date: Fri, 17 Mar 2023 20:06:31 -0500 Subject: [PATCH] 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 --- Cargo.lock | 17 +++- Cargo.toml | 1 + src/bus.rs | 206 +++++++++++++++++++++++++++++++++++++----- src/bus/bus_device.rs | 3 + src/bus/iterator.rs | 52 +++++++++++ src/cpu.rs | 66 ++++++++------ src/lib.rs | 3 +- src/main.rs | 54 +++++++++-- src/mem.rs | 6 ++ src/screen.rs | 40 +++----- 10 files changed, 357 insertions(+), 91 deletions(-) create mode 100644 src/bus/iterator.rs diff --git a/Cargo.lock b/Cargo.lock index 14e2eff..94ad43f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 11efab7..58b32b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" [dependencies] owo-colors = "^3" +rhexdump = "0.1.1" serde = { version = "^1.0", features = ["derive"] } thiserror = "1.0.39" diff --git a/src/bus.rs b/src/bus.rs index 1648cf9..aa517c3 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -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, + region: HashMap<&'static str, Range>, +} + +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) -> 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(&self, index: I) -> Option<&>::Output> + where + I: SliceIndex<[u8]>, + { + self.memory.get(index) + } + /// Gets a mutable slice of bus memory + pub fn get_mut(&mut self, index: I) -> Option<&mut >::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 for NewBus { + fn read(&self, addr: impl Into) -> u8 { + *self.memory.get(addr.into()).unwrap_or(&0xc5) + } +} + +impl Read for NewBus { + fn read(&self, addr: impl Into) -> u16 { + let addr: usize = addr.into(); + if let Some(bytes) = self.memory.get(addr..addr + 2) { + u16::from_be_bytes(bytes.try_into().expect("asked for 2 bytes, got != 2 bytes")) + } else { + 0xc5c5 + } + //u16::from_le_bytes(self.memory.get([addr;2])) + } +} + +impl Write for NewBus { + fn write(&mut self, addr: impl Into, data: u8) { + let addr: usize = addr.into(); + if let Some(byte) = self.get_mut(addr) { + *byte = data; + } + } +} + +impl Write for NewBus { + fn write(&mut self, addr: impl Into, 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; 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 { /// Read a T from address `addr` - fn read(&self, addr: u16) -> T; + fn read(&self, addr: impl Into) -> T; } /// Write "some data" to the Bus pub trait Write { /// Write a T to address `addr` - fn write(&mut self, addr: u16, data: T); + fn write(&mut self, addr: impl Into, 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> { - 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 { - 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 for Bus { - fn read(&self, addr: u16) -> u8 { + fn read(&self, addr: impl Into) -> 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 for Bus { } impl Read for Bus { - fn read(&self, addr: u16) -> u16 { + fn read(&self, addr: impl Into) -> 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 for Bus { - fn write(&mut self, addr: u16, data: u8) { + fn write(&mut self, addr: impl Into, 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 for Bus { } impl Write for Bus { - fn write(&mut self, addr: u16, data: u16) { - for item in &mut self.devices { - item.write_to(addr, (data >> 8) as u8); - item.write_to(addr.wrapping_add(1), data as u8); + fn write(&mut self, addr: impl Into, 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 for Bus { + fn write(&mut self, addr: impl Into, 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) { - 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) + } +} diff --git a/src/bus/bus_device.rs b/src/bus/bus_device.rs index 689df3e..ca310b9 100644 --- a/src/bus/bus_device.rs +++ b/src/bus/bus_device.rs @@ -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 { diff --git a/src/bus/iterator.rs b/src/bus/iterator.rs new file mode 100644 index 0000000..f657a90 --- /dev/null +++ b/src/bus/iterator.rs @@ -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, + addr: u16, + bus: &'a Bus, +} + +impl<'a> BusIterator<'a> { + /// Creates a new BusIterator with a specified range + pub fn new(range: Range, bus: &'a Bus) -> BusIterator<'a> { + BusIterator { + addr: range.start, + range, + bus, + } + } + pub fn range(mut self, range: Range) -> Self { + self.range = range; + self + } +} + +impl<'a> Iterator for BusIterator<'a> { + type Item = u8; + + fn next(&mut self) -> Option { + let mut res = None; + if self.range.contains(&self.addr) { + res = Some(self.bus.read(self.addr)); + self.addr += 1; + } + res + } +} diff --git a/src/cpu.rs b/src/cpu.rs index 142fdc5..3bd628a 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -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(&mut self, bus: &mut B) + where + B: Read + Write + Read + Write + { 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) { 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) { 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) { 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(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut I) + where + I: Read + Read + { + 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) { // 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) { 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) { for reg in 0..=x { self.v[reg] = bus.read(self.i + reg as u16); } diff --git a/src/lib.rs b/src/lib.rs index d0d41b2..e3eacc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/main.rs b/src/main.rs index 32fb17d..b0db83a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(()) } diff --git a/src/mem.rs b/src/mem.rs index a650288..e46e8c9 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -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) + } } diff --git a/src/screen.rs b/src/screen.rs index e0a869b..2ab84e8 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -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 { - 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) { - 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)) - } -}