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:
commit
a721a00232
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/target
|
||||||
|
# Chip-8 test ROMs
|
||||||
|
/chip-8
|
||||||
|
|
||||||
|
# VS Code files
|
||||||
|
/.vscode
|
93
Cargo.lock
generated
Normal file
93
Cargo.lock
generated
Normal 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
11
Cargo.toml
Normal 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
157
src/bus.rs
Normal 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
51
src/bus/bus_device.rs
Normal 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
525
src/cpu.rs
Normal 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
400
src/cpu/disassemble.rs
Normal 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
63
src/dump.rs
Normal 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
12
src/error.rs
Normal 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
27
src/lib.rs
Normal 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
31
src/main.rs
Normal 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
147
src/mem.rs
Normal 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
1
src/mem/charset.bin
Normal 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
46
src/screen.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user