Initial commit:

Created outline of emulator:
The emulator has a Bus, which attaches a CPU to some Memory (Mapped Devices)
The design isn't particularly efficient, but the interpreter only needs to
run at ~500Hz or so. It's Rust. It can do that.

Instructions yet to be implemented:
Cxbb: "Store a random number, masked by bitmask bb, into vX"
Dxyn: "Draw an 8 by n sprite to the screen at coordinates (x, y)"
Fx0A: "Wait for a key, then set vX to the value of the pressed key"
Fx33: "BCD convert X, storing the results in &I[0..3]"

Thoughts going forward:
  - It's probably a good idea to parse instructions out into an enum.
    I had this in an earlier design, but it didn't really look that good.
    However, I haven't read many other emulators before, so I don't know the
    style people generally go for.
  - I haven't used a native graphics library before, and my cg class was done
    entirely in a web browser. That kinda sucks, honestly. Sure the skill
    might transfer well, but, >JS
This commit is contained in:
John 2023-03-08 06:07:33 -06:00
commit a721a00232
14 changed files with 1570 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
# Chip-8 test ROMs
/chip-8
# VS Code files
/.vscode

93
Cargo.lock generated Normal file
View File

@ -0,0 +1,93 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "owo-colors"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "proc-macro2"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rumpulator"
version = "0.1.0"
dependencies = [
"owo-colors",
"serde",
"thiserror",
]
[[package]]
name = "serde"
version = "1.0.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a382c72b4ba118526e187430bb4963cd6d55051ebf13d9b25574d379cc98d20"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ef476a5790f0f6decbc66726b6e5d63680ed518283e64c7df415989d880954f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"

11
Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "rumpulator"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
owo-colors = "^3"
serde = { version = "^1.0", features = ["derive"] }
thiserror = "1.0.39"

157
src/bus.rs Normal file
View File

@ -0,0 +1,157 @@
//! The Bus connects the CPU to Memory
mod bus_device;
use crate::dump::{BinDumpable, Dumpable};
use bus_device::BusDevice;
use std::{
fmt::{Debug, Display, Formatter, Result},
ops::Range,
};
/// Creates a new bus, instantiating BusConnectable devices
/// # Examples
/// ```rust
/// # use rumpulator::prelude::*;
/// let mut bus = bus! {
/// "RAM" [0x0000..0x8000] Mem::new(0x8000),
/// "ROM" [0x8000..0xFFFF] Mem::new(0x8000).w(false),
/// };
/// ```
#[macro_export]
macro_rules! bus {
($($name:literal $(:)? [$range:expr] $(=)? $d:expr) ,* $(,)?) => {
$crate::bus::Bus::new()
$(
.connect($name, $range, Box::new($d))
)*
};
}
/// BusConnectable objects can be connected to a bus with `Bus::connect()`
///
/// The bus performs address translation, so your object will receive
/// reads and writes relative to offset 0
pub trait BusConnectible: Debug + Display {
fn read_at(&self, addr: u16) -> Option<u8>;
fn write_to(&mut self, addr: u16, data: u8);
}
// Traits Read and Write are here purely to make implementing other things more bearable
/// Do whatever `Read` means to you
pub trait Read<T> {
/// Read a T from address `addr`
fn read(&self, addr: u16) -> 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);
}
/// The Bus connects bus readers with bus writers.
/// The design assumes single-threaded operation.
#[derive(Debug, Default)]
pub struct Bus {
devices: Vec<BusDevice>,
}
impl Bus {
/// Construct a new bus
pub fn new() -> Self {
Bus::default()
}
/// Connect a BusConnectible object to the bus
pub fn connect(
mut self,
name: &str,
range: Range<u16>,
device: Box<dyn BusConnectible>,
) -> Self {
self.devices.push(BusDevice::new(name, range, device));
self
}
pub fn get_region_by_name(&self, name: &str) -> Option<Range<u16>> {
for item in &self.devices {
if item.name == name {
return Some(item.range.clone());
}
}
None
}
}
/// lmao
impl BusConnectible for Bus {
fn read_at(&self, addr: u16) -> Option<u8> {
Some(self.read(addr))
}
fn write_to(&mut self, addr: u16, data: u8) {
self.write(addr, data)
}
}
impl Read<u8> for Bus {
fn read(&self, addr: u16) -> u8 {
let mut result: u8 = 0;
for item in &self.devices {
result |= item.read_at(addr).unwrap_or(0)
}
result
}
}
impl Read<u16> for Bus {
fn read(&self, addr: u16) -> u16 {
let mut result = 0;
for item in &self.devices {
result |= (item.read_at(addr).unwrap_or(0) as u16) << 8;
result |= item.read_at(addr.wrapping_add(1)).unwrap_or(0) as u16;
}
result
}
}
impl Write<u8> for Bus {
fn write(&mut self, addr: u16, data: u8) {
for item in &mut self.devices {
item.write_to(addr, data)
}
}
}
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);
}
}
}
impl Display for Bus {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
for device in &self.devices {
write!(f, "{device}")?;
}
write!(f, "")
}
}
impl Dumpable for Bus {
fn dump(&self, range: Range<usize>) {
for index in range {
let byte: u8 = self.read(index as u16);
crate::dump::as_hexdump(index, byte);
}
}
}
impl BinDumpable for Bus {
fn bin_dump(&self, range: Range<usize>) {
for index in range {
let byte: u8 = self.read(index as u16);
crate::dump::as_bindump(index, byte)
}
}
}

51
src/bus/bus_device.rs Normal file
View File

@ -0,0 +1,51 @@
//! Connects a BusConnectible to the Bus
use super::BusConnectible;
use std::{
fmt::{Display, Formatter, Result},
ops::Range,
};
/// BusDevice performs address translation for BusConnectibles.
/// It is an implementation detail of Bus.connect()
#[derive(Debug)]
pub struct BusDevice {
pub name: String,
pub range: Range<u16>,
device: Box<dyn BusConnectible>,
}
impl BusDevice {
pub fn new(name: &str, range: Range<u16>, device: Box<dyn BusConnectible>) -> Self {
BusDevice {
name: name.to_string(),
range,
device,
}
}
fn translate_address(&self, addr: u16) -> Option<u16> {
let addr = addr.wrapping_sub(self.range.start);
if addr < self.range.end {
Some(addr)
} else {
None
}
}
}
impl BusConnectible for BusDevice {
fn read_at(&self, addr: u16) -> Option<u8> {
self.device.read_at(self.translate_address(addr)?)
}
fn write_to(&mut self, addr: u16, data: u8) {
if let Some(addr) = self.translate_address(addr) {
self.device.write_to(addr, data);
}
}
}
impl Display for BusDevice {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
writeln!(f, "{} [{:04x?}]:\n{}", self.name, self.range, self.device)
}
}

525
src/cpu.rs Normal file
View File

@ -0,0 +1,525 @@
//! The CPU decodes and runs instructions
pub mod disassemble;
use self::disassemble::Disassemble;
use crate::bus::{Bus, Read, Write};
use owo_colors::OwoColorize;
type Reg = usize;
type Adr = u16;
type Nib = u8;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CPUBuilder {
screen: Option<Adr>,
font: Option<Adr>,
pc: Option<Adr>,
sp: Option<Adr>,
}
impl CPUBuilder {
pub fn new() -> Self {
CPUBuilder {
screen: None,
font: None,
pc: None,
sp: None,
}
}
pub fn build(self) -> CPU {
CPU {
screen: self.screen.unwrap_or(0xF00),
font: self.font.unwrap_or(0x050),
pc: self.pc.unwrap_or(0x200),
sp: self.sp.unwrap_or(0xefe),
i: 0,
v: [0; 16],
delay: 0,
sound: 0,
cycle: 0,
keys: 0,
disassembler: Disassemble::default(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CPU {
// memory map info
screen: Adr,
font: Adr,
// registers
pc: Adr,
sp: Adr,
i: Adr,
v: [u8; 16],
delay: u8,
sound: u8,
// I/O
keys: usize,
// Execution data
cycle: usize,
disassembler: Disassemble,
}
// public interface
impl CPU {
/// Press keys (where `keys` is a bitmap of the keys [F-0])
pub fn press(mut self, keys: u16) -> Self {
self.keys = keys as usize;
self
}
/// Set a general purpose register in the CPU
/// # Examples
/// ```rust
/// # use rumpulator::prelude::*;
/// // Create a new CPU, and set v4 to 0x41
/// let cpu = CPU::default()
/// .set_gpr(0x4, 0x41);
/// // Dump the CPU registers
/// cpu.dump();
/// ```
pub fn set_gpr(mut self, gpr: Reg, value: u8) -> Self {
if let Some(gpr) = self.v.get_mut(gpr) {
*gpr = value;
}
self
}
/// Constructs a new CPU with sane defaults
///
/// | value | default | description
/// |--------|---------|------------
/// | screen | 0x0f00 | Location of screen memory.
/// | font | 0x0050 | Location of font memory.
/// | pc | 0x0200 | Start location. Generally 0x200 or 0x600.
/// | sp | 0x0efe | Initial top of stack.
/// # Examples
/// ```rust
/// # use rumpulator::prelude::*;
/// let mut cpu = CPU::new(0xf00, 0x50, 0x200, 0xefe, Disassemble::default());
/// ```
pub fn new(screen: Adr, font: Adr, pc: Adr, sp: Adr, disassembler: Disassemble) -> Self {
CPU {
disassembler,
screen,
font,
pc,
sp,
i: 0,
v: [0; 16],
delay: 0,
sound: 0,
cycle: 0,
keys: 0,
}
}
pub fn tick(&mut self, bus: &mut Bus) {
std::print!("{:3} {:03x}: ", self.cycle.bright_black(), self.pc);
// fetch opcode
let opcode: u16 = bus.read(self.pc);
// DINC pc
self.pc = self.pc.wrapping_add(2);
// decode opcode
// Print opcode disassembly:
std::println!("{}", self.disassembler.instruction(opcode));
use disassemble::{a, b, i, n, x, y};
let (i, x, y, n, b, a) = (
i(opcode),
x(opcode),
y(opcode),
n(opcode),
b(opcode),
a(opcode),
);
match i {
// # Issue a system call
// |opcode| effect |
// |------|------------------------------------|
// | 00e0 | Clear screen memory to all 0 |
// | 00ee | Return from subroutine |
0x0 => match a {
0x0e0 => self.clear_screen(bus),
0x0ee => self.ret(bus),
_ => self.sys(a),
},
// | 1aaa | Sets pc to an absolute address
0x1 => self.jump(a),
// | 2aaa | Pushes pc onto the stack, then jumps to a
0x2 => self.call(a, bus),
// | 3xbb | Skips next instruction if register X == b
0x3 => self.skip_if_x_equal_byte(x, b),
// | 4xbb | Skips next instruction if register X != b
0x4 => self.skip_if_x_not_equal_byte(x, b),
// # Performs a register-register comparison
// |opcode| effect |
// |------|------------------------------------|
// | 9XY0 | Skip next instruction if vX == vY |
0x5 => match n {
0x0 => self.skip_if_x_equal_y(x, y),
_ => self.unimplemented(opcode),
},
// 6xbb: Loads immediate byte b into register vX
0x6 => self.load_immediate(x, b),
// 7xbb: Adds immediate byte b to register vX
0x7 => self.add_immediate(x, b),
// # Performs ALU operation
// |opcode| effect |
// |------|------------------------------------|
// | 8xy0 | Y = X |
// | 8xy1 | X = X | Y |
// | 8xy2 | X = X & Y |
// | 8xy3 | X = X ^ Y |
// | 8xy4 | X = X + Y; Set vF=carry |
// | 8xy5 | X = X - Y; Set vF=carry |
// | 8xy6 | X = X >> 1 |
// | 8xy7 | X = Y - X; Set vF=carry |
// | 8xyE | X = X << 1 |
0x8 => match n {
0x0 => self.load_y_into_x(x, y),
0x1 => self.x_orequals_y(x, y),
0x2 => self.x_andequals_y(x, y),
0x3 => self.x_xorequals_y(x, y),
0x4 => self.x_addequals_y(x, y),
0x5 => self.x_subequals_y(x, y),
0x6 => self.shift_right_x(x),
0x7 => self.backwards_subtract(x, y),
0xE => self.shift_left_x(x),
_ => self.unimplemented(opcode),
},
// # Performs a register-register comparison
// |opcode| effect |
// |------|------------------------------------|
// | 9XY0 | Skip next instruction if vX != vY |
0x9 => match n {
0 => self.skip_if_x_not_equal_y(x, y),
_ => self.unimplemented(opcode),
},
// Aaaa: Load address #a into register I
0xa => self.load_indirect_register(a),
// Baaa: Jump to &adr + v0
0xb => self.jump_indexed(a),
// Cxbb: Stores a random number + the provided byte into vX
0xc => self.rand(x, b),
// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY)
0xd => self.draw(x, y, n),
// # Skips instruction on value of keypress
// |opcode| effect |
// |------|------------------------------------|
// | eX9e | Skip next instruction if key == #X |
// | eXa1 | Skip next instruction if key != #X |
0xe => match b {
0x9e => self.skip_if_key_equals_x(x),
0xa1 => self.skip_if_key_not_x(x),
_ => self.unimplemented(opcode),
},
// # Performs IO
// |opcode| effect |
// |------|------------------------------------|
// | fX07 | Set vX to value in delay timer |
// | fX0a | Wait for input, store in vX m |
// | fX15 | Set sound timer to the value in vX |
// | fX18 | set delay timer to the value in vX |
// | fX1e | Add x to I |
// | fX29 | Load sprite for character x into I |
// | fX33 | BCD convert X into I[0..3] |
// | fX55 | DMA Stor from I to registers 0..X |
// | fX65 | DMA Load from I to registers 0..X |
0xf => match b {
0x07 => self.get_delay_timer(x, bus),
0x0A => self.wait_for_key(x, bus),
0x15 => self.load_delay_timer(x, bus),
0x18 => self.load_sound_timer(x, bus),
0x1E => self.add_to_indirect(x, bus),
0x29 => self.load_sprite_x(x, bus),
0x33 => self.bcd_convert_i(x, bus),
0x55 => self.dma_store(x, bus),
0x65 => self.dma_load(x, bus),
_ => self.unimplemented(opcode),
},
_ => unimplemented!("Extracted nibble from byte, got >nibble?"),
}
self.cycle += 1;
}
pub fn dump(&self) {
let dumpstyle = owo_colors::Style::new().bright_black();
let mut dump = format!(
"PC: {:04x}, SP: {:04x}, I: {:04x}\n",
self.pc, self.sp, self.i
);
for (i, gpr) in self.v.into_iter().enumerate() {
dump += &format!(
"V{i:x}: {:02x} {}",
gpr,
match i % 4 {
3 => "\n",
_ => "",
}
)
}
dump += &format!("DLY: {}, SND: {}", self.delay, self.sound);
std::println!("{}", dump.style(dumpstyle));
}
}
impl Default for CPU {
fn default() -> Self {
CPUBuilder::new().build()
}
}
// private implementation
impl CPU {
/// Unused instructions
#[inline]
fn unimplemented(&self, opcode: u16) {
unimplemented!("Opcode: {opcode:04x}")
}
/// 0aaa: Handles a "machine language function call" (lmao)
#[inline]
fn sys(&mut self, a: Adr) {
unimplemented!("SYS\t{a:03x}");
}
/// 00e0: Clears the screen memory to 0
#[inline]
fn clear_screen(&mut self, bus: &mut Bus) {
for addr in self.screen..self.screen + 0x100 {
bus.write(addr, 0u8);
}
//use dump::BinDumpable;
//bus.bin_dump(self.screen as usize..self.screen as usize + 0x100);
}
/// 00ee: Returns from subroutine
#[inline]
fn ret(&mut self, bus: &mut Bus) {
self.sp = self.sp.wrapping_add(2);
self.pc = bus.read(self.sp);
}
/// 1aaa: Sets the program counter to an absolute address
#[inline]
fn jump(&mut self, a: Adr) {
self.pc = a;
}
/// 2aaa: Pushes pc onto the stack, then jumps to a
#[inline]
fn call(&mut self, a: Adr, bus: &mut Bus) {
bus.write(self.sp, self.pc);
self.sp = self.sp.wrapping_sub(2);
self.pc = a;
}
/// 3xbb: Skips the next instruction if register X == b
#[inline]
fn skip_if_x_equal_byte(&mut self, x: Reg, b: u8) {
if self.v[x] == b {
self.pc = self.pc.wrapping_add(2);
}
}
/// 4xbb: Skips the next instruction if register X != b
#[inline]
fn skip_if_x_not_equal_byte(&mut self, x: Reg, b: u8) {
if self.v[x] != b {
self.pc = self.pc.wrapping_add(2);
}
}
/// 5xy0: Skips the next instruction if register X != register Y
#[inline]
fn skip_if_x_equal_y(&mut self, x: Reg, y: Reg) {
if self.v[x] == self.v[y] {
self.pc = self.pc.wrapping_add(2);
}
}
/// 6xbb: Loads immediate byte b into register vX
#[inline]
fn load_immediate(&mut self, x: Reg, b: u8) {
self.v[x] = b;
}
/// 7xbb: Adds immediate byte b to register vX
#[inline]
fn add_immediate(&mut self, x: Reg, b: u8) {
self.v[x] = self.v[x].wrapping_add(b);
}
/// Set the carry register (vF) after math
#[inline]
fn set_carry(&mut self, x: Reg, y: Reg, f: fn(u16, u16) -> u16) -> u8 {
let sum = f(self.v[x] as u16, self.v[y] as u16);
self.v[0xf] = if sum & 0xff00 != 0 { 1 } else { 0 };
(sum & 0xff) as u8
}
/// 8xy0: Loads the value of y into x
#[inline]
fn load_y_into_x(&mut self, x: Reg, y: Reg) {
self.v[x] = self.v[y];
}
/// 8xy1: Performs bitwise or of vX and vY, and stores the result in vX
#[inline]
fn x_orequals_y(&mut self, x: Reg, y: Reg) {
self.v[x] |= self.v[y];
}
/// 8xy2: Performs bitwise and of vX and vY, and stores the result in vX
#[inline]
fn x_andequals_y(&mut self, x: Reg, y: Reg) {
self.v[x] &= self.v[y];
}
/// 8xy3: Performs bitwise xor of vX and vY, and stores the result in vX
#[inline]
fn x_xorequals_y(&mut self, x: Reg, y: Reg) {
self.v[x] ^= self.v[y];
}
/// 8xy4: Performs addition of vX and vY, and stores the result in vX
#[inline]
fn x_addequals_y(&mut self, x: Reg, y: Reg) {
self.v[x] = self.set_carry(x, y, u16::wrapping_add);
}
/// 8xy5: Performs subtraction of vX and vY, and stores the result in vX
#[inline]
fn x_subequals_y(&mut self, x: Reg, y: Reg) {
self.v[x] = self.set_carry(x, y, u16::wrapping_sub);
}
/// 8xy6: Performs bitwise right shift of vX
#[inline]
fn shift_right_x(&mut self, x: Reg) {
self.v[x] >>= 1;
}
/// 8xy7: Performs subtraction of vY and vX, and stores the result in vX
#[inline]
fn backwards_subtract(&mut self, x: Reg, y: Reg) {
self.v[x] = self.set_carry(y, x, u16::wrapping_sub);
}
/// 8X_E: Performs bitwise left shift of vX
#[inline]
fn shift_left_x(&mut self, x: Reg) {
let shift_out: u8 = self.v[x] >> 7;
self.v[x] <<= 1;
self.v[0xf] = shift_out;
}
/// 9xy0: Skip next instruction if X != y
#[inline]
fn skip_if_x_not_equal_y(&mut self, x: Reg, y: Reg) {
if self.v[x] != self.v[y] {
self.pc = self.pc.wrapping_add(2);
}
}
/// Aadr: Load address #adr into register I
#[inline]
fn load_indirect_register(&mut self, a: Adr) {
self.i = a;
}
/// Badr: Jump to &adr + v0
#[inline]
fn jump_indexed(&mut self, a: Adr) {
self.pc = a.wrapping_add(self.v[0] as Adr);
}
/// Cxbb: Stores a random number + the provided byte into vX
/// Pretty sure the input byte is supposed to be the seed of a LFSR or something
#[inline]
fn rand(&mut self, x: Reg, b: u8) {
// TODO: Random Number Generator
todo!("{}", format_args!("rand\t#{b:X}, v{x:x}").red());
}
/// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY)
#[inline]
fn draw(&mut self, x: Reg, y: Reg, n: Nib) {
// TODO: Screen
todo!("{}", format_args!("draw\t#{n:x}, v{x:x}, v{y:x}").red());
// TODO: Repeat for all N
// TODO: Calculate the lower bound address based on the X,Y position on the screen
// TODO: Read a u16 from the bus containing the two bytes which might need to be updated
}
/// Ex9E: Skip next instruction if key == #X
#[inline]
fn skip_if_key_equals_x(&mut self, x: Reg) {
std::println!("{}", format_args!("sek\tv{x:x}"));
if self.keys >> x & 1 == 1 {
std::println!("KEY == {x}");
self.pc += 2;
}
}
/// ExaE: Skip next instruction if key != #X
#[inline]
fn skip_if_key_not_x(&mut self, x: Reg) {
std::println!("{}", format_args!("snek\tv{x:x}"));
if self.keys >> x & 1 == 0 {
std::println!("KEY != {x}");
self.pc += 2;
}
}
/// Fx07: Get the current DT, and put it in vX
/// ```py
/// vX = DT
/// ```
#[inline]
fn get_delay_timer(&mut self, x: Reg, _bus: &mut Bus) {
self.v[x] = self.delay;
}
/// Fx0A: Wait for key, then vX = K
#[inline]
fn wait_for_key(&mut self, x: Reg, _bus: &mut Bus) {
// TODO: I/O
std::println!("{}", format_args!("waitk\tv{x:x}").red());
}
/// Fx15: Load vX into DT
/// ```py
/// DT = vX
/// ```
#[inline]
fn load_delay_timer(&mut self, x: Reg, _bus: &mut Bus) {
self.delay = self.v[x];
}
/// Fx18: Load vX into ST
/// ```py
/// ST = vX;
/// ```
#[inline]
fn load_sound_timer(&mut self, x: Reg, _bus: &mut Bus) {
self.sound = self.v[x];
}
/// Fx1e: Add vX to I,
/// ```py
/// I += vX;
/// ```
#[inline]
fn add_to_indirect(&mut self, x: Reg, _bus: &mut Bus) {
self.i += self.v[x] as u16;
}
/// Fx29: Load sprite for character x into I
/// ```py
/// I = sprite(X);
/// ```
#[inline]
fn load_sprite_x(&mut self, x: Reg, _bus: &mut Bus) {
self.i = self.font + (5 * x as Adr);
}
/// Fx33: BCD convert X into I`[0..3]`
#[inline]
fn bcd_convert_i(&mut self, x: Reg, _bus: &mut Bus) {
// TODO: I/O
std::println!("{}", format_args!("bcd\t{x:x}, &I").red());
}
/// Fx55: DMA Stor from I to registers 0..X
#[inline]
fn dma_store(&mut self, x: Reg, bus: &mut Bus) {
for reg in 0..=x {
bus.write(self.i + reg as u16, self.v[reg]);
}
self.i += x as Adr + 1;
}
/// Fx65: DMA Load from I to registers 0..X
#[inline]
fn dma_load(&mut self, x: Reg, bus: &mut Bus) {
for reg in 0..=x {
self.v[reg] = bus.read(self.i + reg as u16);
}
self.i += x as Adr + 1;
}
}

400
src/cpu/disassemble.rs Normal file
View File

@ -0,0 +1,400 @@
//! A disassembler for Chip-8 opcodes
use owo_colors::{OwoColorize, Style};
use super::{Adr, Nib, Reg};
type Ins = Nib;
#[inline]
pub fn i(ins: u16) -> Ins {
(ins >> 12 & 0xf) as Ins
}
#[inline]
pub fn x(ins: u16) -> Reg {
(ins >> 8 & 0xf) as Reg
}
#[inline]
pub fn y(ins: u16) -> Reg {
(ins >> 4 & 0xf) as Reg
}
#[inline]
pub fn n(ins: u16) -> Nib {
(ins & 0xf) as Nib
}
#[inline]
pub fn b(ins: u16) -> u8 {
(ins & 0xff) as u8
}
#[inline]
pub fn a(ins: u16) -> Adr {
ins & 0x0fff
}
#[derive(Clone, Debug, PartialEq)]
pub struct Disassemble {
invalid: Style,
normal: Style,
}
impl Default for Disassemble {
fn default() -> Self {
Disassemble::builder().build()
}
}
// Public API
impl Disassemble {
// Returns a new Disassemble with the provided Styles
pub fn new(invalid: Style, normal: Style) -> Disassemble {
Disassemble { invalid, normal }
}
//
pub fn builder() -> DisassembleBuilder {
DisassembleBuilder::default()
}
// Disassemble a single instruction
pub fn instruction(&self, opcode: u16) -> String {
let (i, x, y, n, b, a) = (
i(opcode),
x(opcode),
y(opcode),
n(opcode),
b(opcode),
a(opcode),
);
match i {
// # Issue a system call
// |opcode| effect |
// |------|------------------------------------|
// | 00e0 | Clear screen memory to all 0 |
// | 00ee | Return from subroutine |
0x0 => match a {
0x0e0 => self.clear_screen(),
0x0ee => self.ret(),
_ => self.sys(a),
},
// | 1aaa | Sets pc to an absolute address
0x1 => self.jump(a),
// | 2aaa | Pushes pc onto the stack, then jumps to a
0x2 => self.call(a),
// | 3xbb | Skips next instruction if register X == b
0x3 => self.skip_if_x_equal_byte(x, b),
// | 4xbb | Skips next instruction if register X != b
0x4 => self.skip_if_x_not_equal_byte(x, b),
// # Performs a register-register comparison
// |opcode| effect |
// |------|------------------------------------|
// | 9XY0 | Skip next instruction if vX == vY |
0x5 => match n {
0x0 => self.skip_if_x_equal_y(x, y),
_ => self.unimplemented(opcode),
},
// 6xbb: Loads immediate byte b into register vX
0x6 => self.load_immediate(x, b),
// 7xbb: Adds immediate byte b to register vX
0x7 => self.add_immediate(x, b),
// # Performs ALU operation
// |opcode| effect |
// |------|------------------------------------|
// | 8xy0 | Y = X |
// | 8xy1 | X = X | Y |
// | 8xy2 | X = X & Y |
// | 8xy3 | X = X ^ Y |
// | 8xy4 | X = X + Y; Set vF=carry |
// | 8xy5 | X = X - Y; Set vF=carry |
// | 8xy6 | X = X >> 1 |
// | 8xy7 | X = Y - X; Set vF=carry |
// | 8xyE | X = X << 1 |
0x8 => match n {
0x0 => self.load_y_into_x(x, y),
0x1 => self.x_orequals_y(x, y),
0x2 => self.x_andequals_y(x, y),
0x3 => self.x_xorequals_y(x, y),
0x4 => self.x_addequals_y(x, y),
0x5 => self.x_subequals_y(x, y),
0x6 => self.shift_right_x(x),
0x7 => self.backwards_subtract(x, y),
0xE => self.shift_left_x(x),
_ => self.unimplemented(opcode),
},
// # Performs a register-register comparison
// |opcode| effect |
// |------|------------------------------------|
// | 9XY0 | Skip next instruction if vX != vY |
0x9 => match n {
0 => self.skip_if_x_not_equal_y(x, y),
_ => self.unimplemented(opcode),
},
// Aaaa: Load address #a into register I
0xa => self.load_indirect_register(a),
// Baaa: Jump to &adr + v0
0xb => self.jump_indexed(a),
// Cxbb: Stores a random number + the provided byte into vX
0xc => self.rand(x, b),
// Dxyn: Draws n-byte sprite to the screen at coordinates (vX, vY)
0xd => self.draw(x, y, n),
// # Skips instruction on value of keypress
// |opcode| effect |
// |------|------------------------------------|
// | eX9e | Skip next instruction if key == #X |
// | eXa1 | Skip next instruction if key != #X |
0xe => match b {
0x9e => self.skip_if_key_equals_x(x),
0xa1 => self.skip_if_key_not_x(x),
_ => self.unimplemented(opcode),
},
// # Performs IO
// |opcode| effect |
// |------|------------------------------------|
// | fX07 | Set vX to value in delay timer |
// | fX0a | Wait for input, store in vX m |
// | fX15 | Set sound timer to the value in vX |
// | fX18 | set delay timer to the value in vX |
// | fX1e | Add x to I |
// | fX29 | Load sprite for character x into I |
// | fX33 | BCD convert X into I[0..3] |
// | fX55 | DMA Stor from I to registers 0..X |
// | fX65 | DMA Load from I to registers 0..X |
0xf => match b {
0x07 => self.get_delay_timer(x),
0x0A => self.wait_for_key(x),
0x15 => self.load_delay_timer(x),
0x18 => self.load_sound_timer(x),
0x1E => self.add_to_indirect(x),
0x29 => self.load_sprite_x(x),
0x33 => self.bcd_convert_i(x),
0x55 => self.dma_store(x),
0x65 => self.dma_load(x),
_ => self.unimplemented(opcode),
},
_ => unimplemented!("Extracted nibble from byte, got >nibble?"),
}
}
}
// Private api
impl Disassemble {
/// Unused instructions
fn unimplemented(&self, opcode: u16) -> String {
format!("inval\t{opcode:04x}")
.style(self.invalid)
.to_string()
}
/// `0aaa`: Handles a "machine language function call" (lmao)
pub fn sys(&self, a: Adr) -> String {
format!("sysc\t{a:03x}").style(self.invalid).to_string()
}
/// `00e0`: Clears the screen memory to 0
pub fn clear_screen(&self) -> String {
"cls".style(self.normal).to_string()
}
/// `00ee`: Returns from subroutine
pub fn ret(&self) -> String {
"ret".style(self.normal).to_string()
}
/// `1aaa`: Sets the program counter to an absolute address
pub fn jump(&self, a: Adr) -> String {
format!("jmp\t{a:03x}").style(self.normal).to_string()
}
/// `2aaa`: Pushes pc onto the stack, then jumps to a
pub fn call(&self, a: Adr) -> String {
format!("call\t{a:03x}").style(self.normal).to_string()
}
/// `3xbb`: Skips the next instruction if register X == b
pub fn skip_if_x_equal_byte(&self, x: Reg, b: u8) -> String {
format!("se\t#{b:02x}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `4xbb`: Skips the next instruction if register X != b
pub fn skip_if_x_not_equal_byte(&self, x: Reg, b: u8) -> String {
format!("sne\t#{b:02x}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `5xy0`: Skips the next instruction if register X != register Y
pub fn skip_if_x_equal_y(&self, x: Reg, y: Reg) -> String {
format!("se\tv{x:X}, v{y:X}").style(self.normal).to_string()
}
/// `6xbb`: Loads immediate byte b into register vX
pub fn load_immediate(&self, x: Reg, b: u8) -> String {
format!("mov\t#{b:02x}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `7xbb`: Adds immediate byte b to register vX
pub fn add_immediate(&self, x: Reg, b: u8) -> String {
format!("add\t#{b:02x}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `8xy0`: Loads the value of y into x
pub fn load_y_into_x(&self, x: Reg, y: Reg) -> String {
format!("mov\tv{y:X}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `8xy1`: Performs bitwise or of vX and vY, and stores the result in vX
pub fn x_orequals_y(&self, x: Reg, y: Reg) -> String {
format!("or\tv{y:X}, v{x:X}").style(self.normal).to_string()
}
/// `8xy2`: Performs bitwise and of vX and vY, and stores the result in vX
pub fn x_andequals_y(&self, x: Reg, y: Reg) -> String {
format!("and\tv{y:X}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `8xy3`: Performs bitwise xor of vX and vY, and stores the result in vX
pub fn x_xorequals_y(&self, x: Reg, y: Reg) -> String {
format!("xor\tv{y:X}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `8xy4`: Performs addition of vX and vY, and stores the result in vX
pub fn x_addequals_y(&self, x: Reg, y: Reg) -> String {
format!("add\tv{y:X}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `8xy5`: Performs subtraction of vX and vY, and stores the result in vX
pub fn x_subequals_y(&self, x: Reg, y: Reg) -> String {
format!("sub\tv{y:X}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `8xy6`: Performs bitwise right shift of vX
pub fn shift_right_x(&self, x: Reg) -> String {
format!("shr\tv{x:X}").style(self.normal).to_string()
}
/// `8xy7`: Performs subtraction of vY and vX, and stores the result in vX
pub fn backwards_subtract(&self, x: Reg, y: Reg) -> String {
format!("bsub\tv{y:X}, v{x:X}")
.style(self.normal)
.to_string()
}
/// 8X_E: Performs bitwise left shift of vX
pub fn shift_left_x(&self, x: Reg) -> String {
format!("shl\tv{x:X}").style(self.normal).to_string()
}
/// `9xy0`: Skip next instruction if X != y
pub fn skip_if_x_not_equal_y(&self, x: Reg, y: Reg) -> String {
format!("sn\tv{x:X}, v{y:X}").style(self.normal).to_string()
}
/// Aadr: Load address #adr into register I
pub fn load_indirect_register(&self, a: Adr) -> String {
format!("mov\t${a:03x}, I").style(self.normal).to_string()
}
/// Badr: Jump to &adr + v0
pub fn jump_indexed(&self, a: Adr) -> String {
format!("jmp\t${a:03x}+v0").style(self.normal).to_string()
}
/// `Cxbb`: Stores a random number + the provided byte into vX
/// Pretty sure the input byte is supposed to be the seed of a LFSR or something
pub fn rand(&self, x: Reg, b: u8) -> String {
format!("rand\t#{b:X}, v{x:X}")
.style(self.normal)
.to_string()
}
/// `Dxyn`: Draws n-byte sprite to the screen at coordinates (vX, vY)
pub fn draw(&self, x: Reg, y: Reg, n: Nib) -> String {
#[rustfmt::skip]
format!("draw\t#{n:x}, v{x:X}, v{y:X}").style(self.normal).to_string()
}
/// `Ex9E`: Skip next instruction if key == #X
pub fn skip_if_key_equals_x(&self, x: Reg) -> String {
format!("sek\tv{x:X}").style(self.normal).to_string()
}
/// `ExaE`: Skip next instruction if key != #X
pub fn skip_if_key_not_x(&self, x: Reg) -> String {
format!("snek\tv{x:X}").style(self.normal).to_string()
}
/// `Fx07`: Get the current DT, and put it in vX
/// ```py
/// vX = DT
/// ```
pub fn get_delay_timer(&self, x: Reg) -> String {
format!("mov\tDT, v{x:X}").style(self.normal).to_string()
}
/// `Fx0A`: Wait for key, then vX = K
pub fn wait_for_key(&self, x: Reg) -> String {
format!("waitk\tv{x:X}").style(self.normal).to_string()
}
/// `Fx15`: Load vX into DT
/// ```py
/// DT = vX
/// ```
pub fn load_delay_timer(&self, x: Reg) -> String {
format!("ld\tv{x:X}, DT").style(self.normal).to_string()
}
/// `Fx18`: Load vX into ST
/// ```py
/// ST = vX;
/// ```
pub fn load_sound_timer(&self, x: Reg) -> String {
format!("ld\tv{x:X}, ST").style(self.normal).to_string()
}
/// `Fx1e`: Add vX to I,
/// ```py
/// I += vX;
/// ```
pub fn add_to_indirect(&self, x: Reg) -> String {
format!("add\tv{x:X}, I").style(self.normal).to_string()
}
/// `Fx29`: Load sprite for character x into I
/// ```py
/// I = sprite(X);
/// ```
pub fn load_sprite_x(&self, x: Reg) -> String {
format!("font\t#{x:X}, I").style(self.normal).to_string()
}
/// `Fx33`: BCD convert X into I`[0..3]`
pub fn bcd_convert_i(&self, x: Reg) -> String {
format!("bcd\t{x:X}, &I").style(self.normal).to_string()
}
/// `Fx55`: DMA Stor from I to registers 0..X
pub fn dma_store(&self, x: Reg) -> String {
format!("dmao\t{x:X}").style(self.normal).to_string()
}
/// `Fx65`: DMA Load from I to registers 0..X
pub fn dma_load(&self, x: Reg) -> String {
format!("dmai\t{x:X}").style(self.normal).to_string()
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DisassembleBuilder {
invalid: Option<Style>,
normal: Option<Style>,
}
impl DisassembleBuilder {
/// Styles invalid (or unimplemented) instructions
pub fn invalid(mut self, style: Style) -> Self {
self.invalid = Some(style);
self
}
/// Styles valid (implemented) instructions
pub fn normal(mut self, style: Style) -> Self {
self.normal = Some(style);
self
}
/// Builds a Disassemble
pub fn build(self) -> Disassemble {
Disassemble {
invalid: if let Some(style) = self.invalid {
style
} else {
Style::new().bold().red()
},
normal: if let Some(style) = self.normal {
style
} else {
Style::new().green()
},
}
}
}

63
src/dump.rs Normal file
View File

@ -0,0 +1,63 @@
//! Dumps data to stdout
use std::ops::Range;
/// Prints a hexdump of a range within the `Dumpable`
///
/// # Examples
/// ```rust
/// # use rumpulator::prelude::*;
/// let mem = Mem::new(0x50);
/// // Dumps the first 0x10 bytes
/// mem.dump(0x00..0x10);
/// ```
pub trait Dumpable {
/// Prints a hexdump of a range within the object
fn dump(&self, range: Range<usize>);
}
/// Prints a binary dump of a range within the `Dumpable`
///
/// # Examples
/// ```rust
/// # use rumpulator::prelude::*;
/// let mem = bus! {
/// "mem" [0..0x10] = Mem::new(0x10)
/// };
/// // Dumps the first 0x10 bytes
/// mem.bin_dump(0x00..0x10);
/// ```
pub trait BinDumpable {
/// Prints a binary dump of a range within the object
fn bin_dump(&self, _range: Range<usize>) {}
}
pub fn as_hexdump(index: usize, byte: u8) {
use owo_colors::OwoColorize;
let term: owo_colors::Style = owo_colors::Style::new().bold().green().on_black();
if index % 2 == 0 {
print!(" ")
}
if index % 8 == 0 {
print!(" ")
}
if index % 16 == 0 {
print!("{:>03x}{} ", index.style(term), ":".style(term));
}
print!("{byte:02x}");
if index % 16 == 0xf {
println!()
}
}
pub fn as_bindump(index: usize, byte: u8) {
use owo_colors::OwoColorize;
let term: owo_colors::Style = owo_colors::Style::new().bold().green().on_black();
if index % 8 == 0 {
print!("{:>03x}{} ", index.style(term), ":".style(term));
}
print!("{byte:08b} ");
if index % 8 == 7 {
println!()
}
}

12
src/error.rs Normal file
View File

@ -0,0 +1,12 @@
//! Error type for rumpulator
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Debug, Error, PartialEq, Eq, PartialOrd, Ord)]
pub enum Error {
#[error("Unrecognized opcode {word}")]
UnimplementedInstruction { word: u16 },
#[error("Math was funky when parsing {word}: {explanation}")]
FunkyMath { word: u16, explanation: String },
}

27
src/lib.rs Normal file
View File

@ -0,0 +1,27 @@
#![feature(let_chains, stmt_expr_attributes)]
/*!
This crate implements a Chip-8 interpreter as if it were a real CPU architecture,
to the best of my current knowledge. As it's the first emulator project I've
embarked on, and I'm fairly new to Rust, it's going to be rough around the edges.
Hopefully, though, you'll find some use in it.
*/
pub mod bus;
pub mod cpu;
pub mod mem;
pub mod dump;
pub mod error;
pub mod screen;
/// Common imports for rumpulator
pub mod prelude {
use super::*;
pub use crate::bus;
pub use bus::{Bus, BusConnectible};
pub use cpu::{disassemble::Disassemble, CPU};
pub use dump::{BinDumpable, Dumpable};
pub use mem::Mem;
pub use screen::Screen;
}

31
src/main.rs Normal file
View File

@ -0,0 +1,31 @@
use rumpulator::{bus::Read, prelude::*};
use std::fs::read;
fn main() -> Result<(), std::io::Error> {
let mut bus = bus! {
// Load the charset into ROM
"charset" [0x0050..0x00a0] = Mem::new(0x50).load_charset(0).w(false),
// Load the ROM file into RAM
"userram" [0x0200..0x0F00] = Mem::new(0xF00 - 0x200).load(0, &read("chip-8/Fishie.ch8")?),
// Create a screen
"screen" [0x0F00..0x1000] = Screen::new(32, 64),
// Create some stack memory
"stack" [0xF000..0xF800] = Mem::new(0x800).r(true).w(true),
};
println!("{bus}");
let disassembler = Disassemble::default();
for addr in 0x200..0x290 {
if addr % 2 == 0 {
println!("{addr:03x}: {}", disassembler.instruction(bus.read(addr)));
}
}
let mut cpu = CPU::new(0xf00, 0x50, 0x200, 0xf7fe, disassembler);
for _instruction in 0..100 {
cpu.tick(&mut bus);
//bus.dump(0xF7e0..0xf800);
}
Ok(())
}

147
src/mem.rs Normal file
View File

@ -0,0 +1,147 @@
//! Mem covers WOM, ROM, and RAM
use crate::{bus::BusConnectible, dump::Dumpable};
use owo_colors::{OwoColorize, Style};
use std::{
fmt::{Display, Formatter, Result},
ops::Range,
};
const MSIZE: usize = 0x1000;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Attr {
pub r: bool,
pub w: bool,
}
pub struct MemWindow<'a> {
mem: &'a [u8],
}
impl<'a> Display for MemWindow<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
// Green phosphor style formatting, for taste
let term: Style = Style::new().bold().green().on_black();
for (index, byte) in self.mem.iter().enumerate() {
if index % 16 == 0 {
write!(f, "{:>03x}{} ", index.style(term), ":".style(term))?
}
write!(f, "{byte:02x}")?;
write!(
f,
"{}",
match index % 16 {
0xf => "\n",
0x7 => " ",
_ if index % 2 == 1 => " ",
_ => "",
}
)?
}
write!(f, "")
}
}
/// Represents some kind of abstract memory chip
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Mem {
mem: Vec<u8>,
attr: Attr,
}
impl Mem {
pub fn r(mut self, readable: bool) -> Self {
self.attr.r = readable;
self
}
pub fn w(mut self, writable: bool) -> Self {
self.attr.w = writable;
self
}
/// Returns the number of bytes in the `Mem`, also referred to as its length.
///
/// # Examples
/// ``` rust
/// # use rumpulator::prelude::*;
/// let mem = Mem::new(0x100);
/// assert_eq!(mem.len(), 0x100)
/// ```
pub fn len(&self) -> usize {
self.mem.len()
}
/// Because clippy is so kind:
pub fn is_empty(&self) -> bool {
self.mem.is_empty()
}
/// Loads data into a `Mem`
pub fn load(mut self, addr: u16, bytes: &[u8]) -> Self {
let addr = addr as usize;
let end = self.mem.len().min(addr + bytes.len());
self.mem[addr..end].copy_from_slice(&bytes[0..bytes.len()]);
self
}
/// Load a character set from rumpulator/src/mem/charset.bin into this memory section
pub fn load_charset(self, addr: u16) -> Self {
let charset = include_bytes!("mem/charset.bin");
self.load(addr, charset)
}
/// Creates a new `mem` with the specified length
///
/// # Examples
/// ```rust
/// # use rumpulator::prelude::*;
/// let length = 0x100;
/// let mem = Mem::new(length);
/// ```
pub fn new(len: usize) -> Self {
Mem {
mem: vec![0; len],
attr: Attr { r: true, w: true },
}
}
/// Creates a window into the Mem which implements Display
pub fn window(&self, range: Range<usize>) -> MemWindow {
MemWindow {
mem: &self.mem[range],
}
}
}
impl Default for Mem {
fn default() -> Self {
Self::new(MSIZE)
}
}
impl Display for Mem {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", self.window(0..self.len()))
}
}
impl Dumpable for Mem {
fn dump(&self, range: Range<usize>) {
print!("Mem {range:2x?}: ");
print!("{}", self.window(range));
}
}
impl BusConnectible for Mem {
fn read_at(&self, addr: u16) -> Option<u8> {
if !self.attr.r {
return None;
}
self.mem.get(addr as usize).copied()
}
fn write_to(&mut self, addr: u16, data: u8) {
if self.attr.w && let Some(value) = self.mem.get_mut(addr as usize) {
*value = data
}
}
}

1
src/mem/charset.bin Normal file
View File

@ -0,0 +1 @@
ð<EFBFBD><EFBFBD><EFBFBD>ð ` pðð€ðððð<><C3B0>ðð€ððð€ð<E282AC>ðð @@ð<>ð<EFBFBD>ðð<C3B0>ððð<C3B0>ð<EFBFBD><C3B0>à<EFBFBD>à<EFBFBD>àð€€€ðà<C3B0><C3A0><EFBFBD>àð€ð€ðð€ð€€

46
src/screen.rs Normal file
View File

@ -0,0 +1,46 @@
//! Stores and displays the Chip-8's screen memory
use crate::{bus::BusConnectible, dump::Dumpable, mem::Mem};
use std::{
fmt::{Display, Formatter, Result},
ops::Range,
};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Screen {
mem: Mem,
width: usize,
height: usize,
}
impl Screen {
pub fn new(width: usize, height: usize) -> Screen {
Screen {
mem: Mem::new(width * height / 8),
width,
height,
}
}
}
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))
}
}