Compare commits
45 Commits
main
...
mem-refact
Author | SHA1 | Date | |
---|---|---|---|
8d2399e431 | |||
39fe4411d6 | |||
7ab0a596c9 | |||
8de4f42ff6 | |||
31c904f89c | |||
f886aadc63 | |||
bcd5499833 | |||
5ae98346a0 | |||
e4f8001a16 | |||
37b8b96683 | |||
861020a8ab | |||
dcbdd1383d | |||
96b6038bbe | |||
f4d7e514bc | |||
beab7c968b | |||
33b6d0218e | |||
c7226bf9cc | |||
4ec9e2cb71 | |||
8b4d5be49d | |||
cb69af048f | |||
57c2ac681c | |||
ea357be477 | |||
a16d7fe732 | |||
7d5718f384 | |||
05fee3fb75 | |||
16a5e6a2a4 | |||
5b5c5b41ab | |||
04736f1153 | |||
59ba8ac20b | |||
e9f8d917a4 | |||
c1219e60f0 | |||
33da1089a2 | |||
92dc899510 | |||
3839e90b15 | |||
1c1d4dafaf | |||
0113dd95f6 | |||
45adf0a2b8 | |||
e842755d77 | |||
88693f6c72 | |||
381b2a69bd | |||
1573e00928 | |||
401b247c05 | |||
95d4751cdd | |||
7d25a9f5f1 | |||
bafb576705 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
|||||||
|
|
||||||
# Rust files
|
# Rust files
|
||||||
|
**/target
|
||||||
|
**/Cargo.lock
|
||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
flamegraph.svg
|
flamegraph.svg
|
||||||
|
54
Cargo.toml
54
Cargo.toml
@ -1,22 +1,21 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "chirp"
|
members = ["chirp-imgui", "chirp-minifb"]
|
||||||
|
# default-members = ["chirp-imgui"]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
ignore = ["justfile", ".gitmodules", "chip8-test-suite", "chip8Archive"]
|
|
||||||
default-run = "chirp"
|
|
||||||
authors = ["John Breaux"]
|
authors = ["John Breaux"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
[package]
|
||||||
[features]
|
name = "chirp"
|
||||||
default = ["unstable", "drawille", "minifb"]
|
version.workspace = true
|
||||||
unstable = []
|
edition.workspace = true
|
||||||
drawille = ["dep:drawille"]
|
authors.workspace = true
|
||||||
iced = ["dep:iced"]
|
license.workspace = true
|
||||||
minifb = ["dep:minifb"]
|
publish.workspace = true
|
||||||
rhexdump = ["dep:rhexdump"]
|
|
||||||
serde = ["dep:serde"]
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "chirp"
|
name = "chirp"
|
||||||
@ -27,14 +26,17 @@ required-features = ["minifb"]
|
|||||||
name = "chirp-disasm"
|
name = "chirp-disasm"
|
||||||
required-features = ["default"]
|
required-features = ["default"]
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "chirp-iced"
|
|
||||||
required-features = ["iced"]
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "chirp-shot-viewer"
|
name = "chirp-shot-viewer"
|
||||||
required-features = ["default", "drawille"]
|
required-features = ["default", "drawille"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["drawille", "serde"]
|
||||||
|
nightly = []
|
||||||
|
drawille = ["dep:drawille"]
|
||||||
|
minifb = ["dep:minifb"]
|
||||||
|
rhexdump = ["dep:rhexdump"]
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
@ -49,14 +51,14 @@ overflow-checks = false
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
drawille = {version = "0.3.0", optional = true}
|
|
||||||
iced = {version = "0.8.0", optional = true}
|
|
||||||
rhexdump = {version = "^0.1.1", optional = true }
|
|
||||||
serde = { version = "^1.0", features = ["derive"], optional = true }
|
|
||||||
minifb = { version = "^0.24.0", optional = true }
|
|
||||||
|
|
||||||
gumdrop = "^0.8.1"
|
drawille = { version = "0.3.0", optional = true }
|
||||||
|
rhexdump = { version = "0.1.1", optional = true }
|
||||||
|
serde = { version = "1.0.0", features = ["derive"], optional = true }
|
||||||
|
minifb = { version = "0.24.0", optional = true }
|
||||||
|
|
||||||
|
gumdrop = "0.8.1"
|
||||||
imperative-rs = "0.3.1"
|
imperative-rs = "0.3.1"
|
||||||
owo-colors = "^3"
|
owo-colors = "3"
|
||||||
rand = "^0.8.5"
|
rand = "0.8.5"
|
||||||
thiserror = "^1.0.39"
|
thiserror = "1.0.39"
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit af7829841b4c9b043febbd37a4d4114e0ac948ec
|
Subproject commit 253f5de130a6cc4c7e07c6801174717efb0ea9cb
|
@ -1 +1 @@
|
|||||||
Subproject commit 483a642595b025a863f2e4304384a370098a3344
|
Subproject commit 6e1f4025df7c46ef5bdc222262b1e38208cf7c0f
|
23
chirp-imgui/Cargo.toml
Normal file
23
chirp-imgui/Cargo.toml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "chirp-imgui"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
chirp = { path = ".." }
|
||||||
|
|
||||||
|
gumdrop = "0.8.1"
|
||||||
|
owo-colors = "3"
|
||||||
|
thiserror = "1.0.39"
|
||||||
|
|
||||||
|
imgui = { version = "^0.11" }
|
||||||
|
imgui-winit-support = { version = "^0.11" }
|
||||||
|
imgui-wgpu = { version = "^0.23" }
|
||||||
|
pixels = { version = "^0" }
|
||||||
|
# TODO: When imgui-winit-support updates to 0.28 (Soon:tm:), update winit and winit_input_helper
|
||||||
|
winit = { version = "0.27.5", features = ["default", "x11"] }
|
||||||
|
winit_input_helper = { version = "^0.13.0" }
|
52
chirp-imgui/src/args.rs
Normal file
52
chirp-imgui/src/args.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
//! Parses arguments into a struct
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::Emulator;
|
||||||
|
use chirp::Mode;
|
||||||
|
use gumdrop::*;
|
||||||
|
|
||||||
|
/// Parses a hexadecimal string into a u16
|
||||||
|
fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> {
|
||||||
|
u16::from_str_radix(value, 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)]
|
||||||
|
pub struct Arguments {
|
||||||
|
#[options(help = "Load a ROM to run on Chirp.", required, free)]
|
||||||
|
pub file: PathBuf,
|
||||||
|
#[options(help = "Print this help message.")]
|
||||||
|
help: bool,
|
||||||
|
#[options(help = "Enable debug mode at startup.")]
|
||||||
|
pub debug: bool,
|
||||||
|
#[options(help = "Enable pause mode at startup.")]
|
||||||
|
pub pause: bool,
|
||||||
|
#[options(help = "Set the instructions-per-frame rate.")]
|
||||||
|
pub speed: Option<usize>,
|
||||||
|
// #[options(help = "Enable performance benchmarking on stderr (requires -S)")]
|
||||||
|
// pub perf: bool,
|
||||||
|
#[options(help = "Run in (Chip8, SChip, XOChip) mode.")]
|
||||||
|
pub mode: Option<Mode>,
|
||||||
|
#[options(help = "Set the target framerate.", default = "60", meta = "FR")]
|
||||||
|
pub frame_rate: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arguments {
|
||||||
|
pub fn parse() -> Arguments {
|
||||||
|
Arguments::parse_args_default_or_exit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Arguments> for Emulator {
|
||||||
|
fn from(value: Arguments) -> Self {
|
||||||
|
let mut emu = Emulator::new(value.speed.unwrap_or(10), value.file);
|
||||||
|
if let Some(mode) = value.mode {
|
||||||
|
emu.set_quirks(mode.into());
|
||||||
|
}
|
||||||
|
if value.pause {
|
||||||
|
emu.pause()
|
||||||
|
}
|
||||||
|
emu.set_disasm(value.debug);
|
||||||
|
emu
|
||||||
|
}
|
||||||
|
}
|
176
chirp-imgui/src/emu.rs
Normal file
176
chirp-imgui/src/emu.rs
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
//! The emulator's state, including the screen data
|
||||||
|
|
||||||
|
use super::{error, BACKGROUND, FOREGROUND};
|
||||||
|
use chirp::{Grab, Screen, CPU};
|
||||||
|
use pixels::Pixels;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use winit::event::VirtualKeyCode;
|
||||||
|
use winit_input_helper::WinitInputHelper;
|
||||||
|
|
||||||
|
/// The state of the application
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Emulator {
|
||||||
|
screen: Screen,
|
||||||
|
cpu: CPU,
|
||||||
|
pub ipf: usize,
|
||||||
|
pub rom: PathBuf,
|
||||||
|
pub colors: [[u8; 4]; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Emulator {
|
||||||
|
/// Constructs a new CPU, with the provided ROM loaded at 0x0200
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if the provided ROM does not exist
|
||||||
|
pub fn new(ipf: usize, rom: impl AsRef<Path>) -> Self {
|
||||||
|
let screen = Screen::default();
|
||||||
|
let mut cpu = CPU::default();
|
||||||
|
cpu.load_program(&rom).expect("Loaded file MUST exist.");
|
||||||
|
Self {
|
||||||
|
cpu,
|
||||||
|
ipf,
|
||||||
|
rom: rom.as_ref().into(),
|
||||||
|
screen,
|
||||||
|
colors: [*FOREGROUND, *BACKGROUND],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Runs a single epoch
|
||||||
|
pub fn update(&mut self) -> Result<(), error::Error> {
|
||||||
|
self.cpu.multistep(&mut self.screen, self.ipf)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Rasterizes the screen into a [Pixels] buffer
|
||||||
|
pub fn draw(&mut self, pixels: &mut Pixels) -> Result<(), error::Error> {
|
||||||
|
if let Some(screen) = self.screen.grab(..) {
|
||||||
|
let len_log2 = screen.len().ilog2() / 2;
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
let (width, height) = (2u32.pow(len_log2 + 2), 2u32.pow(len_log2 + 1));
|
||||||
|
pixels.resize_buffer(width, height)?;
|
||||||
|
for (idx, pixel) in pixels.frame_mut().iter_mut().enumerate() {
|
||||||
|
let (byte, bit, component) = (idx >> 5, (idx >> 2) % 8, idx & 0b11);
|
||||||
|
*pixel = if screen[byte] & (0x80 >> bit) > 0 {
|
||||||
|
self.colors[0][component]
|
||||||
|
} else {
|
||||||
|
self.colors[1][component]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Processes keyboard input for the Emulator
|
||||||
|
pub fn input(&mut self, input: &WinitInputHelper) -> Result<(), error::Error> {
|
||||||
|
const KEYMAP: [VirtualKeyCode; 16] = [
|
||||||
|
VirtualKeyCode::X,
|
||||||
|
VirtualKeyCode::Key1,
|
||||||
|
VirtualKeyCode::Key2,
|
||||||
|
VirtualKeyCode::Key3,
|
||||||
|
VirtualKeyCode::Q,
|
||||||
|
VirtualKeyCode::W,
|
||||||
|
VirtualKeyCode::E,
|
||||||
|
VirtualKeyCode::A,
|
||||||
|
VirtualKeyCode::S,
|
||||||
|
VirtualKeyCode::D,
|
||||||
|
VirtualKeyCode::Z,
|
||||||
|
VirtualKeyCode::C,
|
||||||
|
VirtualKeyCode::Key4,
|
||||||
|
VirtualKeyCode::R,
|
||||||
|
VirtualKeyCode::F,
|
||||||
|
VirtualKeyCode::V,
|
||||||
|
];
|
||||||
|
for (id, &key) in KEYMAP.iter().enumerate() {
|
||||||
|
if input.key_released(key) {
|
||||||
|
self.cpu.release(id)?;
|
||||||
|
}
|
||||||
|
if input.key_pressed(key) {
|
||||||
|
self.cpu.press(id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quirks(&mut self) -> chirp::Quirks {
|
||||||
|
self.cpu.flags.quirks
|
||||||
|
}
|
||||||
|
pub fn set_quirks(&mut self, quirks: chirp::Quirks) {
|
||||||
|
self.cpu.flags.quirks = quirks
|
||||||
|
}
|
||||||
|
/// Prints the CPU registers and cycle count to stderr
|
||||||
|
pub fn print_registers(&self) {
|
||||||
|
self.cpu.dump()
|
||||||
|
}
|
||||||
|
/// Prints the screen (using the highest resolution available printer) to stdout
|
||||||
|
pub fn print_screen(&self) -> Result<(), error::Error> {
|
||||||
|
self.screen.print_screen();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Dumps the raw screen bytes to a file named `{rom}_{cycle}.bin`,
|
||||||
|
/// or, failing that, `screen_dump.bin`
|
||||||
|
pub fn dump_screen(&self) -> Result<(), error::Error> {
|
||||||
|
let mut path = PathBuf::new().join(format!(
|
||||||
|
"{}_{}.bin",
|
||||||
|
self.rom.file_stem().unwrap_or_default().to_string_lossy(),
|
||||||
|
self.cpu.cycle()
|
||||||
|
));
|
||||||
|
path.set_extension("bin");
|
||||||
|
if std::fs::write(&path, self.screen.as_slice()).is_ok() {
|
||||||
|
eprintln!("Saved to {}", &path.display());
|
||||||
|
} else if std::fs::write("screen_dump.bin", self.screen.as_slice()).is_ok() {
|
||||||
|
eprintln!("Saved to screen_dump.bin");
|
||||||
|
} else {
|
||||||
|
eprintln!("Failed to dump screen to file.")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Sets live disassembly
|
||||||
|
pub fn set_disasm(&mut self, enabled: bool) {
|
||||||
|
self.cpu.flags.debug = enabled;
|
||||||
|
}
|
||||||
|
/// Checks live disassembly
|
||||||
|
pub fn is_disasm(&mut self) {
|
||||||
|
eprintln!(
|
||||||
|
"Live Disassembly {}abled",
|
||||||
|
if self.cpu.flags.debug { "En" } else { "Dis" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/// Toggles emulator pause
|
||||||
|
pub fn pause(&mut self) {
|
||||||
|
self.cpu.flags.pause();
|
||||||
|
eprintln!("{}aused", if self.cpu.flags.pause { "P" } else { "Unp" });
|
||||||
|
}
|
||||||
|
/// Single-steps the emulator, pausing afterward
|
||||||
|
pub fn singlestep(&mut self) -> Result<(), error::Error> {
|
||||||
|
self.cpu.singlestep(&mut self.screen)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Sets a breakpoint at the current address
|
||||||
|
pub fn set_break(&mut self) {
|
||||||
|
self.cpu.set_break(self.cpu.pc());
|
||||||
|
eprintln!("Set breakpoint at {}", self.cpu.pc());
|
||||||
|
}
|
||||||
|
/// Unsets a breakpoint at the current address
|
||||||
|
pub fn unset_break(&mut self) {
|
||||||
|
self.cpu.unset_break(self.cpu.pc());
|
||||||
|
eprintln!("Unset breakpoint at {}", self.cpu.pc());
|
||||||
|
}
|
||||||
|
/// Soft-resets the CPU, keeping the program in memory
|
||||||
|
pub fn soft_reset(&mut self) {
|
||||||
|
self.cpu.reset();
|
||||||
|
self.screen.clear();
|
||||||
|
eprintln!("Soft Reset");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new CPU with the current CPU's flags
|
||||||
|
pub fn hard_reset(&mut self) {
|
||||||
|
self.cpu.reset();
|
||||||
|
self.screen.clear();
|
||||||
|
// keep the flags
|
||||||
|
let flags = self.cpu.flags.clone();
|
||||||
|
// instantiate a completely new CPU, and reload the ROM from disk
|
||||||
|
self.cpu = CPU::default();
|
||||||
|
self.cpu.flags = flags;
|
||||||
|
self.cpu
|
||||||
|
.load_program(&self.rom)
|
||||||
|
.expect("Previously loaded ROM no longer exists (was it moved?)");
|
||||||
|
eprintln!("Hard Reset");
|
||||||
|
}
|
||||||
|
}
|
28
chirp-imgui/src/error.rs
Normal file
28
chirp-imgui/src/error.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//! Error type for chirp-imgui
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Error originated in [`chirp`]
|
||||||
|
#[error(transparent)]
|
||||||
|
Chirp(#[from] chirp::error::Error),
|
||||||
|
/// Error originated in [`std::io`]
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
/// Error originated in [`pixels`]
|
||||||
|
#[error(transparent)]
|
||||||
|
Pixels(#[from] pixels::Error),
|
||||||
|
/// Error originated in [`pixels`]
|
||||||
|
#[error(transparent)]
|
||||||
|
PixelsTexture(#[from] pixels::TextureError),
|
||||||
|
/// Error originated from [`winit::error::OsError`]
|
||||||
|
#[error(transparent)]
|
||||||
|
WinitOs(#[from] winit::error::OsError),
|
||||||
|
/// Error originated from [`winit::error::ExternalError`]
|
||||||
|
#[error(transparent)]
|
||||||
|
WinitExternal(#[from] winit::error::ExternalError),
|
||||||
|
/// Error originated from [`winit::error::NotSupportedError`]
|
||||||
|
#[error(transparent)]
|
||||||
|
WinitNotSupported(#[from] winit::error::NotSupportedError),
|
||||||
|
}
|
194
chirp-imgui/src/gui.rs
Normal file
194
chirp-imgui/src/gui.rs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
//! Represents the Dear Imgui
|
||||||
|
//!
|
||||||
|
//! Adapted from the [ImGui-winit Example]
|
||||||
|
//!
|
||||||
|
//! [ImGui-winit Example]: https://github.com/parasyte/pixels/blob/main/examples/imgui-winit/src/gui.rs
|
||||||
|
|
||||||
|
use pixels::{wgpu, PixelsContext};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
mod about;
|
||||||
|
mod menubar;
|
||||||
|
use menubar::Menubar;
|
||||||
|
|
||||||
|
/// Lays out the imgui widgets for a thing
|
||||||
|
pub trait Drawable {
|
||||||
|
// Lay out the ImGui widgets for this thing
|
||||||
|
fn draw(&mut self, ui: &imgui::Ui);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds state of GUI
|
||||||
|
pub(crate) struct Gui {
|
||||||
|
imgui: imgui::Context,
|
||||||
|
platform: imgui_winit_support::WinitPlatform,
|
||||||
|
renderer: imgui_wgpu::Renderer,
|
||||||
|
last_frame: Instant,
|
||||||
|
last_cursor: Option<imgui::MouseCursor>,
|
||||||
|
pub menubar: Menubar,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries the state of the [Gui]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Wants {
|
||||||
|
Quit,
|
||||||
|
Disasm,
|
||||||
|
SoftReset,
|
||||||
|
HardReset,
|
||||||
|
Reset,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Gui {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("Gui")
|
||||||
|
.field("imgui", &self.imgui)
|
||||||
|
.field("platform", &self.platform)
|
||||||
|
.field("last_frame", &self.last_frame)
|
||||||
|
.field("last_cursor", &self.last_cursor)
|
||||||
|
.field("menubar", &self.menubar)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gui {
|
||||||
|
pub fn new(window: &winit::window::Window, pixels: &pixels::Pixels) -> Self {
|
||||||
|
// Create Dear Imgui context
|
||||||
|
let mut imgui = imgui::Context::create();
|
||||||
|
imgui.set_ini_filename(None);
|
||||||
|
|
||||||
|
// winit init
|
||||||
|
let mut platform = imgui_winit_support::WinitPlatform::init(&mut imgui);
|
||||||
|
platform.attach_window(
|
||||||
|
imgui.io_mut(),
|
||||||
|
window,
|
||||||
|
imgui_winit_support::HiDpiMode::Locked(2.2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Configure fonts
|
||||||
|
let dpi_scale = window.scale_factor();
|
||||||
|
let font_size = (13.0 * dpi_scale) as f32;
|
||||||
|
imgui.io_mut().font_global_scale = (1.0 / dpi_scale) as f32;
|
||||||
|
imgui
|
||||||
|
.fonts()
|
||||||
|
.add_font(&[imgui::FontSource::DefaultFontData {
|
||||||
|
config: Some(imgui::FontConfig {
|
||||||
|
size_pixels: font_size,
|
||||||
|
oversample_h: 2,
|
||||||
|
oversample_v: 2,
|
||||||
|
pixel_snap_h: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// Create WGPU renderer
|
||||||
|
let renderer = imgui_wgpu::Renderer::new(
|
||||||
|
&mut imgui,
|
||||||
|
pixels.device(),
|
||||||
|
pixels.queue(),
|
||||||
|
imgui_wgpu::RendererConfig {
|
||||||
|
texture_format: pixels.render_texture_format(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return Gui context
|
||||||
|
Self {
|
||||||
|
imgui,
|
||||||
|
platform,
|
||||||
|
renderer,
|
||||||
|
last_frame: Instant::now(),
|
||||||
|
last_cursor: None,
|
||||||
|
menubar: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare Dear ImGui.
|
||||||
|
pub(crate) fn prepare(
|
||||||
|
&mut self,
|
||||||
|
window: &winit::window::Window,
|
||||||
|
) -> Result<(), winit::error::ExternalError> {
|
||||||
|
// Prepare Dear ImGui
|
||||||
|
let now = Instant::now();
|
||||||
|
self.imgui.io_mut().update_delta_time(now - self.last_frame);
|
||||||
|
self.last_frame = now;
|
||||||
|
self.platform.prepare_frame(self.imgui.io_mut(), window)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render(
|
||||||
|
&mut self,
|
||||||
|
window: &winit::window::Window,
|
||||||
|
encoder: &mut wgpu::CommandEncoder,
|
||||||
|
render_target: &wgpu::TextureView,
|
||||||
|
context: &PixelsContext,
|
||||||
|
) -> imgui_wgpu::RendererResult<()> {
|
||||||
|
// Start a new Dear ImGui frame and update the cursor
|
||||||
|
let ui = self.imgui.new_frame();
|
||||||
|
|
||||||
|
let mouse_cursor = ui.mouse_cursor();
|
||||||
|
if self.last_cursor != mouse_cursor {
|
||||||
|
self.last_cursor = mouse_cursor;
|
||||||
|
self.platform.prepare_render(ui, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.menubar.draw(ui);
|
||||||
|
// Draw windows and GUI elements here
|
||||||
|
|
||||||
|
if self.menubar.about.about_open {
|
||||||
|
ui.open_popup("About");
|
||||||
|
self.menubar.about.about_open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Dear ImGui with WGPU
|
||||||
|
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
|
label: Some("imgui"),
|
||||||
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
|
view: render_target,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Load,
|
||||||
|
store: true,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.renderer.render(
|
||||||
|
self.imgui.render(),
|
||||||
|
&context.queue,
|
||||||
|
&context.device,
|
||||||
|
&mut rpass,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle any outstanding events
|
||||||
|
pub fn handle_event(
|
||||||
|
&mut self,
|
||||||
|
window: &winit::window::Window,
|
||||||
|
event: &winit::event::Event<()>,
|
||||||
|
) {
|
||||||
|
self.platform
|
||||||
|
.handle_event(self.imgui.io_mut(), window, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows or hides the [Menubar]. If `visible` is [None], toggles visibility.
|
||||||
|
pub fn show_menubar(&mut self, visible: Option<bool>) {
|
||||||
|
match visible {
|
||||||
|
Some(visible) => self.menubar.active = visible,
|
||||||
|
None => self.menubar.active ^= true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query the state of the Gui through a unified interface
|
||||||
|
pub fn wants(&mut self, wants: Wants) -> bool {
|
||||||
|
match wants {
|
||||||
|
Wants::Quit => self.menubar.file.quit,
|
||||||
|
Wants::Disasm => self.menubar.debug.dis,
|
||||||
|
Wants::Reset => {
|
||||||
|
let reset = self.menubar.debug.reset;
|
||||||
|
self.menubar.debug.reset = false;
|
||||||
|
reset
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
chirp-imgui/src/gui/about.rs
Normal file
28
chirp-imgui/src/gui/about.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//! Information about the crate at compile time
|
||||||
|
|
||||||
|
use imgui::Ui;
|
||||||
|
|
||||||
|
/// Project authors
|
||||||
|
const AUTHORS: Option<&str> = option_env!("CARGO_PKG_AUTHORS");
|
||||||
|
/// Compiled binary name
|
||||||
|
const BIN: Option<&str> = option_env!("CARGO_BIN_NAME");
|
||||||
|
/// Crate description
|
||||||
|
const DESCRIPTION: Option<&str> = option_env!("CARGO_PKG_DESCRIPTION");
|
||||||
|
/// Crate name
|
||||||
|
const NAME: Option<&str> = option_env!("CARGO_PKG_NAME");
|
||||||
|
/// Crate repository
|
||||||
|
const REPO: Option<&str> = option_env!("CARGO_PKG_REPOSITORY");
|
||||||
|
/// Crate version
|
||||||
|
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
pub(crate) fn about(ui: &Ui) {
|
||||||
|
ui.popup("About", || {
|
||||||
|
ui.text(format!(
|
||||||
|
"{} v{}",
|
||||||
|
NAME.unwrap_or("Chirp"),
|
||||||
|
VERSION.unwrap_or("None"),
|
||||||
|
));
|
||||||
|
ui.text(format!("Crafted by: {}", AUTHORS.unwrap_or("some people")));
|
||||||
|
ui.text(REPO.unwrap_or("Repo unavailable"));
|
||||||
|
});
|
||||||
|
}
|
164
chirp-imgui/src/gui/menubar.rs
Normal file
164
chirp-imgui/src/gui/menubar.rs
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
//! The menubar that shows at the top of the screen
|
||||||
|
|
||||||
|
use super::Drawable;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||||
|
pub struct Menubar {
|
||||||
|
pub(super) active: bool,
|
||||||
|
pub file: File,
|
||||||
|
pub settings: Settings,
|
||||||
|
pub debug: Debug,
|
||||||
|
pub about: Help,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drawable for Menubar {
|
||||||
|
fn draw(&mut self, ui: &imgui::Ui) {
|
||||||
|
if self.active {
|
||||||
|
ui.main_menu_bar(|| {
|
||||||
|
self.file.draw(ui);
|
||||||
|
self.settings.draw(ui);
|
||||||
|
self.debug.draw(ui);
|
||||||
|
self.about.draw(ui);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Menubar {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
active: true,
|
||||||
|
file: Default::default(),
|
||||||
|
settings: Default::default(),
|
||||||
|
debug: Default::default(),
|
||||||
|
about: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct File {
|
||||||
|
pub(super) reset: bool,
|
||||||
|
pub(super) quit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drawable for File {
|
||||||
|
fn draw(&mut self, ui: &imgui::Ui) {
|
||||||
|
ui.menu("File", || {
|
||||||
|
self.reset = ui.menu_item("Reset");
|
||||||
|
self.quit = ui.menu_item("Quit");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Debug {
|
||||||
|
pub(super) reset: bool,
|
||||||
|
pub(super) dis: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drawable for Debug {
|
||||||
|
fn draw(&mut self, ui: &imgui::Ui) {
|
||||||
|
ui.menu("Debug", || {
|
||||||
|
self.reset = ui.menu_item("Reset");
|
||||||
|
ui.checkbox("Live Disassembly", &mut self.dis);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug {
|
||||||
|
pub fn reset(&self) -> bool {
|
||||||
|
self.reset
|
||||||
|
}
|
||||||
|
pub fn dis(&self) -> bool {
|
||||||
|
self.dis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Help {
|
||||||
|
pub(super) about_open: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drawable for Help {
|
||||||
|
fn draw(&mut self, ui: &imgui::Ui) {
|
||||||
|
ui.menu("Help", || self.about_open = ui.menu_item("About..."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
|
||||||
|
pub struct Settings {
|
||||||
|
pub(super) target_ipf: usize,
|
||||||
|
pub(super) quirks: chirp::Quirks,
|
||||||
|
pub(super) mode_index: usize,
|
||||||
|
pub(super) colors: [[f32; 4]; 2],
|
||||||
|
pub(super) applied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drawable for Settings {
|
||||||
|
fn draw(&mut self, ui: &imgui::Ui) {
|
||||||
|
self.applied = false;
|
||||||
|
ui.menu("Settings", || {
|
||||||
|
use chirp::Mode::*;
|
||||||
|
ui.menu("Foreground Color", || {
|
||||||
|
self.applied |= ui.color_picker4("", &mut self.colors[0])
|
||||||
|
});
|
||||||
|
ui.menu("Background Color", || {
|
||||||
|
self.applied |= ui.color_picker4("", &mut self.colors[1])
|
||||||
|
});
|
||||||
|
const MODES: [chirp::Mode; 3] = [Chip8, SChip, XOChip];
|
||||||
|
if ui.combo_simple_string("Mode", &mut self.mode_index, &MODES) {
|
||||||
|
self.quirks = MODES[self.mode_index].into();
|
||||||
|
self.applied |= true;
|
||||||
|
}
|
||||||
|
self.applied |= {
|
||||||
|
ui.input_scalar("IPF", &mut self.target_ipf)
|
||||||
|
.chars_decimal(true)
|
||||||
|
.build()
|
||||||
|
| ui.checkbox("Bin-ops don't clear vF", &mut self.quirks.bin_ops)
|
||||||
|
| ui.checkbox("DMA doesn't modify I", &mut self.quirks.dma_inc)
|
||||||
|
| ui.checkbox("Draw calls are instant", &mut self.quirks.draw_wait)
|
||||||
|
| ui.checkbox("Screen wraps at edge", &mut self.quirks.screen_wrap)
|
||||||
|
| ui.checkbox("Shift ops ignore vY", &mut self.quirks.shift)
|
||||||
|
| ui.checkbox("Jumps behave eratically", &mut self.quirks.stupid_jumps)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings {
|
||||||
|
pub fn target_ipf(&mut self) -> &mut usize {
|
||||||
|
&mut self.target_ipf
|
||||||
|
}
|
||||||
|
pub fn quirks(&mut self) -> &mut chirp::Quirks {
|
||||||
|
&mut self.quirks
|
||||||
|
}
|
||||||
|
pub fn set_mode(&mut self, mode: chirp::Mode) {
|
||||||
|
self.mode_index = mode as usize;
|
||||||
|
}
|
||||||
|
pub fn set_color(&mut self, fg: &[u8; 4], bg: &[u8; 4]) {
|
||||||
|
for (idx, component) in fg.iter().enumerate() {
|
||||||
|
self.colors[0][idx] = *component as f32 / 255.0;
|
||||||
|
}
|
||||||
|
for (idx, component) in bg.iter().enumerate() {
|
||||||
|
self.colors[1][idx] = *component as f32 / 255.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn applied(&mut self) -> Option<(usize, chirp::Quirks, [u8; 4], [u8; 4])> {
|
||||||
|
let (fg, bg) = (self.colors[0], self.colors[1]);
|
||||||
|
let fg = [
|
||||||
|
(fg[0] * 255.0) as u8,
|
||||||
|
(fg[1] * 255.0) as u8,
|
||||||
|
(fg[2] * 255.0) as u8,
|
||||||
|
(fg[3] * 255.0) as u8,
|
||||||
|
];
|
||||||
|
let bg = [
|
||||||
|
(bg[0] * 255.0) as u8,
|
||||||
|
(bg[1] * 255.0) as u8,
|
||||||
|
(bg[2] * 255.0) as u8,
|
||||||
|
(bg[3] * 255.0) as u8,
|
||||||
|
];
|
||||||
|
self.applied
|
||||||
|
.then_some((self.target_ipf, self.quirks, fg, bg))
|
||||||
|
}
|
||||||
|
}
|
205
chirp-imgui/src/main.rs
Normal file
205
chirp-imgui/src/main.rs
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(clippy::all)]
|
||||||
|
#![allow(dead_code)] // TODO: finish writing the code
|
||||||
|
|
||||||
|
mod args;
|
||||||
|
mod emu;
|
||||||
|
mod error;
|
||||||
|
mod gui;
|
||||||
|
|
||||||
|
use crate::args::Arguments;
|
||||||
|
use crate::emu::*;
|
||||||
|
use crate::gui::*;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
|
use pixels::{Pixels, SurfaceTexture};
|
||||||
|
use std::result::Result;
|
||||||
|
use winit::dpi::LogicalSize;
|
||||||
|
use winit::event::{Event, VirtualKeyCode};
|
||||||
|
use winit::event_loop::{ControlFlow, EventLoop};
|
||||||
|
use winit::window::WindowBuilder;
|
||||||
|
use winit_input_helper::WinitInputHelper;
|
||||||
|
|
||||||
|
// TODO: Make these configurable in the frontend
|
||||||
|
|
||||||
|
const FOREGROUND: &[u8; 4] = &0xFFFF00FF_u32.to_be_bytes();
|
||||||
|
const BACKGROUND: &[u8; 4] = &0x623701FF_u32.to_be_bytes();
|
||||||
|
const INIT_SPEED: usize = 10;
|
||||||
|
|
||||||
|
struct Application {
|
||||||
|
gui: Gui,
|
||||||
|
emu: Emulator,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), error::Error> {
|
||||||
|
let args = Arguments::parse();
|
||||||
|
// Make sure the ROM file exists
|
||||||
|
if !args.file.is_file() {
|
||||||
|
eprintln!(
|
||||||
|
"{} not found. If the file exists, you might not have permission you access it.",
|
||||||
|
args.file.display().italic().red()
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let event_loop = EventLoop::new();
|
||||||
|
let mut input = WinitInputHelper::new();
|
||||||
|
|
||||||
|
let size = LogicalSize::new(128 * 8, 64 * 8);
|
||||||
|
let window = WindowBuilder::new()
|
||||||
|
.with_title("Chirp")
|
||||||
|
.with_inner_size(size)
|
||||||
|
.with_always_on_top(true)
|
||||||
|
.build(&event_loop)?;
|
||||||
|
let mut scale_factor = window.scale_factor();
|
||||||
|
|
||||||
|
let mut pixels = {
|
||||||
|
let window_size = window.inner_size();
|
||||||
|
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
|
||||||
|
Pixels::new(128, 64, surface_texture)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut gui = Gui::new(&window, &pixels);
|
||||||
|
// set initial parameters
|
||||||
|
if let Some(mode) = args.mode {
|
||||||
|
gui.menubar.settings.set_mode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
gui.menubar.settings.set_color(FOREGROUND, BACKGROUND);
|
||||||
|
*gui.menubar.settings.target_ipf() = args.speed.unwrap_or(INIT_SPEED);
|
||||||
|
|
||||||
|
let mut app = Application::new(args.into(), gui);
|
||||||
|
|
||||||
|
// Copy quirks from the running Emulator, for consistency
|
||||||
|
*app.gui.menubar.settings.quirks() = app.emu.quirks();
|
||||||
|
|
||||||
|
// Run event loop
|
||||||
|
event_loop.run(move |event, _, control_flow| {
|
||||||
|
let toggle_fullscreen = || {
|
||||||
|
window.set_fullscreen(if window.fullscreen().is_some() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(winit::window::Fullscreen::Borderless(None))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let redraw = |gui: &mut Gui, pixels: &mut Pixels| -> Result<(), error::Error> {
|
||||||
|
// Prepare gui for redraw
|
||||||
|
gui.prepare(&window)?;
|
||||||
|
|
||||||
|
// Render everything together
|
||||||
|
pixels.render_with(|encoder, render_target, context| {
|
||||||
|
// Render the emulator's screen
|
||||||
|
context.scaling_renderer.render(encoder, render_target);
|
||||||
|
// Render Dear ImGui
|
||||||
|
gui.render(&window, encoder, render_target, context)?;
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut handle_events = |state: &mut Application,
|
||||||
|
pixels: &mut Pixels,
|
||||||
|
control_flow: &mut ControlFlow|
|
||||||
|
-> Result<(), error::Error> {
|
||||||
|
state.gui.handle_event(&window, &event);
|
||||||
|
if input.update(&event) {
|
||||||
|
use VirtualKeyCode::*;
|
||||||
|
match_pressed!( match input {
|
||||||
|
Escape | input.quit() | state.gui.wants(Wants::Quit) => {
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
F1 => state.emu.print_registers(),
|
||||||
|
F2 => state.emu.print_screen()?,
|
||||||
|
F3 => state.emu.dump_screen()?,
|
||||||
|
F4 => state.emu.is_disasm(),
|
||||||
|
F5 => state.emu.pause(),
|
||||||
|
F6 => state.emu.singlestep()?,
|
||||||
|
F7 => state.emu.set_break(),
|
||||||
|
F8 => state.emu.unset_break(),
|
||||||
|
Delete => state.emu.soft_reset(),
|
||||||
|
Insert => state.emu.hard_reset(),
|
||||||
|
F11 => toggle_fullscreen(),
|
||||||
|
LAlt => state.gui.show_menubar(None),
|
||||||
|
});
|
||||||
|
state.emu.input(&input)?;
|
||||||
|
|
||||||
|
// Apply settings
|
||||||
|
if let Some((ipf, quirks, fg, bg)) = state.gui.menubar.settings.applied() {
|
||||||
|
state.emu.ipf = ipf;
|
||||||
|
state.emu.set_quirks(quirks);
|
||||||
|
state.emu.colors = [fg, bg];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the scale factor
|
||||||
|
if let Some(factor) = input.scale_factor() {
|
||||||
|
scale_factor = factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize the window
|
||||||
|
if let Some(size) = input.window_resized() {
|
||||||
|
if size.width > 0 && size.height > 0 {
|
||||||
|
// Resize the surface texture
|
||||||
|
pixels.resize_surface(size.width, size.height)?;
|
||||||
|
// Resize the world
|
||||||
|
let LogicalSize { width, height } = size.to_logical(scale_factor);
|
||||||
|
pixels.resize_buffer(width, height)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.emu.set_disasm(state.gui.wants(Wants::Disasm));
|
||||||
|
if state.gui.wants(Wants::Reset) {
|
||||||
|
state.emu.hard_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the game loop
|
||||||
|
state.emu.update()?;
|
||||||
|
state.emu.draw(pixels)?;
|
||||||
|
|
||||||
|
// redraw the window
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Event::RedrawRequested(_) = event {
|
||||||
|
if let Err(e) = redraw(&mut app.gui, &mut pixels) {
|
||||||
|
eprintln!("{e}");
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = handle_events(&mut app, &mut pixels, control_flow) {
|
||||||
|
eprintln!("{e}");
|
||||||
|
*control_flow = ControlFlow::Exit;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application {
|
||||||
|
fn new(emu: Emulator, gui: Gui) -> Self {
|
||||||
|
Self { gui, emu }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! match_pressed {
|
||||||
|
(match $input:ident {$($key:path $( | $cond:expr)? => $action:expr),+ $(,)?}) => {
|
||||||
|
$(
|
||||||
|
if $input.key_pressed($key) $( || $cond )? {
|
||||||
|
$action;
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! match_released {
|
||||||
|
(match $input:ident {$($key:path $( | $cond:expr)? => $action:expr),+ $(,)?}) => {
|
||||||
|
$(
|
||||||
|
if $input.key_released($key) $( || $cond )? {
|
||||||
|
$action;
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
15
chirp-minifb/Cargo.toml
Normal file
15
chirp-minifb/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "chirp-minifb"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
||||||
|
chirp = { path = ".." }
|
||||||
|
minifb = { version = "0.24.0" }
|
||||||
|
|
||||||
|
gumdrop = "0.8.1"
|
||||||
|
owo-colors = "3"
|
||||||
|
thiserror = "1.0.39"
|
18
chirp-minifb/src/error.rs
Normal file
18
chirp-minifb/src/error.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
//! Error type for chirp-minifb
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Error originated in [`chirp`]
|
||||||
|
#[error(transparent)]
|
||||||
|
Chirp(#[from] chirp::error::Error),
|
||||||
|
/// Error originated in [`std::io`]
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
/// Error originated in [`minifb`]
|
||||||
|
#[error(transparent)]
|
||||||
|
Minifb(#[from] minifb::Error),
|
||||||
|
}
|
@ -1,18 +1,20 @@
|
|||||||
// (c) 2023 John A. Breaux
|
// (c) 2023 John A. Breaux
|
||||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Chirp: A chip-8 interpreter in Rust
|
//! Chirp: A chip-8 interpreter in Rust
|
||||||
//! Hello, world!
|
//! Hello, world!
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
mod error;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
use chirp::error::Error::BreakpointHit;
|
use chirp::error::Error::BreakpointHit;
|
||||||
use chirp::{error::Result, *};
|
use chirp::*;
|
||||||
|
use error::Result;
|
||||||
use gumdrop::*;
|
use gumdrop::*;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use std::fs::read;
|
|
||||||
use std::{
|
use std::{
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
@ -47,7 +49,7 @@ struct Arguments {
|
|||||||
#[options(help = "Enable pause mode at startup.")]
|
#[options(help = "Enable pause mode at startup.")]
|
||||||
pub pause: bool,
|
pub pause: bool,
|
||||||
|
|
||||||
#[options(help = "Set the instructions-per-delay rate, or use realtime.")]
|
#[options(help = "Set the instructions-per-delay rate. If unspecified, use realtime.")]
|
||||||
pub speed: Option<usize>,
|
pub speed: Option<usize>,
|
||||||
#[options(help = "Set the instructions-per-frame rate.")]
|
#[options(help = "Set the instructions-per-frame rate.")]
|
||||||
pub step: Option<usize>,
|
pub step: Option<usize>,
|
||||||
@ -76,16 +78,19 @@ struct Arguments {
|
|||||||
help = "Use CHIP-48 style DMA instructions, which don't touch I."
|
help = "Use CHIP-48 style DMA instructions, which don't touch I."
|
||||||
)]
|
)]
|
||||||
pub memory: bool,
|
pub memory: bool,
|
||||||
|
|
||||||
#[options(
|
#[options(
|
||||||
short = "v",
|
short = "v",
|
||||||
help = "Use CHIP-48 style bit-shifts, which don't touch vY."
|
help = "Use CHIP-48 style bit-shifts, which don't touch vY."
|
||||||
)]
|
)]
|
||||||
pub shift: bool,
|
pub shift: bool,
|
||||||
|
|
||||||
#[options(
|
#[options(
|
||||||
short = "b",
|
short = "b",
|
||||||
help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]."
|
help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]."
|
||||||
)]
|
)]
|
||||||
pub jumping: bool,
|
pub jumping: bool,
|
||||||
|
|
||||||
#[options(
|
#[options(
|
||||||
long = "break",
|
long = "break",
|
||||||
help = "Set breakpoints for the emulator to stop at.",
|
help = "Set breakpoints for the emulator to stop at.",
|
||||||
@ -93,16 +98,23 @@ struct Arguments {
|
|||||||
meta = "BP"
|
meta = "BP"
|
||||||
)]
|
)]
|
||||||
pub breakpoints: Vec<u16>,
|
pub breakpoints: Vec<u16>,
|
||||||
|
|
||||||
#[options(
|
#[options(
|
||||||
help = "Load additional word at address 0x1fe",
|
help = "Load additional word at address 0x1fe",
|
||||||
parse(try_from_str = "parse_hex"),
|
parse(try_from_str = "parse_hex"),
|
||||||
meta = "WORD"
|
meta = "WORD"
|
||||||
)]
|
)]
|
||||||
pub data: u16,
|
pub data: u16,
|
||||||
|
|
||||||
#[options(help = "Set the target framerate.", default = "60", meta = "FR")]
|
#[options(help = "Set the target framerate.", default = "60", meta = "FR")]
|
||||||
pub frame_rate: u64,
|
pub frame_rate: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Chip8 {
|
||||||
|
pub cpu: CPU,
|
||||||
|
pub screen: Screen,
|
||||||
|
}
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct State {
|
struct State {
|
||||||
pub speed: usize,
|
pub speed: usize,
|
||||||
@ -122,31 +134,20 @@ impl State {
|
|||||||
rate: options.frame_rate,
|
rate: options.frame_rate,
|
||||||
perf: options.perf,
|
perf: options.perf,
|
||||||
ch8: Chip8 {
|
ch8: Chip8 {
|
||||||
bus: bus! {
|
|
||||||
// Load the charset into ROM
|
|
||||||
Charset [0x0050..0x00A0] = include_bytes!("../../mem/charset.bin"),
|
|
||||||
// Load the ROM file into RAM
|
|
||||||
Program [0x0200..0x1000] = &read(&options.file)?,
|
|
||||||
// Create a screen
|
|
||||||
Screen [0x1000..0x1100],
|
|
||||||
// Create a stack
|
|
||||||
Stack [0x0EA0..0x0F00],
|
|
||||||
},
|
|
||||||
cpu: CPU::new(
|
cpu: CPU::new(
|
||||||
0x1000,
|
Some(&options.file),
|
||||||
0x50,
|
0x50,
|
||||||
0x200,
|
0x200,
|
||||||
0xefe,
|
|
||||||
Dis::default(),
|
Dis::default(),
|
||||||
options.breakpoints,
|
options.breakpoints,
|
||||||
Flags {
|
Flags {
|
||||||
quirks: options.mode.unwrap_or_default().into(),
|
quirks: options.mode.unwrap_or_default().into(),
|
||||||
debug: options.debug,
|
debug: options.debug,
|
||||||
pause: options.pause,
|
pause: options.pause,
|
||||||
monotonic: options.speed,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
),
|
)?,
|
||||||
|
screen: Screen::default(),
|
||||||
},
|
},
|
||||||
ui: UIBuilder::new(128, 64, &options.file).build()?,
|
ui: UIBuilder::new(128, 64, &options.file).build()?,
|
||||||
ft: Instant::now(),
|
ft: Instant::now(),
|
||||||
@ -157,14 +158,14 @@ impl State {
|
|||||||
state.ch8.cpu.flags.quirks.draw_wait ^= options.drawsync;
|
state.ch8.cpu.flags.quirks.draw_wait ^= options.drawsync;
|
||||||
state.ch8.cpu.flags.quirks.shift ^= options.shift;
|
state.ch8.cpu.flags.quirks.shift ^= options.shift;
|
||||||
state.ch8.cpu.flags.quirks.stupid_jumps ^= options.jumping;
|
state.ch8.cpu.flags.quirks.stupid_jumps ^= options.jumping;
|
||||||
state.ch8.bus.write(0x1feu16, options.data);
|
state.ch8.screen.write(0x1feu16, options.data);
|
||||||
Ok(state)
|
Ok(state)
|
||||||
}
|
}
|
||||||
fn keys(&mut self) -> Result<bool> {
|
fn keys(&mut self) -> Result<bool> {
|
||||||
self.ui.keys(&mut self.ch8)
|
self.ui.keys(&mut self.ch8)
|
||||||
}
|
}
|
||||||
fn frame(&mut self) -> Result<bool> {
|
fn frame(&mut self) -> Result<bool> {
|
||||||
self.ui.frame(&mut self.ch8)
|
self.ui.frame(&self.ch8)
|
||||||
}
|
}
|
||||||
fn tick_cpu(&mut self) -> Result<()> {
|
fn tick_cpu(&mut self) -> Result<()> {
|
||||||
if !self.ch8.cpu.flags.pause {
|
if !self.ch8.cpu.flags.pause {
|
||||||
@ -172,7 +173,7 @@ impl State {
|
|||||||
match self.step {
|
match self.step {
|
||||||
Some(ticks) => {
|
Some(ticks) => {
|
||||||
let time = Instant::now();
|
let time = Instant::now();
|
||||||
self.ch8.cpu.multistep(&mut self.ch8.bus, ticks)?;
|
self.ch8.cpu.multistep(&mut self.ch8.screen, ticks)?;
|
||||||
if self.perf {
|
if self.perf {
|
||||||
let time = time.elapsed();
|
let time = time.elapsed();
|
||||||
let nspt = time.as_secs_f64() / ticks as f64;
|
let nspt = time.as_secs_f64() / ticks as f64;
|
||||||
@ -185,16 +186,16 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.ch8.cpu.multistep(&mut self.ch8.bus, rate)?;
|
self.ch8.cpu.multistep(&mut self.ch8.screen, rate)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn wait_for_next_frame(&mut self) {
|
fn wait_for_next_frame(&mut self) {
|
||||||
let rate = 1_000_000_000 / self.rate + 1;
|
let rate = Duration::from_nanos(1_000_000_000 / self.rate + 1);
|
||||||
std::thread::sleep(Duration::from_nanos(rate).saturating_sub(self.ft.elapsed()));
|
std::thread::sleep(rate.saturating_sub(self.ft.elapsed()));
|
||||||
self.ft = Instant::now();
|
self.ft += rate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +212,7 @@ impl Iterator for State {
|
|||||||
}
|
}
|
||||||
// Allow breakpoint hit messages
|
// Allow breakpoint hit messages
|
||||||
match self.tick_cpu() {
|
match self.tick_cpu() {
|
||||||
Err(BreakpointHit { addr, next }) => {
|
Err(error::Error::Chirp(BreakpointHit { addr, next })) => {
|
||||||
eprintln!("Breakpoint hit: {:3x} ({:4x})", addr, next);
|
eprintln!("Breakpoint hit: {:3x} ({:4x})", addr, next);
|
||||||
}
|
}
|
||||||
Err(e) => return Some(Err(e)),
|
Err(e) => return Some(Err(e)),
|
@ -1,6 +1,9 @@
|
|||||||
//! Tests for chirp-minifb
|
//! Tests for chirp-minifb
|
||||||
|
#![allow(clippy::redundant_clone)]
|
||||||
|
|
||||||
use super::ui::*;
|
use super::ui::*;
|
||||||
|
use super::Chip8;
|
||||||
|
use crate::error::Result;
|
||||||
use chirp::*;
|
use chirp::*;
|
||||||
use std::{collections::hash_map::DefaultHasher, hash::Hash};
|
use std::{collections::hash_map::DefaultHasher, hash::Hash};
|
||||||
|
|
||||||
@ -29,14 +32,14 @@ mod ui {
|
|||||||
fn new_chip8() -> Chip8 {
|
fn new_chip8() -> Chip8 {
|
||||||
Chip8 {
|
Chip8 {
|
||||||
cpu: CPU::default(),
|
cpu: CPU::default(),
|
||||||
bus: bus! {},
|
screen: Screen::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn frame() -> Result<()> {
|
fn frame() -> Result<()> {
|
||||||
let mut ui = UIBuilder::new(32, 64, "dummy.ch8").build()?;
|
let mut ui = UIBuilder::new(32, 64, "dummy.ch8").build()?;
|
||||||
let mut ch8 = new_chip8();
|
let ch8 = new_chip8();
|
||||||
ui.frame(&mut ch8).unwrap();
|
ui.frame(&ch8).unwrap();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
@ -1,22 +1,18 @@
|
|||||||
// (c) 2023 John A. Breaux
|
// (c) 2023 John A. Breaux
|
||||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
//! Platform-specific IO/UI code, and some debug functionality.
|
//! Platform-specific IO/UI code, and some debug functionality.
|
||||||
//! TODO: Destroy this all.
|
//! TODO: Destroy this all.
|
||||||
|
|
||||||
|
use super::Chip8;
|
||||||
|
use crate::error::Result;
|
||||||
|
use chirp::screen::Screen;
|
||||||
|
use minifb::*;
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
use chirp::{
|
|
||||||
bus::{Bus, Region},
|
|
||||||
error::Result,
|
|
||||||
Chip8,
|
|
||||||
};
|
|
||||||
use minifb::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct UIBuilder {
|
pub struct UIBuilder {
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
@ -81,8 +77,10 @@ pub struct FrameBufferFormat {
|
|||||||
impl Default for FrameBufferFormat {
|
impl Default for FrameBufferFormat {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
FrameBufferFormat {
|
FrameBufferFormat {
|
||||||
fg: 0x0011a434,
|
// fg: 0x0011a434,
|
||||||
bg: 0x001E2431,
|
// bg: 0x001E2431,
|
||||||
|
fg: 0x00FFFF00,
|
||||||
|
bg: 0x00623701,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,27 +102,25 @@ impl FrameBuffer {
|
|||||||
format: Default::default(),
|
format: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn render(&mut self, window: &mut Window, bus: &Bus) -> Result<()> {
|
pub fn render(&mut self, window: &mut Window, screen: &Screen) -> Result<()> {
|
||||||
if let Some(screen) = bus.get_region(Region::Screen) {
|
// Resizing the buffer does not unmap memory.
|
||||||
// Resizing the buffer does not unmap memory.
|
// After the first use of high-res mode, this is pretty cheap
|
||||||
// After the first use of high-res mode, this is pretty cheap
|
(self.width, self.height) = match screen.len() {
|
||||||
(self.width, self.height) = match screen.len() {
|
256 => (64, 32),
|
||||||
256 => (64, 32),
|
1024 => (128, 64),
|
||||||
1024 => (128, 64),
|
_ => {
|
||||||
_ => {
|
unimplemented!("Screen must be 64*32 or 128*64");
|
||||||
unimplemented!("Screen must be 64*32 or 128*64");
|
}
|
||||||
}
|
};
|
||||||
};
|
self.buffer.resize(self.width * self.height, 0);
|
||||||
self.buffer.resize(self.width * self.height, 0);
|
for (idx, byte) in screen.as_slice().iter().enumerate() {
|
||||||
for (idx, byte) in screen.iter().enumerate() {
|
for bit in 0..8 {
|
||||||
for bit in 0..8 {
|
self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 {
|
||||||
self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 {
|
self.format.fg
|
||||||
self.format.fg
|
} else {
|
||||||
} else {
|
self.format.bg
|
||||||
self.format.bg
|
// .wrapping_add(0x001104 * (idx / self.width) as u32)
|
||||||
// .wrapping_add(0x001104 * (idx / self.width) as u32)
|
// .wrapping_add(0x141000 * (idx & 3) as u32)
|
||||||
// .wrapping_add(0x141000 * (idx & 3) as u32)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,7 +145,7 @@ pub struct UI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UI {
|
impl UI {
|
||||||
pub fn frame(&mut self, ch8: &mut Chip8) -> Result<bool> {
|
pub fn frame(&mut self, ch8: &Chip8) -> Result<bool> {
|
||||||
if ch8.cpu.flags.pause {
|
if ch8.cpu.flags.pause {
|
||||||
self.window.set_title("Chirp ⏸")
|
self.window.set_title("Chirp ⏸")
|
||||||
} else {
|
} else {
|
||||||
@ -163,7 +159,7 @@ impl UI {
|
|||||||
}
|
}
|
||||||
self.time = Instant::now();
|
self.time = Instant::now();
|
||||||
// update framebuffer
|
// update framebuffer
|
||||||
self.fb.render(&mut self.window, &ch8.bus)?;
|
self.fb.render(&mut self.window, &ch8.screen)?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +177,6 @@ impl UI {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|key| !self.window.get_keys().contains(key))
|
.filter(|key| !self.window.get_keys().contains(key))
|
||||||
};
|
};
|
||||||
use crate::ui::Region::*;
|
|
||||||
for key in get_keys_released() {
|
for key in get_keys_released() {
|
||||||
if let Some(key) = identify_key(key) {
|
if let Some(key) = identify_key(key) {
|
||||||
ch8.cpu.release(key)?;
|
ch8.cpu.release(key)?;
|
||||||
@ -192,7 +187,7 @@ impl UI {
|
|||||||
use Key::*;
|
use Key::*;
|
||||||
match key {
|
match key {
|
||||||
F1 | Comma => ch8.cpu.dump(),
|
F1 | Comma => ch8.cpu.dump(),
|
||||||
F2 | Period => ch8.bus.print_screen()?,
|
F2 | Period => ch8.screen.print_screen(),
|
||||||
F3 => {
|
F3 => {
|
||||||
debug_dump_screen(ch8, &self.rom).expect("Unable to write debug screen dump");
|
debug_dump_screen(ch8, &self.rom).expect("Unable to write debug screen dump");
|
||||||
}
|
}
|
||||||
@ -216,7 +211,7 @@ impl UI {
|
|||||||
}),
|
}),
|
||||||
F6 | Enter => {
|
F6 | Enter => {
|
||||||
eprintln!("Step");
|
eprintln!("Step");
|
||||||
ch8.cpu.singlestep(&mut ch8.bus)?;
|
ch8.cpu.singlestep(&mut ch8.screen)?;
|
||||||
}
|
}
|
||||||
F7 => {
|
F7 => {
|
||||||
eprintln!("Set breakpoint {:03x}.", ch8.cpu.pc());
|
eprintln!("Set breakpoint {:03x}.", ch8.cpu.pc());
|
||||||
@ -229,7 +224,7 @@ impl UI {
|
|||||||
F9 | Delete => {
|
F9 | Delete => {
|
||||||
eprintln!("Soft reset state.cpu {:03x}", ch8.cpu.pc());
|
eprintln!("Soft reset state.cpu {:03x}", ch8.cpu.pc());
|
||||||
ch8.cpu.soft_reset();
|
ch8.cpu.soft_reset();
|
||||||
ch8.bus.clear_region(Screen);
|
ch8.screen.clear();
|
||||||
}
|
}
|
||||||
Escape => return Ok(false),
|
Escape => return Ok(false),
|
||||||
key => {
|
key => {
|
||||||
@ -267,29 +262,18 @@ pub fn identify_key(key: Key) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn debug_dump_screen(ch8: &Chip8, rom: &Path) -> Result<()> {
|
pub fn debug_dump_screen(ch8: &Chip8, rom: &Path) -> Result<()> {
|
||||||
let path = PathBuf::new()
|
let mut path = PathBuf::new().join(format!(
|
||||||
.join("src/cpu/tests/screens/")
|
"{}_{}.bin",
|
||||||
.join(if rom.is_absolute() {
|
rom.file_stem().unwrap_or_default().to_string_lossy(),
|
||||||
Path::new("unknown/")
|
ch8.cpu.cycle()
|
||||||
} else {
|
));
|
||||||
rom.file_name().unwrap_or(OsStr::new("unknown")).as_ref()
|
path.set_extension("bin");
|
||||||
})
|
if std::fs::write(&path, ch8.screen.as_slice()).is_ok() {
|
||||||
.join(format!("{}.bin", ch8.cpu.cycle()));
|
eprintln!("Saved to {}", &path.display());
|
||||||
std::fs::write(
|
} else if std::fs::write("screen_dump.bin", ch8.screen.as_slice()).is_ok() {
|
||||||
&path,
|
eprintln!("Saved to screen_dump.bin");
|
||||||
ch8.bus
|
} else {
|
||||||
.get_region(Region::Screen)
|
eprintln!("Failed to dump screen to file.")
|
||||||
.expect("Region::Screen should exist"),
|
}
|
||||||
)
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
std::fs::write(
|
|
||||||
"screendump.bin",
|
|
||||||
ch8.bus
|
|
||||||
.get_region(Region::Screen)
|
|
||||||
.expect("Region::Screen should exist"),
|
|
||||||
)
|
|
||||||
.ok(); // lmao
|
|
||||||
});
|
|
||||||
eprintln!("Saved to {}", &path.display());
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
5
justfile
5
justfile
@ -1,3 +1,6 @@
|
|||||||
|
# (c) 2023 John A. Breaux
|
||||||
|
# This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
# Some common commands for working on this stuff
|
# Some common commands for working on this stuff
|
||||||
|
|
||||||
# Run All Tests
|
# Run All Tests
|
||||||
@ -14,7 +17,7 @@ debug rom:
|
|||||||
cargo run -- -d '{{rom}}'
|
cargo run -- -d '{{rom}}'
|
||||||
# Run at 2100000 instructions per frame, and output per-frame runtime statistics
|
# Run at 2100000 instructions per frame, and output per-frame runtime statistics
|
||||||
bench:
|
bench:
|
||||||
cargo run --release -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip
|
cargo run --release --bin chirp --features="minifb" -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip
|
||||||
|
|
||||||
flame rom:
|
flame rom:
|
||||||
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10
|
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10
|
||||||
|
@ -3,24 +3,48 @@ use gumdrop::*;
|
|||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use std::{fs::read, path::PathBuf};
|
use std::{fs::read, path::PathBuf};
|
||||||
|
|
||||||
|
mod tree;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let options = Arguments::parse_args_default_or_exit();
|
let mut options = Arguments::parse_args_default_or_exit();
|
||||||
let contents = &read(&options.file)?;
|
while let Some(file) = options.file.pop() {
|
||||||
let disassembler = Dis::default();
|
println!("{file:?}");
|
||||||
for (addr, insn) in contents[options.offset..].chunks_exact(2).enumerate() {
|
let contents = &read(&file)?;
|
||||||
let insn = u16::from_be_bytes(
|
if options.tree || options.traverse {
|
||||||
insn.try_into()
|
let loadaddr = options.loadaddr as usize;
|
||||||
.expect("Iterated over 2-byte chunks, got <2 bytes"),
|
let mem = mem! {
|
||||||
);
|
cpu::mem::Region::Program [loadaddr..loadaddr + contents.len()] = contents
|
||||||
println!(
|
};
|
||||||
"{}",
|
let mut nodes = Default::default();
|
||||||
format_args!(
|
let tree = tree::DisNode::traverse(
|
||||||
"{:03x}: {} {:04x}",
|
mem.grab(..).expect("grabbing [..] should never fail"),
|
||||||
2 * addr + 0x200 + options.offset,
|
&mut nodes,
|
||||||
disassembler.once(insn),
|
options.loadaddr as usize + options.offset,
|
||||||
insn.bright_black(),
|
);
|
||||||
)
|
if options.traverse {
|
||||||
);
|
for (k, v) in nodes.iter() {
|
||||||
|
if let Some(v) = &v.upgrade().as_ref().map(std::rc::Rc::as_ref) {
|
||||||
|
println!("{k:03x}: {v:04x}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("{tree}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let disassembler = Dis::default();
|
||||||
|
for (addr, insn) in contents[options.offset..].chunks_exact(2).enumerate() {
|
||||||
|
let insn = u16::from_be_bytes(
|
||||||
|
insn.try_into()
|
||||||
|
.expect("Iterated over 2-byte chunks, got <2 bytes"),
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"{:03x}: {:04x} {}",
|
||||||
|
2 * addr + 0x200 + options.offset,
|
||||||
|
insn.bright_black(),
|
||||||
|
disassembler.once(insn),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -30,11 +54,19 @@ struct Arguments {
|
|||||||
#[options(help = "Show help text")]
|
#[options(help = "Show help text")]
|
||||||
help: bool,
|
help: bool,
|
||||||
#[options(help = "Load a ROM to run on Chirp", free, required)]
|
#[options(help = "Load a ROM to run on Chirp", free, required)]
|
||||||
pub file: PathBuf,
|
pub file: Vec<PathBuf>,
|
||||||
#[options(help = "Load address (usually 200)", parse(try_from_str = "parse_hex"))]
|
#[options(
|
||||||
|
help = "Load address (usually 200)",
|
||||||
|
parse(try_from_str = "parse_hex"),
|
||||||
|
default = "200"
|
||||||
|
)]
|
||||||
pub loadaddr: u16,
|
pub loadaddr: u16,
|
||||||
#[options(help = "Start disassembling at offset...")]
|
#[options(help = "Start disassembling at offset...")]
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
|
#[options(help = "Print the disassembly as a tree")]
|
||||||
|
pub tree: bool,
|
||||||
|
#[options(help = "Prune unreachable instructions ")]
|
||||||
|
pub traverse: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> {
|
fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> {
|
||||||
|
183
src/bin/chirp-disasm/tree.rs
Normal file
183
src/bin/chirp-disasm/tree.rs
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
#![allow(unused_variables)]
|
||||||
|
use std::{
|
||||||
|
collections::{BTreeMap, HashSet},
|
||||||
|
fmt::{Display, LowerHex},
|
||||||
|
rc::{Rc, Weak},
|
||||||
|
};
|
||||||
|
|
||||||
|
use chirp::cpu::instruction::Insn;
|
||||||
|
use imperative_rs::InstructionSet;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
|
|
||||||
|
type Adr = usize;
|
||||||
|
|
||||||
|
/// Represents the kinds of control flow an instruction can take
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub enum DisNodeContents {
|
||||||
|
Subroutine {
|
||||||
|
insn: Insn,
|
||||||
|
jump: Rc<DisNode>,
|
||||||
|
ret: Rc<DisNode>,
|
||||||
|
},
|
||||||
|
Branch {
|
||||||
|
insn: Insn,
|
||||||
|
next: Rc<DisNode>,
|
||||||
|
jump: Rc<DisNode>,
|
||||||
|
},
|
||||||
|
Continue {
|
||||||
|
insn: Insn,
|
||||||
|
next: Rc<DisNode>,
|
||||||
|
},
|
||||||
|
End {
|
||||||
|
insn: Insn,
|
||||||
|
},
|
||||||
|
RelBranch {
|
||||||
|
insn: Insn,
|
||||||
|
},
|
||||||
|
Merge {
|
||||||
|
insn: Insn,
|
||||||
|
back: Option<Weak<DisNode>>,
|
||||||
|
},
|
||||||
|
PendingMerge {
|
||||||
|
insn: Insn,
|
||||||
|
},
|
||||||
|
#[default]
|
||||||
|
Invalid,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the kinds of control flow an instruction can take
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DisNode {
|
||||||
|
pub contents: DisNodeContents,
|
||||||
|
pub addr: Adr,
|
||||||
|
pub depth: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisNode {
|
||||||
|
pub fn traverse(
|
||||||
|
mem: &[u8],
|
||||||
|
nodes: &mut BTreeMap<Adr, Weak<DisNode>>,
|
||||||
|
addr: Adr,
|
||||||
|
) -> Rc<DisNode> {
|
||||||
|
Self::tree_recurse(mem, &mut Default::default(), nodes, addr, 0)
|
||||||
|
}
|
||||||
|
pub fn tree_recurse(
|
||||||
|
mem: &[u8],
|
||||||
|
visited: &mut HashSet<Adr>,
|
||||||
|
nodes: &mut BTreeMap<Adr, Weak<DisNode>>,
|
||||||
|
addr: Adr,
|
||||||
|
depth: usize,
|
||||||
|
) -> Rc<DisNode> {
|
||||||
|
use DisNodeContents::*;
|
||||||
|
// Try to decode an instruction. If the instruction is invalid, fail early.
|
||||||
|
let Ok((len, insn)) = Insn::decode(&mem[addr..]) else {
|
||||||
|
return Rc::new(DisNode {
|
||||||
|
contents: Invalid,
|
||||||
|
addr,
|
||||||
|
depth,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let mut next = DisNode {
|
||||||
|
contents: {
|
||||||
|
match insn {
|
||||||
|
// instruction is already visited, but the branch isn't guaranteed to be in the tree yet
|
||||||
|
_ if !visited.insert(addr) => PendingMerge { insn },
|
||||||
|
|
||||||
|
Insn::ret | Insn::halt => End { insn },
|
||||||
|
|
||||||
|
// A branch to the current address will halt the machine
|
||||||
|
Insn::jmp { A } | Insn::call { A } if A as usize == addr => End { insn },
|
||||||
|
|
||||||
|
Insn::jmp { A } => Continue {
|
||||||
|
insn,
|
||||||
|
next: DisNode::tree_recurse(mem, visited, nodes, A as usize, depth),
|
||||||
|
},
|
||||||
|
|
||||||
|
Insn::call { A } => Branch {
|
||||||
|
insn,
|
||||||
|
jump: DisNode::tree_recurse(mem, visited, nodes, A as usize, depth + 1),
|
||||||
|
next: DisNode::tree_recurse(mem, visited, nodes, addr + len, depth),
|
||||||
|
},
|
||||||
|
|
||||||
|
// If the instruction is a skip instruction, first visit the next instruction,
|
||||||
|
// then visit the skip instruction. This preserves visitor order.
|
||||||
|
Insn::seb { .. }
|
||||||
|
| Insn::sneb { .. }
|
||||||
|
| Insn::se { .. }
|
||||||
|
| Insn::sne { .. }
|
||||||
|
| Insn::sek { .. } => Branch {
|
||||||
|
insn,
|
||||||
|
// FIXME: If the next instruction is Long I, this will just break
|
||||||
|
next: DisNode::tree_recurse(mem, visited, nodes, addr + len, depth),
|
||||||
|
jump: DisNode::tree_recurse(mem, visited, nodes, addr + len + 2, depth + 1),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Relative branch prediction is out of scope right now
|
||||||
|
Insn::jmpr { .. } => RelBranch { insn },
|
||||||
|
|
||||||
|
// If the instruction is any other instruction, emit a Continue token
|
||||||
|
_ => Continue {
|
||||||
|
insn,
|
||||||
|
next: DisNode::tree_recurse(mem, visited, nodes, addr + len, depth),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addr,
|
||||||
|
depth,
|
||||||
|
};
|
||||||
|
// Resolve pending merges
|
||||||
|
if let PendingMerge { insn } = next.contents {
|
||||||
|
next.contents = Merge {
|
||||||
|
insn,
|
||||||
|
back: nodes.get(&addr).cloned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let next = Rc::new(next);
|
||||||
|
nodes.insert(addr, Rc::downgrade(&next));
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DisNode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
use DisNodeContents::*;
|
||||||
|
write!(f, "\n{:04x}: ", self.addr,)?;
|
||||||
|
for indent in 0..self.depth {
|
||||||
|
Display::fmt(&"│ ".bright_magenta(), f)?;
|
||||||
|
}
|
||||||
|
match &self.contents {
|
||||||
|
Subroutine { insn, ret, jump } => write!(f, "{insn}{jump}{ret}"),
|
||||||
|
Branch { insn, next, jump } => write!(f, "{insn}{jump}{next}"),
|
||||||
|
Continue { insn, next } => write!(f, "{insn}{next}"),
|
||||||
|
RelBranch { insn } => Display::fmt(&insn.underline(), f),
|
||||||
|
PendingMerge { insn } | Merge { insn, .. } => write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
format_args!("{}; ...", insn).italic().bright_black()
|
||||||
|
),
|
||||||
|
End { insn } => Display::fmt(insn, f),
|
||||||
|
Invalid => Display::fmt(&"Invalid".bold().red(), f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LowerHex for DisNode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
use DisNodeContents::*;
|
||||||
|
match self.contents {
|
||||||
|
Subroutine { insn, .. }
|
||||||
|
| Branch { insn, .. }
|
||||||
|
| Continue { insn, .. }
|
||||||
|
| Merge { insn, .. }
|
||||||
|
| End { insn }
|
||||||
|
| RelBranch { insn }
|
||||||
|
| PendingMerge { insn } => {
|
||||||
|
LowerHex::fmt(&u32::from(&insn).bright_black(), f)?;
|
||||||
|
f.write_str(" ")?;
|
||||||
|
Display::fmt(&insn.cyan(), f)
|
||||||
|
}
|
||||||
|
Invalid => Display::fmt("Invalid", f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +0,0 @@
|
|||||||
#![allow(unused_imports)]
|
|
||||||
use chirp::*;
|
|
||||||
#[cfg(features = "iced")]
|
|
||||||
use iced::{
|
|
||||||
executor, time, window, Alignment, Application, Command, Element, Length, Settings,
|
|
||||||
Subscription,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(features = "iced")]
|
|
||||||
fn main() -> iced::Result {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(features = "iced"))]
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// TODO: `impl Application for Emulator {}`
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
|
||||||
struct Emulator {
|
|
||||||
mem: Bus,
|
|
||||||
cpu: CPU,
|
|
||||||
fps: f64,
|
|
||||||
ipf: usize,
|
|
||||||
}
|
|
@ -4,7 +4,7 @@ use std::{env::args, fs::read};
|
|||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
for screen in args().skip(1).inspect(|screen| println!("{screen}")) {
|
for screen in args().skip(1).inspect(|screen| println!("{screen}")) {
|
||||||
let screen = read(screen)?;
|
let screen = read(screen)?;
|
||||||
bus! {Screen [0..screen.len()] = &screen}.print_screen()?;
|
Screen::from(screen).print_screen();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
497
src/bus.rs
497
src/bus.rs
@ -1,497 +0,0 @@
|
|||||||
// (c) 2023 John A. Breaux
|
|
||||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
|
||||||
|
|
||||||
//! The Bus connects the CPU to Memory
|
|
||||||
//!
|
|
||||||
//! This is more of a memory management unit + some utils for reading/writing
|
|
||||||
|
|
||||||
use crate::error::{Error::MissingRegion, Result};
|
|
||||||
use std::{
|
|
||||||
fmt::{Debug, Display, Formatter},
|
|
||||||
ops::Range,
|
|
||||||
slice::SliceIndex,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Creates a new bus, growing the backing memory as needed
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
/// # use chirp::*;
|
|
||||||
/// let mut bus = bus! {
|
|
||||||
/// Stack [0x0000..0x0800] = b"ABCDEF",
|
|
||||||
/// Program [0x0800..0x1000] = include_bytes!("bus.rs"),
|
|
||||||
/// };
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! bus {
|
|
||||||
($($name:path $(:)? [$range:expr] $(= $data:expr)?) ,* $(,)?) => {
|
|
||||||
$crate::bus::Bus::default()
|
|
||||||
$(
|
|
||||||
.add_region($name, $range)
|
|
||||||
$(
|
|
||||||
.load_region($name, $data)
|
|
||||||
)?
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traits Read and Write are here purely to make implementing other things more bearable
|
|
||||||
/// Read a T from address `addr`
|
|
||||||
pub trait Read<T> {
|
|
||||||
/// Read a T from address `addr`
|
|
||||||
fn read(&self, addr: impl Into<usize>) -> T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write "some data" to the Bus
|
|
||||||
pub trait Write<T> {
|
|
||||||
/// Write a T to address `addr`
|
|
||||||
fn write(&mut self, addr: impl Into<usize>, data: T);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a named region in memory
|
|
||||||
#[non_exhaustive]
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub enum Region {
|
|
||||||
/// Character ROM (but writable!)
|
|
||||||
Charset,
|
|
||||||
/// Program memory
|
|
||||||
Program,
|
|
||||||
/// Screen buffer
|
|
||||||
Screen,
|
|
||||||
/// Stack space
|
|
||||||
Stack,
|
|
||||||
#[doc(hidden)]
|
|
||||||
/// Total number of named regions
|
|
||||||
Count,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Region {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
match self {
|
|
||||||
Region::Charset => "charset",
|
|
||||||
Region::Program => "program",
|
|
||||||
Region::Screen => "screen",
|
|
||||||
Region::Stack => "stack",
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stores memory in a series of named regions with ranges
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
|
||||||
pub struct Bus {
|
|
||||||
memory: Vec<u8>,
|
|
||||||
region: [Option<Range<usize>>; Region::Count as usize],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bus {
|
|
||||||
// TODO: make bus::new() give a properly set up bus with a default memory map
|
|
||||||
/// Constructs a new bus
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new();
|
|
||||||
/// assert!(bus.is_empty());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Bus::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the length of the bus' backing memory
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..1234);
|
|
||||||
/// assert_eq!(1234, bus.len());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn len(&self) -> usize {
|
|
||||||
self.memory.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the backing memory contains no elements
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new();
|
|
||||||
/// assert!(bus.is_empty());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.memory.is_empty()
|
|
||||||
}
|
|
||||||
/// Grows the Bus backing memory to at least size bytes, but does not truncate
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let mut bus = Bus::new();
|
|
||||||
/// bus.with_size(1234);
|
|
||||||
/// assert_eq!(1234, bus.len());
|
|
||||||
/// bus.with_size(0);
|
|
||||||
/// assert_eq!(1234, bus.len());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn with_size(&mut self, size: usize) {
|
|
||||||
if self.len() < size {
|
|
||||||
self.memory.resize(size, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Adds a new named range (Region) to the bus
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new().add_region(Program, 0..1234);
|
|
||||||
/// assert_eq!(1234, bus.len());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn add_region(mut self, name: Region, range: Range<usize>) -> Self {
|
|
||||||
self.with_size(range.end);
|
|
||||||
if let Some(region) = self.region.get_mut(name as usize) {
|
|
||||||
*region = Some(range);
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Updates an existing named range (Region)
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new().add_region(Program, 0..1234);
|
|
||||||
/// assert_eq!(1234, bus.len());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn set_region(&mut self, name: Region, range: Range<usize>) -> &mut Self {
|
|
||||||
self.with_size(range.end);
|
|
||||||
if let Some(region) = self.region.get_mut(name as usize) {
|
|
||||||
*region = Some(range);
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Loads data into a named region
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..1234)
|
|
||||||
/// .load_region(Program, b"Hello, world!");
|
|
||||||
///# // TODO: Test if region actually contains "Hello, world!"
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn load_region(mut self, name: Region, data: &[u8]) -> Self {
|
|
||||||
use std::io::Write;
|
|
||||||
if let Some(mut region) = self.get_region_mut(name) {
|
|
||||||
region.write(data).ok(); // TODO: THIS SUCKS
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
/// Fills a named region with zeroes
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..1234)
|
|
||||||
/// .clear_region(Program);
|
|
||||||
///# // TODO: test if region actually clear
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
/// If the region doesn't exist, that's okay.
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..1234)
|
|
||||||
/// .clear_region(Screen);
|
|
||||||
///# // TODO: test if region actually clear
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn clear_region(&mut self, name: Region) -> &mut Self {
|
|
||||||
if let Some(region) = self.get_region_mut(name) {
|
|
||||||
region.fill(0)
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a slice of bus memory
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..10);
|
|
||||||
/// assert!([0;10].as_slice() == bus.get(0..10).unwrap());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn get<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
|
|
||||||
where
|
|
||||||
I: SliceIndex<[u8]>,
|
|
||||||
{
|
|
||||||
self.memory.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a mutable slice of bus memory
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let mut bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..10);
|
|
||||||
/// assert!([0;10].as_slice() == bus.get_mut(0..10).unwrap());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn get_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[u8]>>::Output>
|
|
||||||
where
|
|
||||||
I: SliceIndex<[u8]>,
|
|
||||||
{
|
|
||||||
self.memory.get_mut(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a slice of a named region of memory
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..10);
|
|
||||||
/// assert!([0;10].as_slice() == bus.get_region(Program).unwrap());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn get_region(&self, name: Region) -> Option<&[u8]> {
|
|
||||||
self.get(self.region.get(name as usize)?.clone()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets a mutable slice of a named region of memory
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let mut bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..10);
|
|
||||||
/// assert!([0;10].as_slice() == bus.get_region_mut(Program).unwrap());
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn get_region_mut(&mut self, name: Region) -> Option<&mut [u8]> {
|
|
||||||
self.get_mut(self.region.get(name as usize)?.clone()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prints the region of memory called `Screen` at 1bpp using box characters
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// [Bus::print_screen] will print the screen
|
|
||||||
/// ```rust
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let bus = Bus::new()
|
|
||||||
/// .add_region(Screen, 0x000..0x100);
|
|
||||||
/// bus.print_screen()?;
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
/// If there is no Screen region, it will return Err([MissingRegion])
|
|
||||||
/// ```rust,should_panic
|
|
||||||
///# use chirp::*;
|
|
||||||
///# fn main() -> Result<()> {
|
|
||||||
/// let mut bus = Bus::new()
|
|
||||||
/// .add_region(Program, 0..10);
|
|
||||||
/// bus.print_screen()?;
|
|
||||||
///# Ok(())
|
|
||||||
///# }
|
|
||||||
/// ```
|
|
||||||
pub fn print_screen(&self) -> Result<()> {
|
|
||||||
const REGION: Region = Region::Screen;
|
|
||||||
if let Some(screen) = self.get_region(REGION) {
|
|
||||||
let len_log2 = screen.len().ilog2() / 2;
|
|
||||||
#[allow(unused_variables)]
|
|
||||||
let (width, height) = (2u32.pow(len_log2 - 1), 2u32.pow(len_log2));
|
|
||||||
// draw with the drawille library, if available
|
|
||||||
#[cfg(feature = "drawille")]
|
|
||||||
{
|
|
||||||
use drawille::Canvas;
|
|
||||||
let mut canvas = Canvas::new(width * 8, height);
|
|
||||||
let width = width * 8;
|
|
||||||
screen
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.flat_map(|(bytei, byte)| {
|
|
||||||
(0..8)
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(move |(biti, bit)| {
|
|
||||||
if (byte << bit) & 0x80 != 0 {
|
|
||||||
Some(bytei * 8 + biti)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.for_each(|index| canvas.set(index as u32 % (width), index as u32 / (width)));
|
|
||||||
println!("{}", canvas.frame());
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "drawille"))]
|
|
||||||
for (index, byte) in screen.iter().enumerate() {
|
|
||||||
if index % width as usize == 0 {
|
|
||||||
print!("{index:03x}|");
|
|
||||||
}
|
|
||||||
print!(
|
|
||||||
"{}",
|
|
||||||
format!("{byte:08b}").replace('0', " ").replace('1', "█")
|
|
||||||
);
|
|
||||||
if index % width as usize == width as usize - 1 {
|
|
||||||
println!("|");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Err(MissingRegion { region: REGION });
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read<u8> for Bus {
|
|
||||||
/// Read a u8 from address `addr`
|
|
||||||
fn read(&self, addr: impl Into<usize>) -> u8 {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
*self.memory.get(addr).unwrap_or(&0xc5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read<u16> for Bus {
|
|
||||||
/// Read a u16 from address `addr`
|
|
||||||
fn read(&self, addr: impl Into<usize>) -> u16 {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(bytes) = self.memory.get(addr..addr + 2) {
|
|
||||||
u16::from_be_bytes(bytes.try_into().expect("Should get 2 bytes"))
|
|
||||||
} else {
|
|
||||||
0xc5c5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read<u32> for Bus {
|
|
||||||
/// Read a u16 from address `addr`
|
|
||||||
fn read(&self, addr: impl Into<usize>) -> u32 {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(bytes) = self.memory.get(addr..addr + 4) {
|
|
||||||
u32::from_be_bytes(bytes.try_into().expect("Should get 4 bytes"))
|
|
||||||
} else {
|
|
||||||
0xc5c5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read<u64> for Bus {
|
|
||||||
/// Read a u16 from address `addr`
|
|
||||||
fn read(&self, addr: impl Into<usize>) -> u64 {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(bytes) = self.memory.get(addr..addr + 8) {
|
|
||||||
u64::from_be_bytes(bytes.try_into().expect("Should get 8 bytes"))
|
|
||||||
} else {
|
|
||||||
0xc5c5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Read<u128> for Bus {
|
|
||||||
/// Read a u16 from address `addr`
|
|
||||||
fn read(&self, addr: impl Into<usize>) -> u128 {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(bytes) = self.memory.get(addr..addr + 16) {
|
|
||||||
u128::from_be_bytes(bytes.try_into().expect("Should get 16 bytes"))
|
|
||||||
} else {
|
|
||||||
0xc5c5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write<u8> for Bus {
|
|
||||||
/// Write a u8 to address `addr`
|
|
||||||
fn write(&mut self, addr: impl Into<usize>, data: u8) {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(byte) = self.get_mut(addr) {
|
|
||||||
*byte = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write<u16> for Bus {
|
|
||||||
/// Write a u16 to address `addr`
|
|
||||||
fn write(&mut self, addr: impl Into<usize>, data: u16) {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(slice) = self.get_mut(addr..addr + 2) {
|
|
||||||
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write<u32> for Bus {
|
|
||||||
/// Write a u16 to address `addr`
|
|
||||||
fn write(&mut self, addr: impl Into<usize>, data: u32) {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(slice) = self.get_mut(addr..addr + 4) {
|
|
||||||
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write<u64> for Bus {
|
|
||||||
/// Write a u16 to address `addr`
|
|
||||||
fn write(&mut self, addr: impl Into<usize>, data: u64) {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(slice) = self.get_mut(addr..addr + 8) {
|
|
||||||
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write<u128> for Bus {
|
|
||||||
/// Write a u16 to address `addr`
|
|
||||||
fn write(&mut self, addr: impl Into<usize>, data: u128) {
|
|
||||||
let addr: usize = addr.into();
|
|
||||||
if let Some(slice) = self.get_mut(addr..addr + 16) {
|
|
||||||
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_feature = "rhexdump")]
|
|
||||||
impl Display for Bus {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
use rhexdump::Rhexdump;
|
|
||||||
let mut rhx = Rhexdump::default();
|
|
||||||
rhx.set_bytes_per_group(2)
|
|
||||||
.expect("2 <= MAX_BYTES_PER_GROUP (8)");
|
|
||||||
rhx.display_duplicate_lines(false);
|
|
||||||
for (&name, range) in &self.region {
|
|
||||||
writeln!(
|
|
||||||
f,
|
|
||||||
"[{name}]\n{}\n",
|
|
||||||
rhx.hexdump(&self.memory[range.clone()])
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
write!(f, "")
|
|
||||||
}
|
|
||||||
}
|
|
352
src/cpu.rs
352
src/cpu.rs
@ -1,114 +1,141 @@
|
|||||||
// (c) 2023 John A. Breaux
|
// (c) 2023 John A. Breaux
|
||||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Decodes and runs instructions
|
//! Decodes and runs instructions
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub mod disassembler;
|
mod behavior;
|
||||||
pub mod flags;
|
pub mod flags;
|
||||||
pub mod instruction;
|
pub mod instruction;
|
||||||
|
#[macro_use]
|
||||||
|
pub mod mem;
|
||||||
pub mod mode;
|
pub mod mode;
|
||||||
pub mod quirks;
|
pub mod quirks;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
disassembler::{Dis, Disassembler, Insn},
|
|
||||||
flags::Flags,
|
flags::Flags,
|
||||||
|
instruction::{
|
||||||
|
disassembler::{Dis, Disassembler},
|
||||||
|
Insn,
|
||||||
|
},
|
||||||
|
mem::{Mem, Region::*},
|
||||||
mode::Mode,
|
mode::Mode,
|
||||||
quirks::Quirks,
|
quirks::Quirks,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
bus::{Bus, Read, Region, Write},
|
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
|
screen::Screen,
|
||||||
|
traits::{AutoCast, Grab},
|
||||||
};
|
};
|
||||||
use imperative_rs::InstructionSet;
|
use imperative_rs::InstructionSet;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use rand::random;
|
use std::fmt::Debug;
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
type Reg = usize;
|
type Reg = usize;
|
||||||
type Adr = u16;
|
type Adr = u16;
|
||||||
type Nib = u8;
|
type Nib = u8;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
struct Timers {
|
|
||||||
frame: Instant,
|
|
||||||
insn: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Timers {
|
|
||||||
fn default() -> Self {
|
|
||||||
let now = Instant::now();
|
|
||||||
Self {
|
|
||||||
frame: now,
|
|
||||||
insn: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the internal state of the CPU interpreter
|
/// Represents the internal state of the CPU interpreter
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct CPU {
|
pub struct CPU {
|
||||||
/// Flags that control how the CPU behaves, but which aren't inherent to the
|
/// Flags that control how the CPU behaves, but which aren't inherent to the
|
||||||
/// chip-8. Includes [Quirks], target IPF, etc.
|
/// chip-8. Includes [Quirks], target IPF, etc.
|
||||||
pub flags: Flags,
|
pub flags: Flags,
|
||||||
// memory map info
|
// memory map info
|
||||||
screen: Adr,
|
mem: Mem,
|
||||||
font: Adr,
|
font: Adr,
|
||||||
|
// memory
|
||||||
|
stack: Vec<Adr>,
|
||||||
// registers
|
// registers
|
||||||
pc: Adr,
|
pc: Adr,
|
||||||
sp: Adr,
|
|
||||||
i: Adr,
|
i: Adr,
|
||||||
v: [u8; 16],
|
v: [u8; 16],
|
||||||
delay: f64,
|
delay: u8,
|
||||||
sound: f64,
|
sound: u8,
|
||||||
// I/O
|
// I/O
|
||||||
keys: [bool; 16],
|
keys: [bool; 16],
|
||||||
|
/// Set to the last key that's been *released* after a keypause
|
||||||
|
pub lastkey: Option<usize>,
|
||||||
// Execution data
|
// Execution data
|
||||||
timers: Timers,
|
|
||||||
cycle: usize,
|
cycle: usize,
|
||||||
breakpoints: Vec<Adr>,
|
breakpoints: Vec<Adr>,
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
disassembler: Dis,
|
disassembler: Dis,
|
||||||
}
|
}
|
||||||
|
|
||||||
// public interface
|
// public interface
|
||||||
impl CPU {
|
impl CPU {
|
||||||
// TODO: implement From<&bus> for CPU
|
|
||||||
/// Constructs a new CPU, taking all configurable parameters
|
/// Constructs a new CPU, taking all configurable parameters
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use chirp::*;
|
/// # use chirp::*;
|
||||||
/// let cpu = CPU::new(
|
/// let cpu = CPU::new(
|
||||||
/// 0xf00, // screen location
|
/// None::<&str>, // ROM to load
|
||||||
/// 0x50, // font location
|
/// 0x50, // font location
|
||||||
/// 0x200, // start of program
|
/// 0x200, // start of program
|
||||||
/// 0xefe, // top of stack
|
|
||||||
/// Dis::default(),
|
/// Dis::default(),
|
||||||
/// vec![], // Breakpoints
|
/// vec![], // Breakpoints
|
||||||
/// ControlFlags::default()
|
/// Flags::default()
|
||||||
/// );
|
/// );
|
||||||
/// dbg!(cpu);
|
/// dbg!(cpu);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new(
|
pub fn new(
|
||||||
screen: Adr,
|
rom: Option<impl AsRef<std::path::Path>>,
|
||||||
font: Adr,
|
font: Adr,
|
||||||
pc: Adr,
|
pc: Adr,
|
||||||
sp: Adr,
|
|
||||||
disassembler: Dis,
|
disassembler: Dis,
|
||||||
breakpoints: Vec<Adr>,
|
breakpoints: Vec<Adr>,
|
||||||
flags: Flags,
|
flags: Flags,
|
||||||
) -> Self {
|
) -> Result<Self> {
|
||||||
CPU {
|
const CHARSET: &[u8] = include_bytes!("mem/charset.bin");
|
||||||
|
let mem = mem! {
|
||||||
|
Charset [font as usize..font as usize + CHARSET.len()] = CHARSET,
|
||||||
|
Program [pc as usize..0x1000],
|
||||||
|
};
|
||||||
|
let mut cpu = CPU {
|
||||||
disassembler,
|
disassembler,
|
||||||
screen,
|
|
||||||
font,
|
font,
|
||||||
pc,
|
pc,
|
||||||
sp,
|
|
||||||
breakpoints,
|
breakpoints,
|
||||||
flags,
|
flags,
|
||||||
|
mem,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
};
|
||||||
|
// load the provided rom
|
||||||
|
if let Some(rom) = rom {
|
||||||
|
cpu.load_program(rom)?;
|
||||||
}
|
}
|
||||||
|
Ok(cpu)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a program into the CPU's program space
|
||||||
|
pub fn load_program(&mut self, rom: impl AsRef<std::path::Path>) -> Result<&mut Self> {
|
||||||
|
self.load_program_bytes(&std::fs::read(rom)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads bytes into the CPU's program space
|
||||||
|
pub fn load_program_bytes(&mut self, rom: &[u8]) -> Result<&mut Self> {
|
||||||
|
self.mem.clear_region(Program);
|
||||||
|
self.mem.load_region(Program, rom)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pokes a value into memory
|
||||||
|
pub fn poke(&mut self, addr: Adr, data: u8) {
|
||||||
|
self.mem.write(addr, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Peeks a value from memory
|
||||||
|
pub fn peek(&mut self, addr: Adr) -> u8 {
|
||||||
|
self.mem.read(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grabs a reference to the [CPU]'s memory
|
||||||
|
pub fn introspect(&mut self) -> &Mem {
|
||||||
|
&self.mem
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Presses a key, and reports whether the key's state changed.
|
/// Presses a key, and reports whether the key's state changed.
|
||||||
@ -143,8 +170,8 @@ impl CPU {
|
|||||||
/// Releases a key, and reports whether the key's state changed.
|
/// Releases a key, and reports whether the key's state changed.
|
||||||
/// If key is outside range `0..=0xF`, returns [Error::InvalidKey].
|
/// If key is outside range `0..=0xF`, returns [Error::InvalidKey].
|
||||||
///
|
///
|
||||||
/// If [ControlFlags::keypause] was enabled, it is disabled,
|
/// If [Flags::keypause] was enabled, it is disabled,
|
||||||
/// and the [ControlFlags::lastkey] is recorded.
|
/// and the lastkey is recorded.
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use chirp::*;
|
/// # use chirp::*;
|
||||||
@ -163,7 +190,7 @@ impl CPU {
|
|||||||
if *keyref {
|
if *keyref {
|
||||||
*keyref = false;
|
*keyref = false;
|
||||||
if self.flags.keypause {
|
if self.flags.keypause {
|
||||||
self.flags.lastkey = Some(key);
|
self.lastkey = Some(key);
|
||||||
self.flags.keypause = false;
|
self.flags.keypause = false;
|
||||||
}
|
}
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
@ -240,7 +267,7 @@ impl CPU {
|
|||||||
/// assert_eq!(0, cpu.sound());
|
/// assert_eq!(0, cpu.sound());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn sound(&self) -> u8 {
|
pub fn sound(&self) -> u8 {
|
||||||
self.sound as u8
|
self.sound
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the value in the Delay Timer register
|
/// Gets the value in the Delay Timer register
|
||||||
@ -251,7 +278,7 @@ impl CPU {
|
|||||||
/// assert_eq!(0, cpu.delay());
|
/// assert_eq!(0, cpu.delay());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn delay(&self) -> u8 {
|
pub fn delay(&self) -> u8 {
|
||||||
self.delay as u8
|
self.delay
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the number of cycles the CPU has executed
|
/// Gets the number of cycles the CPU has executed
|
||||||
@ -274,14 +301,13 @@ impl CPU {
|
|||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use chirp::*;
|
/// # use chirp::*;
|
||||||
/// let mut cpu = CPU::new(
|
/// let mut cpu = CPU::new(
|
||||||
/// 0xf00,
|
/// None::<&str>,
|
||||||
/// 0x50,
|
/// 0x50,
|
||||||
/// 0x340,
|
/// 0x340,
|
||||||
/// 0xefe,
|
|
||||||
/// Dis::default(),
|
/// Dis::default(),
|
||||||
/// vec![],
|
/// vec![],
|
||||||
/// ControlFlags::default()
|
/// Flags::default()
|
||||||
/// );
|
/// ).unwrap();
|
||||||
/// cpu.flags.keypause = true;
|
/// cpu.flags.keypause = true;
|
||||||
/// cpu.flags.draw_wait = true;
|
/// cpu.flags.draw_wait = true;
|
||||||
/// assert_eq!(0x340, cpu.pc());
|
/// assert_eq!(0x340, cpu.pc());
|
||||||
@ -296,6 +322,35 @@ impl CPU {
|
|||||||
self.flags.draw_wait = false;
|
self.flags.draw_wait = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resets the emulator.
|
||||||
|
///
|
||||||
|
/// Touches the [Flags] (keypause, draw_wait, draw_mode, and lastkey),
|
||||||
|
/// stack, pc, registers, keys, and cycle count.
|
||||||
|
///
|
||||||
|
/// Does not touch [Quirks], [Mode], [Dis], breakpoints, or memory map.
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.flags = Flags {
|
||||||
|
keypause: false,
|
||||||
|
draw_wait: false,
|
||||||
|
draw_mode: false,
|
||||||
|
..self.flags
|
||||||
|
};
|
||||||
|
// clear the stack
|
||||||
|
self.stack.truncate(0);
|
||||||
|
// Reset the program counter
|
||||||
|
self.pc = 0x200;
|
||||||
|
// Zero the registers
|
||||||
|
self.i = 0;
|
||||||
|
self.v = [0; 16];
|
||||||
|
self.delay = 0;
|
||||||
|
self.sound = 0;
|
||||||
|
// I/O
|
||||||
|
self.keys = [false; 16];
|
||||||
|
self.lastkey = None;
|
||||||
|
// Execution data
|
||||||
|
self.cycle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// Set a breakpoint
|
/// Set a breakpoint
|
||||||
// TODO: Unit test this
|
// TODO: Unit test this
|
||||||
pub fn set_break(&mut self, point: Adr) -> &mut Self {
|
pub fn set_break(&mut self, point: Adr) -> &mut Self {
|
||||||
@ -344,18 +399,13 @@ impl CPU {
|
|||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use chirp::*;
|
/// # use chirp::*;
|
||||||
/// let mut cpu = CPU::default();
|
/// let mut cpu = CPU::default();
|
||||||
/// let mut bus = bus!{
|
/// let mut screen = Screen::default();
|
||||||
/// Program [0x0200..0x0f00] = &[
|
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
|
||||||
/// 0x00, 0xe0, // cls
|
/// cpu.singlestep(&mut screen).unwrap();
|
||||||
/// 0x22, 0x02, // jump 0x202 (pc)
|
|
||||||
/// ],
|
|
||||||
/// Screen [0x0f00..0x1000],
|
|
||||||
/// };
|
|
||||||
/// cpu.singlestep(&mut bus).unwrap();
|
|
||||||
/// assert_eq!(0x202, cpu.pc());
|
/// assert_eq!(0x202, cpu.pc());
|
||||||
/// assert_eq!(1, cpu.cycle());
|
/// assert_eq!(1, cpu.cycle());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn singlestep(&mut self, bus: &mut Bus) -> Result<&mut Self> {
|
pub fn singlestep(&mut self, bus: &mut Screen) -> Result<&mut Self> {
|
||||||
self.flags.pause = false;
|
self.flags.pause = false;
|
||||||
self.tick(bus)?;
|
self.tick(bus)?;
|
||||||
self.flags.draw_wait = false;
|
self.flags.draw_wait = false;
|
||||||
@ -370,65 +420,25 @@ impl CPU {
|
|||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use chirp::*;
|
/// # use chirp::*;
|
||||||
/// let mut cpu = CPU::default();
|
/// let mut cpu = CPU::default();
|
||||||
/// let mut bus = bus!{
|
/// let mut screen = Screen::default();
|
||||||
/// Program [0x0200..0x0f00] = &[
|
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
|
||||||
/// 0x00, 0xe0, // cls
|
/// cpu.multistep(&mut screen, 0x20)
|
||||||
/// 0x22, 0x02, // jump 0x202 (pc)
|
|
||||||
/// ],
|
|
||||||
/// Screen [0x0f00..0x1000],
|
|
||||||
/// };
|
|
||||||
/// cpu.multistep(&mut bus, 0x20)
|
|
||||||
/// .expect("The program should only have valid opcodes.");
|
/// .expect("The program should only have valid opcodes.");
|
||||||
/// assert_eq!(0x202, cpu.pc());
|
/// assert_eq!(0x202, cpu.pc());
|
||||||
/// assert_eq!(0x20, cpu.cycle());
|
/// assert_eq!(0x20, cpu.cycle());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn multistep(&mut self, bus: &mut Bus, steps: usize) -> Result<&mut Self> {
|
pub fn multistep(&mut self, screen: &mut Screen, steps: usize) -> Result<&mut Self> {
|
||||||
|
//let speed = 1.0 / steps as f64;
|
||||||
for _ in 0..steps {
|
for _ in 0..steps {
|
||||||
self.tick(bus)?;
|
self.tick(screen)?;
|
||||||
self.vertical_blank();
|
if self.flags.is_paused() {
|
||||||
}
|
break;
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Simulates vertical blanking
|
|
||||||
///
|
|
||||||
/// If monotonic timing is `enabled`:
|
|
||||||
/// - Ticks the sound and delay timers according to CPU cycle count
|
|
||||||
/// - Disables framepause
|
|
||||||
/// If monotonic timing is `disabled`:
|
|
||||||
/// - Subtracts the elapsed time in fractions of a frame
|
|
||||||
/// from st/dt
|
|
||||||
/// - Disables framepause if the duration exceeds that of a frame
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn vertical_blank(&mut self) -> &mut Self {
|
|
||||||
if self.flags.pause {
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
// Use a monotonic counter when testing
|
|
||||||
if let Some(speed) = self.flags.monotonic {
|
|
||||||
if self.flags.draw_wait {
|
|
||||||
self.flags.draw_wait = self.cycle % speed != 0;
|
|
||||||
}
|
}
|
||||||
let speed = 1.0 / speed as f64;
|
|
||||||
self.delay -= speed;
|
|
||||||
self.sound -= speed;
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert the elapsed time to 60ths of a second
|
|
||||||
let frame = Instant::now();
|
|
||||||
let time = (frame - self.timers.frame).as_secs_f64() * 60.0;
|
|
||||||
self.timers.frame = frame;
|
|
||||||
if time > 1.0 {
|
|
||||||
self.flags.draw_wait = false;
|
|
||||||
}
|
}
|
||||||
if self.delay > 0.0 {
|
self.delay = self.delay.saturating_sub(1);
|
||||||
self.delay -= time;
|
self.sound = self.sound.saturating_sub(1);
|
||||||
}
|
self.flags.draw_wait = false;
|
||||||
if self.sound > 0.0 {
|
Ok(self)
|
||||||
self.sound -= time;
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes a single instruction
|
/// Executes a single instruction
|
||||||
@ -437,80 +447,69 @@ impl CPU {
|
|||||||
/// This result contains information about the breakpoint, but can be safely ignored.
|
/// This result contains information about the breakpoint, but can be safely ignored.
|
||||||
///
|
///
|
||||||
/// Returns [Error::UnimplementedInstruction] if the instruction at `pc` is unimplemented.
|
/// Returns [Error::UnimplementedInstruction] if the instruction at `pc` is unimplemented.
|
||||||
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use chirp::*;
|
/// # use chirp::*;
|
||||||
/// let mut cpu = CPU::default();
|
/// let mut cpu = CPU::default();
|
||||||
/// let mut bus = bus!{
|
/// let mut screen = Screen::default();
|
||||||
/// Program [0x0200..0x0f00] = &[
|
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
|
||||||
/// 0x00, 0xe0, // cls
|
/// cpu.tick(&mut screen)
|
||||||
/// 0x22, 0x02, // jump 0x202 (pc)
|
|
||||||
/// ],
|
|
||||||
/// Screen [0x0f00..0x1000],
|
|
||||||
/// };
|
|
||||||
/// cpu.tick(&mut bus)
|
|
||||||
/// .expect("0x00e0 (cls) should be a valid opcode.");
|
/// .expect("0x00e0 (cls) should be a valid opcode.");
|
||||||
/// assert_eq!(0x202, cpu.pc());
|
/// assert_eq!(0x202, cpu.pc());
|
||||||
/// assert_eq!(1, cpu.cycle());
|
/// assert_eq!(1, cpu.cycle());
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
/// Returns [Error::UnimplementedInstruction] if the instruction is not implemented.
|
/// Returns [Error::UnimplementedInstruction] if the instruction is not implemented.
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use chirp::*;
|
/// # use chirp::*;
|
||||||
/// # use chirp::error::Error;
|
/// # use chirp::error::Error;
|
||||||
/// let mut cpu = CPU::default();
|
/// let mut cpu = CPU::default();
|
||||||
/// # cpu.flags.debug = true; // enable live disassembly
|
/// # cpu.flags.debug = true; // enable live disassembly
|
||||||
/// # cpu.flags.monotonic = Some(8); // enable monotonic/test timing
|
/// # cpu.flags.monotonic = true; // enable monotonic/test timing
|
||||||
/// let mut bus = bus!{
|
/// let mut bus = Screen::default();
|
||||||
/// Program [0x0200..0x0f00] = &[
|
/// cpu.load_program_bytes(&[
|
||||||
/// 0xff, 0xff, // invalid!
|
/// 0xff, 0xff, // invalid!
|
||||||
/// 0x22, 0x02, // jump 0x202 (pc)
|
/// 0x22, 0x02, // jump 0x202
|
||||||
/// ],
|
/// ]);
|
||||||
/// Screen [0x0f00..0x1000],
|
|
||||||
/// };
|
|
||||||
/// dbg!(cpu.tick(&mut bus))
|
/// dbg!(cpu.tick(&mut bus))
|
||||||
/// .expect_err("Should return Error::InvalidInstruction { 0xffff }");
|
/// .expect_err("Should return Error::InvalidInstruction { 0xffff }");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn tick(&mut self, bus: &mut Bus) -> Result<&mut Self> {
|
pub fn tick(&mut self, screen: &mut Screen) -> Result<&mut Self> {
|
||||||
// Do nothing if paused
|
// Do nothing if paused
|
||||||
if self.flags.is_paused() {
|
if self.flags.is_paused() {
|
||||||
// always tick in test mode
|
// always tick in test mode
|
||||||
if self.flags.monotonic.is_some() {
|
if self.flags.monotonic {
|
||||||
self.cycle += 1;
|
self.cycle += 1;
|
||||||
}
|
}
|
||||||
return Ok(self);
|
return Ok(self);
|
||||||
}
|
}
|
||||||
self.cycle += 1;
|
self.cycle += 1;
|
||||||
// fetch opcode
|
// Fetch slice of memory starting at pc, for var-width opcode 0xf000_iiii
|
||||||
let opcode: &[u8; 2] = if let Some(slice) = bus.get(self.pc as usize..self.pc as usize + 2)
|
let opchunk = self
|
||||||
{
|
.mem
|
||||||
slice
|
.grab(self.pc as usize..)
|
||||||
.try_into()
|
.ok_or(Error::InvalidAddressRange {
|
||||||
.expect("`slice` should be exactly 2 bytes.")
|
range: (self.pc as usize..).into(),
|
||||||
} else {
|
})?;
|
||||||
return Err(Error::InvalidBusRange {
|
|
||||||
range: self.pc as usize..self.pc as usize + 2,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Print opcode disassembly:
|
// Print opcode disassembly:
|
||||||
if self.flags.debug {
|
if self.flags.debug {
|
||||||
println!("{:?}", self.timers.insn.elapsed().bright_black());
|
std::println!(
|
||||||
self.timers.insn = Instant::now();
|
|
||||||
std::print!(
|
|
||||||
"{:3} {:03x}: {:<36}",
|
"{:3} {:03x}: {:<36}",
|
||||||
self.cycle.bright_black(),
|
self.cycle.bright_black(),
|
||||||
self.pc,
|
self.pc,
|
||||||
self.disassembler.once(u16::from_be_bytes(*opcode))
|
self.disassembler.once(self.mem.read(self.pc))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode opcode
|
// decode opcode
|
||||||
if let Ok((inc, insn)) = Insn::decode(opcode) {
|
if let Ok((inc, insn)) = Insn::decode(opchunk) {
|
||||||
self.pc = self.pc.wrapping_add(inc as u16);
|
self.pc = self.pc.wrapping_add(inc as u16);
|
||||||
self.execute(bus, insn);
|
self.execute(screen, insn);
|
||||||
} else {
|
} else {
|
||||||
return Err(Error::UnimplementedInstruction {
|
return Err(Error::UnimplementedInstruction {
|
||||||
word: u16::from_be_bytes(*opcode),
|
word: self.mem.read(self.pc),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,7 +518,7 @@ impl CPU {
|
|||||||
self.flags.pause = true;
|
self.flags.pause = true;
|
||||||
return Err(Error::BreakpointHit {
|
return Err(Error::BreakpointHit {
|
||||||
addr: self.pc,
|
addr: self.pc,
|
||||||
next: bus.read(self.pc),
|
next: self.mem.read(self.pc),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
@ -543,31 +542,52 @@ impl CPU {
|
|||||||
/// ```
|
/// ```
|
||||||
pub fn dump(&self) {
|
pub fn dump(&self) {
|
||||||
//let dumpstyle = owo_colors::Style::new().bright_black();
|
//let dumpstyle = owo_colors::Style::new().bright_black();
|
||||||
|
use std::fmt::Write;
|
||||||
std::println!(
|
std::println!(
|
||||||
"PC: {:04x}, SP: {:04x}, I: {:04x}\n{}DLY: {}, SND: {}, CYC: {:6}",
|
"PC: {:04x}, SP: {:04x}, I: {:04x}\n{}DLY: {}, SND: {}, CYC: {:6}",
|
||||||
self.pc,
|
self.pc,
|
||||||
self.sp,
|
self.stack.len(),
|
||||||
self.i,
|
self.i,
|
||||||
self.v
|
self.v
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, gpr)| {
|
.fold(String::new(), |mut s, (i, gpr)| {
|
||||||
format!(
|
let _ = write!(
|
||||||
"v{i:X}: {gpr:02x} {}",
|
s,
|
||||||
|
"{}v{i:X}: {gpr:02x}",
|
||||||
match i % 4 {
|
match i % 4 {
|
||||||
3 => "\n",
|
0 => "\n",
|
||||||
_ => "",
|
_ => "",
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
})
|
s
|
||||||
.collect::<String>(),
|
}),
|
||||||
self.delay as u8,
|
self.delay,
|
||||||
self.sound as u8,
|
self.sound,
|
||||||
self.cycle,
|
self.cycle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Debug for CPU {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("CPU")
|
||||||
|
.field("flags", &self.flags)
|
||||||
|
.field("font", &self.font)
|
||||||
|
.field("stack", &self.stack)
|
||||||
|
.field("pc", &self.pc)
|
||||||
|
.field("i", &self.i)
|
||||||
|
.field("v", &self.v)
|
||||||
|
.field("delay", &self.delay)
|
||||||
|
.field("sound", &self.sound)
|
||||||
|
.field("keys", &self.keys)
|
||||||
|
.field("cycle", &self.cycle)
|
||||||
|
.field("breakpoints", &self.breakpoints)
|
||||||
|
.field("disassembler", &self.disassembler)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for CPU {
|
impl Default for CPU {
|
||||||
/// Constructs a new CPU with sane defaults and debug mode ON
|
/// Constructs a new CPU with sane defaults and debug mode ON
|
||||||
///
|
///
|
||||||
@ -576,8 +596,6 @@ impl Default for CPU {
|
|||||||
/// | screen |`0x0f00` | Location of screen memory.
|
/// | screen |`0x0f00` | Location of screen memory.
|
||||||
/// | font |`0x0050` | Location of font memory.
|
/// | font |`0x0050` | Location of font memory.
|
||||||
/// | pc |`0x0200` | Start location. Generally 0x200 or 0x600.
|
/// | pc |`0x0200` | Start location. Generally 0x200 or 0x600.
|
||||||
/// | sp |`0x0efe` | Initial top of stack.
|
|
||||||
///
|
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```rust
|
/// ```rust
|
||||||
@ -586,21 +604,25 @@ impl Default for CPU {
|
|||||||
/// ```
|
/// ```
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
CPU {
|
CPU {
|
||||||
screen: 0xf00,
|
stack: vec![],
|
||||||
|
mem: mem! {
|
||||||
|
Charset [0x0050..0x00a0] = include_bytes!("mem/charset.bin"),
|
||||||
|
HiresCharset [0x00a0..0x0140] = include_bytes!("mem/hires.bin"),
|
||||||
|
Program [0x0200..0x1000],
|
||||||
|
},
|
||||||
font: 0x050,
|
font: 0x050,
|
||||||
pc: 0x200,
|
pc: 0x200,
|
||||||
sp: 0xefe,
|
|
||||||
i: 0,
|
i: 0,
|
||||||
v: [0; 16],
|
v: [0; 16],
|
||||||
delay: 0.0,
|
delay: 0,
|
||||||
sound: 0.0,
|
sound: 0,
|
||||||
cycle: 0,
|
cycle: 0,
|
||||||
keys: [false; 16],
|
keys: [false; 16],
|
||||||
|
lastkey: None,
|
||||||
flags: Flags {
|
flags: Flags {
|
||||||
debug: true,
|
debug: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
timers: Default::default(),
|
|
||||||
breakpoints: vec![],
|
breakpoints: vec![],
|
||||||
disassembler: Dis::default(),
|
disassembler: Dis::default(),
|
||||||
}
|
}
|
||||||
|
750
src/cpu/behavior.rs
Normal file
750
src/cpu/behavior.rs
Normal file
@ -0,0 +1,750 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
|
//! Contains implementations for each Chip-8 [Insn]
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::traits::Grab;
|
||||||
|
use rand::random;
|
||||||
|
|
||||||
|
impl CPU {
|
||||||
|
/// Executes a single [Insn]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn execute(&mut self, screen: &mut Screen, instruction: Insn) {
|
||||||
|
match instruction {
|
||||||
|
// Core Chip-8 instructions
|
||||||
|
Insn::cls => self.clear_screen(screen),
|
||||||
|
Insn::ret => self.ret(),
|
||||||
|
Insn::jmp { A } => self.jump(A),
|
||||||
|
Insn::call { A } => self.call(A),
|
||||||
|
Insn::seb { x, B } => self.skip_equals_immediate(x, B),
|
||||||
|
Insn::sneb { x, B } => self.skip_not_equals_immediate(x, B),
|
||||||
|
Insn::se { y, x } => self.skip_equals(x, y),
|
||||||
|
Insn::movb { x, B } => self.load_immediate(x, B),
|
||||||
|
Insn::addb { x, B } => self.add_immediate(x, B),
|
||||||
|
Insn::mov { y, x } => self.load(x, y),
|
||||||
|
Insn::or { y, x } => self.or(x, y),
|
||||||
|
Insn::and { y, x } => self.and(x, y),
|
||||||
|
Insn::xor { y, x } => self.xor(x, y),
|
||||||
|
Insn::add { y, x } => self.add(x, y),
|
||||||
|
Insn::sub { y, x } => self.sub(x, y),
|
||||||
|
Insn::shr { y, x } => self.shift_right(x, y),
|
||||||
|
Insn::bsub { y, x } => self.backwards_sub(x, y),
|
||||||
|
Insn::shl { y, x } => self.shift_left(x, y),
|
||||||
|
Insn::sne { y, x } => self.skip_not_equals(x, y),
|
||||||
|
Insn::movI { A } => self.load_i_immediate(A),
|
||||||
|
Insn::jmpr { A } => self.jump_indexed(A),
|
||||||
|
Insn::rand { x, B } => self.rand(x, B),
|
||||||
|
Insn::draw { y, x, n } => self.draw(x, y, n, screen),
|
||||||
|
Insn::sek { x } => self.skip_key_equals(x),
|
||||||
|
Insn::snek { x } => self.skip_key_not_equals(x),
|
||||||
|
Insn::getdt { x } => self.load_delay_timer(x),
|
||||||
|
Insn::waitk { x } => self.wait_for_key(x),
|
||||||
|
Insn::setdt { x } => self.store_delay_timer(x),
|
||||||
|
Insn::movst { x } => self.store_sound_timer(x),
|
||||||
|
Insn::addI { x } => self.add_i(x),
|
||||||
|
Insn::font { x } => self.load_sprite(x),
|
||||||
|
Insn::bcd { x } => self.bcd_convert(x),
|
||||||
|
Insn::dmao { x } => self.store_dma(x),
|
||||||
|
Insn::dmai { x } => self.load_dma(x),
|
||||||
|
// Super-Chip extensions
|
||||||
|
Insn::scd { n } => self.scroll_down(n, screen),
|
||||||
|
Insn::scr => self.scroll_right(screen),
|
||||||
|
Insn::scl => self.scroll_left(screen),
|
||||||
|
Insn::halt => self.flags.pause(),
|
||||||
|
Insn::lores => self.init_lores(screen),
|
||||||
|
Insn::hires => self.init_hires(screen),
|
||||||
|
Insn::hfont { x } => self.load_big_sprite(x),
|
||||||
|
Insn::flgo { x } => self.store_flags(x),
|
||||||
|
Insn::flgi { x } => self.load_flags(x),
|
||||||
|
// XO-Chip extensions
|
||||||
|
_ => unimplemented!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`0aaa`| Issues a "System call" (ML routine)
|
||||||
|
///
|
||||||
|
/// |opcode| effect |
|
||||||
|
/// |------|------------------------------------|
|
||||||
|
/// |`00e0`| Clear screen memory to all 0 |
|
||||||
|
/// |`00ee`| Return from subroutine |
|
||||||
|
impl CPU {
|
||||||
|
/// |`00e0`| Clears the screen memory to 0
|
||||||
|
/// Corresponds to [Insn::cls]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn clear_screen(&mut self, screen: &mut Screen) {
|
||||||
|
screen.clear()
|
||||||
|
}
|
||||||
|
/// |`00ee`| Returns from subroutine
|
||||||
|
/// Corresponds to [Insn::ret]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn ret(&mut self) {
|
||||||
|
self.pc = self.stack.pop().unwrap_or(0x200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Super Chip screen-control routines
|
||||||
|
///
|
||||||
|
/// |opcode| effect |
|
||||||
|
/// |------|------------------------------------|
|
||||||
|
/// |`00cN`| Scroll the screen down N lines |
|
||||||
|
/// |`00fb`| Scroll the screen right |
|
||||||
|
/// |`00fc`| Scroll the screen left |
|
||||||
|
/// |`00fe`| Initialize lores mode |
|
||||||
|
/// |`00ff`| Initialize hires mode |
|
||||||
|
impl CPU {
|
||||||
|
/// # |`00cN`|
|
||||||
|
/// Scroll the screen down N lines
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::scd]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn scroll_down(&mut self, n: Nib, screen: &mut Screen) {
|
||||||
|
match self.flags.draw_mode {
|
||||||
|
true => {
|
||||||
|
// Get a line from the bus
|
||||||
|
for i in (0..16 * (64 - n as usize)).step_by(16).rev() {
|
||||||
|
let line: u128 = screen.read(i);
|
||||||
|
screen.write(i - (n as usize * 16), 0u128);
|
||||||
|
screen.write(i, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false => {
|
||||||
|
// Get a line from the bus
|
||||||
|
for i in (0..8 * (32 - n as usize)).step_by(8).rev() {
|
||||||
|
let line: u64 = screen.read(i);
|
||||||
|
screen.write(i, 0u64);
|
||||||
|
screen.write(i + (n as usize * 8), line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # |`00fb`|
|
||||||
|
/// Scroll the screen right
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::scr]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn scroll_right(&mut self, screen: &mut impl AutoCast<u128>) {
|
||||||
|
// Get a line from the bus
|
||||||
|
for i in (0..16 * 64_usize).step_by(16) {
|
||||||
|
//let line: u128 = bus.read(self.screen + i) >> 4;
|
||||||
|
screen.write(i, screen.read(i) >> 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// # |`00fc`|
|
||||||
|
/// Scroll the screen left
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::scl]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn scroll_left(&mut self, screen: &mut impl AutoCast<u128>) {
|
||||||
|
// Get a line from the bus
|
||||||
|
for i in (0..16 * 64_usize).step_by(16) {
|
||||||
|
let line: u128 = u128::wrapping_shl(screen.read(i), 4);
|
||||||
|
screen.write(i, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// # |`00fe`|
|
||||||
|
/// Initialize lores mode
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::lores]
|
||||||
|
pub(super) fn init_lores(&mut self, screen: &mut Screen) {
|
||||||
|
self.flags.draw_mode = false;
|
||||||
|
screen.with_size(256);
|
||||||
|
self.clear_screen(screen);
|
||||||
|
}
|
||||||
|
/// # |`00ff`|
|
||||||
|
/// Initialize hires mode
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::hires]
|
||||||
|
pub(super) fn init_hires(&mut self, screen: &mut Screen) {
|
||||||
|
self.flags.draw_mode = true;
|
||||||
|
screen.with_size(1024);
|
||||||
|
self.clear_screen(screen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`1aaa`| Sets pc to an absolute address
|
||||||
|
impl CPU {
|
||||||
|
/// |`1aaa`| Sets the program counter to an absolute address
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::jmp]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn jump(&mut self, a: Adr) {
|
||||||
|
// jump to self == halt
|
||||||
|
if a.wrapping_add(2) == self.pc {
|
||||||
|
self.flags.pause = true;
|
||||||
|
}
|
||||||
|
self.pc = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`2aaa`| Pushes pc onto the stack, then jumps to a
|
||||||
|
impl CPU {
|
||||||
|
/// |`2aaa`| Pushes pc onto the stack, then jumps to a
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::call]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn call(&mut self, a: Adr) {
|
||||||
|
self.stack.push(self.pc);
|
||||||
|
self.pc = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`3xbb`| Skips next instruction if register X == b
|
||||||
|
impl CPU {
|
||||||
|
/// |`3xbb`| Skips the next instruction if register X == b
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::seb]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn skip_equals_immediate(&mut self, x: Reg, b: u8) {
|
||||||
|
if self.v[x] == b {
|
||||||
|
self.pc = self.pc.wrapping_add(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`4xbb`| Skips next instruction if register X != b
|
||||||
|
impl CPU {
|
||||||
|
/// |`4xbb`| Skips the next instruction if register X != b
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::sneb]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn skip_not_equals_immediate(&mut self, x: Reg, b: u8) {
|
||||||
|
if self.v[x] != b {
|
||||||
|
self.pc = self.pc.wrapping_add(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`5xyn`| Performs a register-register comparison
|
||||||
|
///
|
||||||
|
/// |opcode| effect |
|
||||||
|
/// |------|------------------------------------|
|
||||||
|
/// |`5XY0`| Skip next instruction if vX == vY |
|
||||||
|
impl CPU {
|
||||||
|
/// |`5xy0`| Skips the next instruction if register X != register Y
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::sne]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn skip_equals(&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
|
||||||
|
impl CPU {
|
||||||
|
/// |`6xbb`| Loads immediate byte b into register vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::movb]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn load_immediate(&mut self, x: Reg, b: u8) {
|
||||||
|
self.v[x] = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`7xbb`| Adds immediate byte b to register vX
|
||||||
|
impl CPU {
|
||||||
|
/// |`7xbb`| Adds immediate byte b to register vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::addb]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn add_immediate(&mut self, x: Reg, b: u8) {
|
||||||
|
self.v[x] = self.v[x].wrapping_add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`8xyn`| 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 |
|
||||||
|
impl CPU {
|
||||||
|
/// |`8xy0`| Loads the value of y into x
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::mov]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn load(&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
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::or]
|
||||||
|
///
|
||||||
|
/// # Quirk
|
||||||
|
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn or(&mut self, x: Reg, y: Reg) {
|
||||||
|
self.v[x] |= self.v[y];
|
||||||
|
if !self.flags.quirks.bin_ops {
|
||||||
|
self.v[0xf] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// |`8xy2`| Performs bitwise and of vX and vY, and stores the result in vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::and]
|
||||||
|
///
|
||||||
|
/// # Quirk
|
||||||
|
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn and(&mut self, x: Reg, y: Reg) {
|
||||||
|
self.v[x] &= self.v[y];
|
||||||
|
if !self.flags.quirks.bin_ops {
|
||||||
|
self.v[0xf] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// |`8xy3`| Performs bitwise xor of vX and vY, and stores the result in vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::xor]
|
||||||
|
///
|
||||||
|
/// # Quirk
|
||||||
|
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn xor(&mut self, x: Reg, y: Reg) {
|
||||||
|
self.v[x] ^= self.v[y];
|
||||||
|
if !self.flags.quirks.bin_ops {
|
||||||
|
self.v[0xf] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// |`8xy4`| Performs addition of vX and vY, and stores the result in vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::add]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn add(&mut self, x: Reg, y: Reg) {
|
||||||
|
let carry;
|
||||||
|
(self.v[x], carry) = self.v[x].overflowing_add(self.v[y]);
|
||||||
|
self.v[0xf] = carry.into();
|
||||||
|
}
|
||||||
|
/// |`8xy5`| Performs subtraction of vX and vY, and stores the result in vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::sub]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn sub(&mut self, x: Reg, y: Reg) {
|
||||||
|
let carry;
|
||||||
|
(self.v[x], carry) = self.v[x].overflowing_sub(self.v[y]);
|
||||||
|
self.v[0xf] = (!carry).into();
|
||||||
|
}
|
||||||
|
/// |`8xy6`| Performs bitwise right shift of vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::shr]
|
||||||
|
///
|
||||||
|
/// # Quirk
|
||||||
|
/// On the original chip-8 interpreter, this shifts vY and stores the result in vX
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn shift_right(&mut self, x: Reg, y: Reg) {
|
||||||
|
let src: Reg = if self.flags.quirks.shift { x } else { y };
|
||||||
|
let shift_out = self.v[src] & 1;
|
||||||
|
self.v[x] = self.v[src] >> 1;
|
||||||
|
self.v[0xf] = shift_out;
|
||||||
|
}
|
||||||
|
/// |`8xy7`| Performs subtraction of vY and vX, and stores the result in vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::bsub]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn backwards_sub(&mut self, x: Reg, y: Reg) {
|
||||||
|
let carry;
|
||||||
|
(self.v[x], carry) = self.v[y].overflowing_sub(self.v[x]);
|
||||||
|
self.v[0xf] = (!carry).into();
|
||||||
|
}
|
||||||
|
/// 8X_E: Performs bitwise left shift of vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::shl]
|
||||||
|
///
|
||||||
|
/// # Quirk
|
||||||
|
/// On the original chip-8 interpreter, this would perform the operation on vY
|
||||||
|
/// and store the result in vX. This behavior was left out, for now.
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn shift_left(&mut self, x: Reg, y: Reg) {
|
||||||
|
let src: Reg = if self.flags.quirks.shift { x } else { y };
|
||||||
|
let shift_out: u8 = self.v[src] >> 7;
|
||||||
|
self.v[x] = self.v[src] << 1;
|
||||||
|
self.v[0xf] = shift_out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`9xyn`| Performs a register-register comparison
|
||||||
|
///
|
||||||
|
/// |opcode| effect |
|
||||||
|
/// |------|------------------------------------|
|
||||||
|
/// |`9XY0`| Skip next instruction if vX != vY |
|
||||||
|
impl CPU {
|
||||||
|
/// |`9xy0`| Skip next instruction if X != y
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::sne]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn skip_not_equals(&mut self, x: Reg, y: Reg) {
|
||||||
|
if self.v[x] != self.v[y] {
|
||||||
|
self.pc = self.pc.wrapping_add(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Aaaa`| Load address #a into register I
|
||||||
|
impl CPU {
|
||||||
|
/// |`Aadr`| Load address #adr into register I
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::movI]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn load_i_immediate(&mut self, a: Adr) {
|
||||||
|
self.i = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Baaa`| Jump to &adr + v0
|
||||||
|
impl CPU {
|
||||||
|
/// |`Badr`| Jump to &adr + v0
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::jmpr]
|
||||||
|
///
|
||||||
|
/// Quirk:
|
||||||
|
/// On the Super-Chip, this does stupid shit
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn jump_indexed(&mut self, a: Adr) {
|
||||||
|
let reg = if self.flags.quirks.stupid_jumps {
|
||||||
|
a as usize >> 8
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
self.pc = a.wrapping_add(self.v[reg] as Adr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Cxbb`| Stores a random number & the provided byte into vX
|
||||||
|
impl CPU {
|
||||||
|
/// |`Cxbb`| Stores a random number & the provided byte into vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::rand]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn rand(&mut self, x: Reg, b: u8) {
|
||||||
|
self.v[x] = random::<u8>() & b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
|
||||||
|
impl CPU {
|
||||||
|
/// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::draw]
|
||||||
|
///
|
||||||
|
/// # Quirk
|
||||||
|
/// On the original chip-8 interpreter, this will wait for a VBI
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn draw(&mut self, x: Reg, y: Reg, n: Nib, screen: &mut Screen) {
|
||||||
|
if !self.flags.quirks.draw_wait {
|
||||||
|
self.flags.draw_wait = true;
|
||||||
|
}
|
||||||
|
// self.draw_hires handles both hi-res mode and drawing 16x16 sprites
|
||||||
|
if self.flags.draw_mode || n == 0 {
|
||||||
|
self.draw_hires(x, y, n, screen);
|
||||||
|
} else {
|
||||||
|
self.draw_lores(x, y, n, screen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Dxyn`| Chip-8: Draws n-byte sprite to the screen at coordinates (vX, vY)
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, scr: &mut Screen) {
|
||||||
|
self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, scr);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn draw_sprite(
|
||||||
|
&mut self,
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
n: Nib,
|
||||||
|
w: u16,
|
||||||
|
h: u16,
|
||||||
|
screen: &mut Screen,
|
||||||
|
) {
|
||||||
|
let w_bytes = w / 8;
|
||||||
|
self.v[0xf] = 0;
|
||||||
|
if let Some(sprite) = self.mem.grab(self.i as usize..(self.i + n as u16) as usize) {
|
||||||
|
for (line, &sprite) in sprite.iter().enumerate() {
|
||||||
|
let line = line as u16;
|
||||||
|
let sprite = ((sprite as u16) << (8 - (x % 8))).to_be_bytes();
|
||||||
|
for (addr, &byte) in sprite.iter().enumerate().filter_map(|(idx, byte)| {
|
||||||
|
let x = (x / 8) + idx as u16;
|
||||||
|
Some((
|
||||||
|
if self.flags.quirks.screen_wrap {
|
||||||
|
((y + line) % h * w_bytes + (x % w_bytes)) % (w_bytes * h)
|
||||||
|
} else if x < w_bytes {
|
||||||
|
(y + line) * w_bytes + x
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
},
|
||||||
|
byte,
|
||||||
|
))
|
||||||
|
}) {
|
||||||
|
let display: u8 = screen.read(addr);
|
||||||
|
screen.write(addr, byte ^ display);
|
||||||
|
if byte & display != 0 {
|
||||||
|
self.v[0xf] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// |`Dxyn`| Super-Chip extension high-resolution graphics mode
|
||||||
|
impl CPU {
|
||||||
|
/// |`Dxyn`| Super-Chip extension high-resolution graphics mode
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, screen: &mut Screen) {
|
||||||
|
if !self.flags.quirks.draw_wait {
|
||||||
|
self.flags.draw_wait = true;
|
||||||
|
}
|
||||||
|
let (w, h) = match self.flags.draw_mode {
|
||||||
|
true => (128, 64),
|
||||||
|
false => (64, 32),
|
||||||
|
};
|
||||||
|
let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h);
|
||||||
|
match n {
|
||||||
|
0 => self.draw_schip_sprite(x, y, w, screen),
|
||||||
|
_ => self.draw_sprite(x, y, n, w, h, screen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Draws a 16x16 Super Chip sprite
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, screen: &mut Screen) {
|
||||||
|
self.v[0xf] = 0;
|
||||||
|
let w_bytes = w / 8;
|
||||||
|
if let Some(sprite) = self.mem.grab(self.i as usize..(self.i + 32) as usize) {
|
||||||
|
let sprite = sprite.to_owned();
|
||||||
|
for (line, sprite) in sprite.chunks_exact(2).enumerate() {
|
||||||
|
let sprite = u16::from_be_bytes(
|
||||||
|
sprite
|
||||||
|
.try_into()
|
||||||
|
.expect("Chunks should only return 2 bytes"),
|
||||||
|
);
|
||||||
|
let addr = (y + line as u16) * w_bytes + x / 8;
|
||||||
|
let sprite = (sprite as u32) << (16 - (x % 8));
|
||||||
|
let display: u32 = screen.read(addr);
|
||||||
|
screen.write(addr, display ^ sprite);
|
||||||
|
if display & sprite != 0 {
|
||||||
|
self.v[0xf] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Exbb`| Skips instruction on value of keypress
|
||||||
|
///
|
||||||
|
/// |opcode| effect |
|
||||||
|
/// |------|------------------------------------|
|
||||||
|
/// |`eX9e`| Skip next instruction if key == vX |
|
||||||
|
/// |`eXa1`| Skip next instruction if key != vX |
|
||||||
|
impl CPU {
|
||||||
|
/// |`Ex9E`| Skip next instruction if key == vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::sek]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn skip_key_equals(&mut self, x: Reg) {
|
||||||
|
if self.keys[self.v[x] as usize & 0xf] {
|
||||||
|
self.pc += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// |`ExaE`| Skip next instruction if key != vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::snek]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn skip_key_not_equals(&mut self, x: Reg) {
|
||||||
|
if !self.keys[self.v[x] as usize & 0xf] {
|
||||||
|
self.pc += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Fxbb`| Performs IO
|
||||||
|
///
|
||||||
|
/// |opcode| effect |
|
||||||
|
/// |------|------------------------------------|
|
||||||
|
/// |`fX07`| Set vX to value in delay timer |
|
||||||
|
/// |`fX0a`| Wait for input, store key in vX |
|
||||||
|
/// |`fX15`| Set sound timer to the value in vX |
|
||||||
|
/// |`fX18`| set delay timer to the value in vX |
|
||||||
|
/// |`fX1e`| Add vX 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 |
|
||||||
|
impl CPU {
|
||||||
|
/// |`Fx07`| Get the current DT, and put it in vX
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::getdt]
|
||||||
|
///
|
||||||
|
/// ```py
|
||||||
|
/// vX = DT
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn load_delay_timer(&mut self, x: Reg) {
|
||||||
|
self.v[x] = self.delay;
|
||||||
|
}
|
||||||
|
/// |`Fx0A`| Wait for key, then vX = K
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn wait_for_key(&mut self, x: Reg) {
|
||||||
|
if let Some(key) = self.lastkey {
|
||||||
|
self.v[x] = key as u8;
|
||||||
|
self.lastkey = None;
|
||||||
|
} else {
|
||||||
|
self.pc = self.pc.wrapping_sub(2);
|
||||||
|
self.flags.keypause = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// |`Fx15`| Load vX into DT
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::setdt]
|
||||||
|
/// ```py
|
||||||
|
/// DT = vX
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn store_delay_timer(&mut self, x: Reg) {
|
||||||
|
self.delay = self.v[x];
|
||||||
|
}
|
||||||
|
/// |`Fx18`| Load vX into ST
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::movst]
|
||||||
|
/// ```py
|
||||||
|
/// ST = vX;
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn store_sound_timer(&mut self, x: Reg) {
|
||||||
|
self.sound = self.v[x];
|
||||||
|
}
|
||||||
|
/// |`Fx1e`| Add vX to I,
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::addI]
|
||||||
|
/// ```py
|
||||||
|
/// I += vX;
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn add_i(&mut self, x: Reg) {
|
||||||
|
self.i += self.v[x] as u16;
|
||||||
|
}
|
||||||
|
/// |`Fx29`| Load sprite for character x into I
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::font]
|
||||||
|
/// ```py
|
||||||
|
/// I = sprite(X);
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn load_sprite(&mut self, x: Reg) {
|
||||||
|
self.i = self.font + (5 * (self.v[x] as Adr % 0x10));
|
||||||
|
}
|
||||||
|
/// |`Fx33`| BCD convert X into I`[0..3]`
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::bcd]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn bcd_convert(&mut self, x: Reg) {
|
||||||
|
let x = self.v[x];
|
||||||
|
self.mem.write(self.i.wrapping_add(2), x % 10);
|
||||||
|
self.mem.write(self.i.wrapping_add(1), x / 10 % 10);
|
||||||
|
self.mem.write(self.i, x / 100 % 10);
|
||||||
|
}
|
||||||
|
/// |`Fx55`| DMA Stor from I to registers 0..=X
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::dmao]
|
||||||
|
///
|
||||||
|
/// # Quirk
|
||||||
|
/// The original chip-8 interpreter uses I to directly index memory,
|
||||||
|
/// with the side effect of leaving I as I+X+1 after the transfer is done.
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn store_dma(&mut self, x: Reg) {
|
||||||
|
let i = self.i as usize;
|
||||||
|
for (reg, value) in self
|
||||||
|
.mem
|
||||||
|
.grab_mut(i..=i + x)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
*value = self.v[reg]
|
||||||
|
}
|
||||||
|
if !self.flags.quirks.dma_inc {
|
||||||
|
self.i += x as Adr + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// |`Fx65`| DMA Load from I to registers 0..=X
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::dmai]
|
||||||
|
///
|
||||||
|
/// # Quirk
|
||||||
|
/// The original chip-8 interpreter uses I to directly index memory,
|
||||||
|
/// with the side effect of leaving I as I+X+1 after the transfer is done.
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn load_dma(&mut self, x: Reg) {
|
||||||
|
let i = self.i as usize;
|
||||||
|
for (reg, value) in self
|
||||||
|
.mem
|
||||||
|
.grab(i..=i + x)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
self.v[reg] = *value;
|
||||||
|
}
|
||||||
|
if !self.flags.quirks.dma_inc {
|
||||||
|
self.i += x as Adr + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Fxbb`| Super Chip: Performs IO
|
||||||
|
///
|
||||||
|
/// |opcode| effect |
|
||||||
|
/// |------|------------------------------------|
|
||||||
|
/// |`Fx30`| 16x16 equivalent of load_sprite |
|
||||||
|
/// |`Fx75`| Save to "flag registers" |
|
||||||
|
/// |`Fx85`| Load from "flag registers" |
|
||||||
|
impl CPU {
|
||||||
|
/// |`Fx30`| (Super-Chip) 16x16 equivalent of [CPU::load_sprite]
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::hfont]
|
||||||
|
///
|
||||||
|
/// TODO: Actually make and import the 16x font
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn load_big_sprite(&mut self, x: Reg) {
|
||||||
|
self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Fx75`| (Super-Chip) Save to "flag registers"
|
||||||
|
/// I just chuck it in 0x0..0xf. Screw it.
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::flgo]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn store_flags(&mut self, x: Reg) {
|
||||||
|
// TODO: Save these, maybe
|
||||||
|
for (reg, value) in self
|
||||||
|
.mem
|
||||||
|
.grab_mut(0..=x)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
*value = self.v[reg]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// |`Fx85`| (Super-Chip) Load from "flag registers"
|
||||||
|
/// I just chuck it in 0x0..0xf. Screw it.
|
||||||
|
///
|
||||||
|
/// Corresponds to [Insn::flgi]
|
||||||
|
#[inline(always)]
|
||||||
|
pub(super) fn load_flags(&mut self, x: Reg) {
|
||||||
|
for (reg, value) in self.mem.grab(0..=x).unwrap_or_default().iter().enumerate() {
|
||||||
|
self.v[reg] = *value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,230 +0,0 @@
|
|||||||
//! A disassembler for Chip-8 opcodes
|
|
||||||
#![allow(clippy::bad_bit_mask)]
|
|
||||||
use imperative_rs::InstructionSet;
|
|
||||||
use owo_colors::{OwoColorize, Style};
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
/// Disassembles Chip-8 instructions
|
|
||||||
pub trait Disassembler {
|
|
||||||
/// Disassemble a single instruction
|
|
||||||
fn once(&self, insn: u16) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_camel_case_types, non_snake_case, missing_docs)]
|
|
||||||
#[derive(Clone, Copy, Debug, InstructionSet, PartialEq, Eq)]
|
|
||||||
/// Implements a Disassembler using imperative_rs
|
|
||||||
pub enum Insn {
|
|
||||||
// Base instruction set
|
|
||||||
/// | 00e0 | Clear screen memory to 0s
|
|
||||||
#[opcode = "0x00e0"]
|
|
||||||
cls,
|
|
||||||
/// | 00ee | Return from subroutine
|
|
||||||
#[opcode = "0x00ee"]
|
|
||||||
ret,
|
|
||||||
/// | 1aaa | Jumps to an absolute address
|
|
||||||
#[opcode = "0x1AAA"]
|
|
||||||
jmp { A: u16 },
|
|
||||||
/// | 2aaa | Pushes pc onto the stack, then jumps to a
|
|
||||||
#[opcode = "0x2AAA"]
|
|
||||||
call { A: u16 },
|
|
||||||
/// | 3xbb | Skips next instruction if register X == b
|
|
||||||
#[opcode = "0x3xBB"]
|
|
||||||
seb { B: u8, x: usize },
|
|
||||||
/// | 4xbb | Skips next instruction if register X != b
|
|
||||||
#[opcode = "0x4xBB"]
|
|
||||||
sneb { B: u8, x: usize },
|
|
||||||
/// | 9XY0 | Skip next instruction if vX == vY |
|
|
||||||
#[opcode = "0x5xy0"]
|
|
||||||
se { y: usize, x: usize },
|
|
||||||
/// | 6xbb | Loads immediate byte b into register vX
|
|
||||||
#[opcode = "0x6xBB"]
|
|
||||||
movb { B: u8, x: usize },
|
|
||||||
/// | 7xbb | Adds immediate byte b to register vX
|
|
||||||
#[opcode = "0x7xBB"]
|
|
||||||
addb { B: u8, x: usize },
|
|
||||||
/// | 8xy0 | Loads the value of y into x
|
|
||||||
#[opcode = "0x8xy0"]
|
|
||||||
mov { x: usize, y: usize },
|
|
||||||
/// | 8xy1 | Performs bitwise or of vX and vY, and stores the result in vX
|
|
||||||
#[opcode = "0x8xy1"]
|
|
||||||
or { y: usize, x: usize },
|
|
||||||
/// | 8xy2 | Performs bitwise and of vX and vY, and stores the result in vX
|
|
||||||
#[opcode = "0x8xy2"]
|
|
||||||
and { y: usize, x: usize },
|
|
||||||
/// | 8xy3 | Performs bitwise xor of vX and vY, and stores the result in vX
|
|
||||||
#[opcode = "0x8xy3"]
|
|
||||||
xor { y: usize, x: usize },
|
|
||||||
/// | 8xy4 | Performs addition of vX and vY, and stores the result in vX
|
|
||||||
#[opcode = "0x8xy4"]
|
|
||||||
add { y: usize, x: usize },
|
|
||||||
/// | 8xy5 | Performs subtraction of vX and vY, and stores the result in vX
|
|
||||||
#[opcode = "0x8xy5"]
|
|
||||||
sub { y: usize, x: usize },
|
|
||||||
/// | 8xy6 | Performs bitwise right shift of vX (or vY)
|
|
||||||
#[opcode = "0x8xy6"]
|
|
||||||
shr { y: usize, x: usize },
|
|
||||||
/// | 8xy7 | Performs subtraction of vY and vX, and stores the result in vX
|
|
||||||
#[opcode = "0x8xy7"]
|
|
||||||
bsub { y: usize, x: usize },
|
|
||||||
/// | 8xyE | Performs bitwise left shift of vX
|
|
||||||
#[opcode = "0x8xye"]
|
|
||||||
shl { y: usize, x: usize },
|
|
||||||
/// | 9XY0 | Skip next instruction if vX != vY
|
|
||||||
#[opcode = "0x9xy0"]
|
|
||||||
sne { y: usize, x: usize },
|
|
||||||
/// | Aaaa | Load address #a into register I
|
|
||||||
#[opcode = "0xaAAA"]
|
|
||||||
movI { A: u16 },
|
|
||||||
/// | Baaa | Jump to &adr + v0
|
|
||||||
#[opcode = "0xbAAA"]
|
|
||||||
jmpr { A: u16 },
|
|
||||||
/// | Cxbb | Stores a random number & the provided byte into vX
|
|
||||||
#[opcode = "0xcxBB"]
|
|
||||||
rand { B: u8, x: usize },
|
|
||||||
/// | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY)
|
|
||||||
#[opcode = "0xdxyn"]
|
|
||||||
draw { y: usize, x: usize, n: u8 },
|
|
||||||
/// | eX9e | Skip next instruction if key == vX
|
|
||||||
#[opcode = "0xex9e"]
|
|
||||||
sek { x: usize },
|
|
||||||
/// | eXa1 | Skip next instruction if key != vX
|
|
||||||
#[opcode = "0xexa1"]
|
|
||||||
snek { x: usize },
|
|
||||||
// | fX07 | Set vX to value in delay timer
|
|
||||||
#[opcode = "0xfx07"]
|
|
||||||
getdt { x: usize },
|
|
||||||
// | fX0a | Wait for input, store key in vX
|
|
||||||
#[opcode = "0xfx0a"]
|
|
||||||
waitk { x: usize },
|
|
||||||
// | fX15 | Set sound timer to the value in vX
|
|
||||||
#[opcode = "0xfx15"]
|
|
||||||
setdt { x: usize },
|
|
||||||
// | fX18 | set delay timer to the value in vX
|
|
||||||
#[opcode = "0xfx18"]
|
|
||||||
movst { x: usize },
|
|
||||||
// | fX1e | Add vX to I
|
|
||||||
#[opcode = "0xfx1e"]
|
|
||||||
addI { x: usize },
|
|
||||||
// | fX29 | Load sprite for character x into I
|
|
||||||
#[opcode = "0xfx29"]
|
|
||||||
font { x: usize },
|
|
||||||
// | fX33 | BCD convert X into I[0..3]
|
|
||||||
#[opcode = "0xfx33"]
|
|
||||||
bcd { x: usize },
|
|
||||||
// | fX55 | DMA Stor from I to registers 0..X
|
|
||||||
#[opcode = "0xfx55"]
|
|
||||||
dmao { x: usize },
|
|
||||||
// | fX65 | DMA Load from I to registers 0..X
|
|
||||||
#[opcode = "0xfx65"]
|
|
||||||
dmai { x: usize },
|
|
||||||
|
|
||||||
// Super Chip extensions
|
|
||||||
/// | 00cN | Scroll the screen down
|
|
||||||
#[opcode = "0x00cn"]
|
|
||||||
scd { n: u8 },
|
|
||||||
/// | 00fb | Scroll the screen right
|
|
||||||
#[opcode = "0x00fb"]
|
|
||||||
scr,
|
|
||||||
/// | 00fc | Scroll the screen left
|
|
||||||
#[opcode = "0x00fc"]
|
|
||||||
scl,
|
|
||||||
/// | 00fd | Exit (halt and catch fire)
|
|
||||||
#[opcode = "0x00fd"]
|
|
||||||
halt,
|
|
||||||
/// | 00fe | Return to low-resolution mode
|
|
||||||
#[opcode = "0x00fe"]
|
|
||||||
lores,
|
|
||||||
/// | 00ff | Enter high-resolution mode
|
|
||||||
#[opcode = "0x00ff"]
|
|
||||||
hires,
|
|
||||||
/// | fx30 | Enter high-resolution mode
|
|
||||||
#[opcode = "0xfx30"]
|
|
||||||
hfont { x: usize },
|
|
||||||
/// | fx75 | Save to "flag registers"
|
|
||||||
#[opcode = "0xfx75"]
|
|
||||||
flgo { x: usize },
|
|
||||||
/// | fx85 | Load from "flag registers"
|
|
||||||
#[opcode = "0xfx85"]
|
|
||||||
flgi { x: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Insn {
|
|
||||||
#[rustfmt::skip]
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
// Base instruction set
|
|
||||||
Insn::cls => write!(f, "cls "),
|
|
||||||
Insn::ret => write!(f, "ret "),
|
|
||||||
Insn::jmp { A } => write!(f, "jmp {A:03x}"),
|
|
||||||
Insn::call { A } => write!(f, "call {A:03x}"),
|
|
||||||
Insn::seb { B, x } => write!(f, "se #{B:02x}, v{x:X}"),
|
|
||||||
Insn::sneb { B, x } => write!(f, "sne #{B:02x}, v{x:X}"),
|
|
||||||
Insn::se { y, x } => write!(f, "se v{y:X}, v{x:X}"),
|
|
||||||
Insn::movb { B, x } => write!(f, "mov #{B:02x}, v{x:X}"),
|
|
||||||
Insn::addb { B, x } => write!(f, "add #{B:02x}, v{x:X}"),
|
|
||||||
Insn::mov { x, y } => write!(f, "mov v{y:X}, v{x:X}"),
|
|
||||||
Insn::or { y, x } => write!(f, "or v{y:X}, v{x:X}"),
|
|
||||||
Insn::and { y, x } => write!(f, "and v{y:X}, v{x:X}"),
|
|
||||||
Insn::xor { y, x } => write!(f, "xor v{y:X}, v{x:X}"),
|
|
||||||
Insn::add { y, x } => write!(f, "add v{y:X}, v{x:X}"),
|
|
||||||
Insn::sub { y, x } => write!(f, "sub v{y:X}, v{x:X}"),
|
|
||||||
Insn::shr { y, x } => write!(f, "shr v{y:X}, v{x:X}"),
|
|
||||||
Insn::bsub { y, x } => write!(f, "bsub v{y:X}, v{x:X}"),
|
|
||||||
Insn::shl { y, x } => write!(f, "shl v{y:X}, v{x:X}"),
|
|
||||||
Insn::sne { y, x } => write!(f, "sne v{y:X}, v{x:X}"),
|
|
||||||
Insn::movI { A } => write!(f, "mov ${A:03x}, I"),
|
|
||||||
Insn::jmpr { A } => write!(f, "jmp ${A:03x}+v0"),
|
|
||||||
Insn::rand { B, x } => write!(f, "rand #{B:02x}, v{x:X}"),
|
|
||||||
Insn::draw { y, x, n } => write!(f, "draw #{n:x}, v{x:X}, v{y:X}"),
|
|
||||||
Insn::sek { x } => write!(f, "sek v{x:X}"),
|
|
||||||
Insn::snek { x } => write!(f, "snek v{x:X}"),
|
|
||||||
Insn::getdt { x } => write!(f, "mov DT, v{x:X}"),
|
|
||||||
Insn::waitk { x } => write!(f, "waitk v{x:X}"),
|
|
||||||
Insn::setdt { x } => write!(f, "mov v{x:X}, DT"),
|
|
||||||
Insn::movst { x } => write!(f, "mov v{x:X}, ST"),
|
|
||||||
Insn::addI { x } => write!(f, "add v{x:X}, I"),
|
|
||||||
Insn::font { x } => write!(f, "font v{x:X}, I"),
|
|
||||||
Insn::bcd { x } => write!(f, "bcd v{x:X}, &I"),
|
|
||||||
Insn::dmao { x } => write!(f, "dmao v{x:X}"),
|
|
||||||
Insn::dmai { x } => write!(f, "dmai v{x:X}"),
|
|
||||||
// Super Chip extensions
|
|
||||||
Insn::scd { n } => write!(f, "scd #{n:x}"),
|
|
||||||
Insn::scr => write!(f, "scr "),
|
|
||||||
Insn::scl => write!(f, "scl "),
|
|
||||||
Insn::halt => write!(f, "halt "),
|
|
||||||
Insn::lores => write!(f, "lores "),
|
|
||||||
Insn::hires => write!(f, "hires "),
|
|
||||||
Insn::hfont { x } => write!(f, "hfont v{x:X}"),
|
|
||||||
Insn::flgo { x } => write!(f, "flgo v{x:X}"),
|
|
||||||
Insn::flgi { x } => write!(f, "flgi v{x:X}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Disassembles Chip-8 instructions, printing them in the provided [owo_colors::Style]s
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Dis {
|
|
||||||
/// Styles invalid instructions
|
|
||||||
pub invalid: Style,
|
|
||||||
/// Styles valid instruction
|
|
||||||
pub normal: Style,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Dis {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
invalid: Style::new().bold().red(),
|
|
||||||
normal: Style::new().green(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Disassembler for Dis {
|
|
||||||
fn once(&self, insn: u16) -> String {
|
|
||||||
if let Ok((_, insn)) = Insn::decode(&insn.to_be_bytes()) {
|
|
||||||
format!("{}", insn.style(self.normal))
|
|
||||||
} else {
|
|
||||||
format!("{}", format_args!("inval {insn:04x}").style(self.invalid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,13 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Represents [Flags] that aid in implementation but aren't a part of the Chip-8 spec
|
//! Represents [Flags] that aid in implementation but aren't a part of the Chip-8 spec
|
||||||
|
|
||||||
use super::{Mode, Quirks};
|
use super::{Mode, Quirks};
|
||||||
|
|
||||||
/// Represents flags that aid in implementation but aren't a part of the Chip-8 spec
|
/// Represents flags that aid in implementation but aren't a part of the Chip-8 spec
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Flags {
|
pub struct Flags {
|
||||||
/// Set when debug (live disassembly) mode enabled
|
/// Set when debug (live disassembly) mode enabled
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
@ -15,14 +19,12 @@ pub struct Flags {
|
|||||||
pub draw_wait: bool,
|
pub draw_wait: bool,
|
||||||
/// Set when the emulator is in high-res mode
|
/// Set when the emulator is in high-res mode
|
||||||
pub draw_mode: bool,
|
pub draw_mode: bool,
|
||||||
/// Set to the last key that's been *released* after a keypause
|
|
||||||
pub lastkey: Option<usize>,
|
|
||||||
/// Represents the current emulator [Mode]
|
/// Represents the current emulator [Mode]
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
/// Represents the set of emulator [Quirks] to enable, independent of the [Mode]
|
/// Represents the set of emulator [Quirks] to enable, independent of the [Mode]
|
||||||
pub quirks: Quirks,
|
pub quirks: Quirks,
|
||||||
/// Represents the number of instructions to run per tick of the internal timer
|
/// Set when the interpreter should increase the cycle count during a pause
|
||||||
pub monotonic: Option<usize>,
|
pub monotonic: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Flags {
|
impl Flags {
|
||||||
|
@ -1,625 +1,231 @@
|
|||||||
// (c) 2023 John A. Breaux
|
// (c) 2023 John A. Breaux
|
||||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Contains implementations for each [Insn] as private member functions of [CPU]
|
//! Contains the definition of a Chip-8 [Insn]
|
||||||
|
#![allow(clippy::bad_bit_mask)]
|
||||||
|
|
||||||
use super::*;
|
pub mod disassembler;
|
||||||
|
|
||||||
impl CPU {
|
use imperative_rs::InstructionSet;
|
||||||
/// Executes a single [Insn]
|
use std::fmt::Display;
|
||||||
#[inline(always)]
|
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types, non_snake_case, missing_docs)]
|
||||||
|
#[derive(Clone, Copy, Debug, InstructionSet, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
/// Represents a Chip-8 Instruction
|
||||||
|
pub enum Insn {
|
||||||
|
// Base instruction set
|
||||||
|
/// | 00e0 | Clear screen memory to 0s
|
||||||
|
#[opcode = "0x00e0"]
|
||||||
|
cls,
|
||||||
|
/// | 00ee | Return from subroutine
|
||||||
|
#[opcode = "0x00ee"]
|
||||||
|
ret,
|
||||||
|
/// | 1aaa | Jumps to an absolute address
|
||||||
|
#[opcode = "0x1AAA"]
|
||||||
|
jmp { A: u16 },
|
||||||
|
/// | 2aaa | Pushes pc onto the stack, then jumps to a
|
||||||
|
#[opcode = "0x2AAA"]
|
||||||
|
call { A: u16 },
|
||||||
|
/// | 3xbb | Skips next instruction if register X == b
|
||||||
|
#[opcode = "0x3xBB"]
|
||||||
|
seb { B: u8, x: usize },
|
||||||
|
/// | 4xbb | Skips next instruction if register X != b
|
||||||
|
#[opcode = "0x4xBB"]
|
||||||
|
sneb { B: u8, x: usize },
|
||||||
|
/// | 9XY0 | Skip next instruction if vX == vY |
|
||||||
|
#[opcode = "0x5xy0"]
|
||||||
|
se { y: usize, x: usize },
|
||||||
|
/// | 6xbb | Loads immediate byte b into register vX
|
||||||
|
#[opcode = "0x6xBB"]
|
||||||
|
movb { B: u8, x: usize },
|
||||||
|
/// | 7xbb | Adds immediate byte b to register vX
|
||||||
|
#[opcode = "0x7xBB"]
|
||||||
|
addb { B: u8, x: usize },
|
||||||
|
/// | 8xy0 | Loads the value of y into x
|
||||||
|
#[opcode = "0x8xy0"]
|
||||||
|
mov { x: usize, y: usize },
|
||||||
|
/// | 8xy1 | Performs bitwise or of vX and vY, and stores the result in vX
|
||||||
|
#[opcode = "0x8xy1"]
|
||||||
|
or { y: usize, x: usize },
|
||||||
|
/// | 8xy2 | Performs bitwise and of vX and vY, and stores the result in vX
|
||||||
|
#[opcode = "0x8xy2"]
|
||||||
|
and { y: usize, x: usize },
|
||||||
|
/// | 8xy3 | Performs bitwise xor of vX and vY, and stores the result in vX
|
||||||
|
#[opcode = "0x8xy3"]
|
||||||
|
xor { y: usize, x: usize },
|
||||||
|
/// | 8xy4 | Performs addition of vX and vY, and stores the result in vX
|
||||||
|
#[opcode = "0x8xy4"]
|
||||||
|
add { y: usize, x: usize },
|
||||||
|
/// | 8xy5 | Performs subtraction of vX and vY, and stores the result in vX
|
||||||
|
#[opcode = "0x8xy5"]
|
||||||
|
sub { y: usize, x: usize },
|
||||||
|
/// | 8xy6 | Performs bitwise right shift of vX (or vY)
|
||||||
|
#[opcode = "0x8xy6"]
|
||||||
|
shr { y: usize, x: usize },
|
||||||
|
/// | 8xy7 | Performs subtraction of vY and vX, and stores the result in vX
|
||||||
|
#[opcode = "0x8xy7"]
|
||||||
|
bsub { y: usize, x: usize },
|
||||||
|
/// | 8xyE | Performs bitwise left shift of vX
|
||||||
|
#[opcode = "0x8xye"]
|
||||||
|
shl { y: usize, x: usize },
|
||||||
|
/// | 9XY0 | Skip next instruction if vX != vY
|
||||||
|
#[opcode = "0x9xy0"]
|
||||||
|
sne { y: usize, x: usize },
|
||||||
|
/// | Aaaa | Load address #a into register I
|
||||||
|
#[opcode = "0xaAAA"]
|
||||||
|
movI { A: u16 },
|
||||||
|
/// | Baaa | Jump to &adr + v0
|
||||||
|
#[opcode = "0xbAAA"]
|
||||||
|
jmpr { A: u16 },
|
||||||
|
/// | Cxbb | Stores a random number & the provided byte into vX
|
||||||
|
#[opcode = "0xcxBB"]
|
||||||
|
rand { B: u8, x: usize },
|
||||||
|
/// | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY)
|
||||||
|
#[opcode = "0xdxyn"]
|
||||||
|
draw { y: usize, x: usize, n: u8 },
|
||||||
|
/// | eX9e | Skip next instruction if key == vX
|
||||||
|
#[opcode = "0xex9e"]
|
||||||
|
sek { x: usize },
|
||||||
|
/// | eXa1 | Skip next instruction if key != vX
|
||||||
|
#[opcode = "0xexa1"]
|
||||||
|
snek { x: usize },
|
||||||
|
/// | fX07 | Set vX to value in delay timer
|
||||||
|
#[opcode = "0xfx07"]
|
||||||
|
getdt { x: usize },
|
||||||
|
/// | fX0a | Wait for input, store key in vX
|
||||||
|
#[opcode = "0xfx0a"]
|
||||||
|
waitk { x: usize },
|
||||||
|
/// | fX15 | Set sound timer to the value in vX
|
||||||
|
#[opcode = "0xfx15"]
|
||||||
|
setdt { x: usize },
|
||||||
|
/// | fX18 | set delay timer to the value in vX
|
||||||
|
#[opcode = "0xfx18"]
|
||||||
|
movst { x: usize },
|
||||||
|
/// | fX1e | Add vX to I
|
||||||
|
#[opcode = "0xfx1e"]
|
||||||
|
addI { x: usize },
|
||||||
|
/// | fX29 | Load sprite for character x into I
|
||||||
|
#[opcode = "0xfx29"]
|
||||||
|
font { x: usize },
|
||||||
|
/// | fX33 | BCD convert X into I[0..3]
|
||||||
|
#[opcode = "0xfx33"]
|
||||||
|
bcd { x: usize },
|
||||||
|
/// | fX55 | DMA Stor from I to registers 0..X
|
||||||
|
#[opcode = "0xfx55"]
|
||||||
|
dmao { x: usize },
|
||||||
|
/// | fX65 | DMA Load from I to registers 0..X
|
||||||
|
#[opcode = "0xfx65"]
|
||||||
|
dmai { x: usize },
|
||||||
|
|
||||||
|
// Super Chip extensions
|
||||||
|
/// | 00cN | Scroll the screen down
|
||||||
|
#[opcode = "0x00cn"]
|
||||||
|
scd { n: u8 },
|
||||||
|
/// | 00fb | Scroll the screen right
|
||||||
|
#[opcode = "0x00fb"]
|
||||||
|
scr,
|
||||||
|
/// | 00fc | Scroll the screen left
|
||||||
|
#[opcode = "0x00fc"]
|
||||||
|
scl,
|
||||||
|
/// | 00fd | Exit (halt and catch fire)
|
||||||
|
#[opcode = "0x0000"]
|
||||||
|
#[opcode = "0x00fd"]
|
||||||
|
halt,
|
||||||
|
/// | 00fe | Return to low-resolution mode
|
||||||
|
#[opcode = "0x00fe"]
|
||||||
|
lores,
|
||||||
|
/// | 00ff | Enter high-resolution mode
|
||||||
|
#[opcode = "0x00ff"]
|
||||||
|
hires,
|
||||||
|
/// | fx30 | Load hires font address into vX
|
||||||
|
#[opcode = "0xfx30"]
|
||||||
|
hfont { x: usize },
|
||||||
|
/// | fx75 | Save to "flag registers"
|
||||||
|
#[opcode = "0xfx75"]
|
||||||
|
flgo { x: usize },
|
||||||
|
/// | fx85 | Load from "flag registers"
|
||||||
|
#[opcode = "0xfx85"]
|
||||||
|
flgi { x: usize },
|
||||||
|
|
||||||
|
// XO-Chip instructions
|
||||||
|
/// | 00dN | Scroll the screen up
|
||||||
|
#[opcode = "0x00dn"]
|
||||||
|
scu { n: u8 },
|
||||||
|
/// | 5XY2 | DMA Load from I to vX..vY
|
||||||
|
#[opcode = "0x5xy2"]
|
||||||
|
dmaro { y: usize, x: usize },
|
||||||
|
/// | 5XY3 | DMA Load from I to vX..vY
|
||||||
|
#[opcode = "0x5xy3"]
|
||||||
|
dmari { y: usize, x: usize },
|
||||||
|
/// | F000 | Load long address into character I
|
||||||
|
#[opcode = "0xf000_iiii"]
|
||||||
|
long { i: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Insn> for u32 {
|
||||||
|
fn from(value: &Insn) -> Self {
|
||||||
|
let mut buf = [0u8;4];
|
||||||
|
let len = Insn::encode(value, &mut buf).unwrap_or_default();
|
||||||
|
u32::from_be_bytes(buf) >> ((4-len)*8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Insn {
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
pub(super) fn execute(&mut self, bus: &mut Bus, instruction: Insn) {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match instruction {
|
match self {
|
||||||
// Core Chip-8 instructions
|
// Base instruction set
|
||||||
Insn::cls => self.clear_screen(bus),
|
Insn::cls => write!(f, "cls "),
|
||||||
Insn::ret => self.ret(bus),
|
Insn::ret => write!(f, "ret "),
|
||||||
Insn::jmp { A } => self.jump(A),
|
Insn::jmp { A } => write!(f, "jmp {A:03x}"),
|
||||||
Insn::call { A } => self.call(A, bus),
|
Insn::call { A } => write!(f, "call {A:03x}"),
|
||||||
Insn::seb { x, B } => self.skip_equals_immediate(x, B),
|
Insn::seb { B, x } => write!(f, "se #{B:02x}, v{x:X}"),
|
||||||
Insn::sneb { x, B } => self.skip_not_equals_immediate(x, B),
|
Insn::sneb { B, x } => write!(f, "sne #{B:02x}, v{x:X}"),
|
||||||
Insn::se { y, x } => self.skip_equals(x, y),
|
Insn::se { y, x } => write!(f, "se v{y:X}, v{x:X}"),
|
||||||
Insn::movb { x, B } => self.load_immediate(x, B),
|
Insn::movb { B, x } => write!(f, "mov #{B:02x}, v{x:X}"),
|
||||||
Insn::addb { x, B } => self.add_immediate(x, B),
|
Insn::addb { B, x } => write!(f, "add #{B:02x}, v{x:X}"),
|
||||||
Insn::mov { y, x } => self.load(x, y),
|
Insn::mov { x, y } => write!(f, "mov v{y:X}, v{x:X}"),
|
||||||
Insn::or { y, x } => self.or(x, y),
|
Insn::or { y, x } => write!(f, "or v{y:X}, v{x:X}"),
|
||||||
Insn::and { y, x } => self.and(x, y),
|
Insn::and { y, x } => write!(f, "and v{y:X}, v{x:X}"),
|
||||||
Insn::xor { y, x } => self.xor(x, y),
|
Insn::xor { y, x } => write!(f, "xor v{y:X}, v{x:X}"),
|
||||||
Insn::add { y, x } => self.add(x, y),
|
Insn::add { y, x } => write!(f, "add v{y:X}, v{x:X}"),
|
||||||
Insn::sub { y, x } => self.sub(x, y),
|
Insn::sub { y, x } => write!(f, "sub v{y:X}, v{x:X}"),
|
||||||
Insn::shr { y, x } => self.shift_right(x, y),
|
Insn::shr { y, x } => write!(f, "shr v{y:X}, v{x:X}"),
|
||||||
Insn::bsub { y, x } => self.backwards_sub(x, y),
|
Insn::bsub { y, x } => write!(f, "bsub v{y:X}, v{x:X}"),
|
||||||
Insn::shl { y, x } => self.shift_left(x, y),
|
Insn::shl { y, x } => write!(f, "shl v{y:X}, v{x:X}"),
|
||||||
Insn::sne { y, x } => self.skip_not_equals(x, y),
|
Insn::sne { y, x } => write!(f, "sne v{y:X}, v{x:X}"),
|
||||||
Insn::movI { A } => self.load_i_immediate(A),
|
Insn::movI { A } => write!(f, "mov ${A:03x}, I"),
|
||||||
Insn::jmpr { A } => self.jump_indexed(A),
|
Insn::jmpr { A } => write!(f, "jmp ${A:03x}+v0"),
|
||||||
Insn::rand { x, B } => self.rand(x, B),
|
Insn::rand { B, x } => write!(f, "rand #{B:02x}, v{x:X}"),
|
||||||
Insn::draw { y, x, n } => self.draw(x, y, n, bus),
|
Insn::draw { y, x, n } => write!(f, "draw #{n:x}, v{x:X}, v{y:X}"),
|
||||||
Insn::sek { x } => self.skip_key_equals(x),
|
Insn::sek { x } => write!(f, "sek v{x:X}"),
|
||||||
Insn::snek { x } => self.skip_key_not_equals(x),
|
Insn::snek { x } => write!(f, "snek v{x:X}"),
|
||||||
Insn::getdt { x } => self.load_delay_timer(x),
|
Insn::getdt { x } => write!(f, "mov DT, v{x:X}"),
|
||||||
Insn::waitk { x } => self.wait_for_key(x),
|
Insn::waitk { x } => write!(f, "waitk v{x:X}"),
|
||||||
Insn::setdt { x } => self.store_delay_timer(x),
|
Insn::setdt { x } => write!(f, "mov v{x:X}, DT"),
|
||||||
Insn::movst { x } => self.store_sound_timer(x),
|
Insn::movst { x } => write!(f, "mov v{x:X}, ST"),
|
||||||
Insn::addI { x } => self.add_i(x),
|
Insn::addI { x } => write!(f, "add v{x:X}, I"),
|
||||||
Insn::font { x } => self.load_sprite(x),
|
Insn::font { x } => write!(f, "font v{x:X}, I"),
|
||||||
Insn::bcd { x } => self.bcd_convert(x, bus),
|
Insn::bcd { x } => write!(f, "bcd v{x:X}, &I"),
|
||||||
Insn::dmao { x } => self.store_dma(x, bus),
|
Insn::dmao { x } => write!(f, "dmao v{x:X}"),
|
||||||
Insn::dmai { x } => self.load_dma(x, bus),
|
Insn::dmai { x } => write!(f, "dmai v{x:X}"),
|
||||||
// Super-Chip extensions
|
// Super Chip extensions
|
||||||
Insn::scd { n } => self.scroll_down(n, bus),
|
Insn::scd { n } => write!(f, "scd #{n:x}"),
|
||||||
Insn::scr => self.scroll_right(bus),
|
Insn::scr => write!(f, "scr "),
|
||||||
Insn::scl => self.scroll_left(bus),
|
Insn::scl => write!(f, "scl "),
|
||||||
Insn::halt => self.flags.pause(),
|
Insn::halt => write!(f, "halt "),
|
||||||
Insn::lores => self.init_lores(bus),
|
Insn::lores => write!(f, "lores "),
|
||||||
Insn::hires => self.init_hires(bus),
|
Insn::hires => write!(f, "hires "),
|
||||||
Insn::hfont { x } => self.load_big_sprite(x),
|
Insn::hfont { x } => write!(f, "hfont v{x:X}"),
|
||||||
Insn::flgo { x } => self.store_flags(x, bus),
|
Insn::flgo { x } => write!(f, "flgo v{x:X}"),
|
||||||
Insn::flgi { x } => self.load_flags(x, bus),
|
Insn::flgi { x } => write!(f, "flgi v{x:X}"),
|
||||||
}
|
// XO-Chip extensions
|
||||||
}
|
Insn::scu { n } => write!(f, "scu #{n:x}"),
|
||||||
}
|
Insn::dmaro { y, x } => write!(f, "dmaro v{x:X}..v{y:X}"),
|
||||||
|
Insn::dmari { y, x } => write!(f, "dmari v{x:X}..v{y:X}"),
|
||||||
// |`0aaa`| Issues a "System call" (ML routine)
|
Insn::long { i } => write!(f, "long ${i:04x}"),
|
||||||
//
|
}
|
||||||
// |opcode| effect |
|
|
||||||
// |------|------------------------------------|
|
|
||||||
// |`00e0`| Clear screen memory to all 0 |
|
|
||||||
// |`00ee`| Return from subroutine |
|
|
||||||
impl CPU {
|
|
||||||
/// |`00e0`| Clears the screen memory to 0
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn clear_screen(&mut self, bus: &mut Bus) {
|
|
||||||
bus.clear_region(Region::Screen);
|
|
||||||
}
|
|
||||||
/// |`00ee`| Returns from subroutine
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn ret(&mut self, bus: &impl Read<u16>) {
|
|
||||||
self.sp = self.sp.wrapping_add(2);
|
|
||||||
self.pc = bus.read(self.sp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`1aaa`| Sets pc to an absolute address
|
|
||||||
impl CPU {
|
|
||||||
/// |`1aaa`| Sets the program counter to an absolute address
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn jump(&mut self, a: Adr) {
|
|
||||||
// jump to self == halt
|
|
||||||
if a.wrapping_add(2) == self.pc {
|
|
||||||
self.flags.pause = true;
|
|
||||||
}
|
|
||||||
self.pc = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`2aaa`| Pushes pc onto the stack, then jumps to a
|
|
||||||
impl CPU {
|
|
||||||
/// |`2aaa`| Pushes pc onto the stack, then jumps to a
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn call(&mut self, a: Adr, bus: &mut impl Write<u16>) {
|
|
||||||
bus.write(self.sp, self.pc);
|
|
||||||
self.sp = self.sp.wrapping_sub(2);
|
|
||||||
self.pc = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`3xbb`| Skips next instruction if register X == b
|
|
||||||
impl CPU {
|
|
||||||
/// |`3xbb`| Skips the next instruction if register X == b
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn skip_equals_immediate(&mut self, x: Reg, b: u8) {
|
|
||||||
if self.v[x] == b {
|
|
||||||
self.pc = self.pc.wrapping_add(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`4xbb`| Skips next instruction if register X != b
|
|
||||||
impl CPU {
|
|
||||||
/// |`4xbb`| Skips the next instruction if register X != b
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn skip_not_equals_immediate(&mut self, x: Reg, b: u8) {
|
|
||||||
if self.v[x] != b {
|
|
||||||
self.pc = self.pc.wrapping_add(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`5xyn`| Performs a register-register comparison
|
|
||||||
//
|
|
||||||
// |opcode| effect |
|
|
||||||
// |------|------------------------------------|
|
|
||||||
// |`5XY0`| Skip next instruction if vX == vY |
|
|
||||||
impl CPU {
|
|
||||||
/// |`5xy0`| Skips the next instruction if register X != register Y
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn skip_equals(&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
|
|
||||||
impl CPU {
|
|
||||||
/// |`6xbb`| Loads immediate byte b into register vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn load_immediate(&mut self, x: Reg, b: u8) {
|
|
||||||
self.v[x] = b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`7xbb`| Adds immediate byte b to register vX
|
|
||||||
impl CPU {
|
|
||||||
/// |`7xbb`| Adds immediate byte b to register vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn add_immediate(&mut self, x: Reg, b: u8) {
|
|
||||||
self.v[x] = self.v[x].wrapping_add(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`8xyn`| 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 |
|
|
||||||
impl CPU {
|
|
||||||
/// |`8xy0`| Loads the value of y into x
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn load(&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
|
|
||||||
///
|
|
||||||
/// # Quirk
|
|
||||||
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn or(&mut self, x: Reg, y: Reg) {
|
|
||||||
self.v[x] |= self.v[y];
|
|
||||||
if !self.flags.quirks.bin_ops {
|
|
||||||
self.v[0xf] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// |`8xy2`| Performs bitwise and of vX and vY, and stores the result in vX
|
|
||||||
///
|
|
||||||
/// # Quirk
|
|
||||||
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn and(&mut self, x: Reg, y: Reg) {
|
|
||||||
self.v[x] &= self.v[y];
|
|
||||||
if !self.flags.quirks.bin_ops {
|
|
||||||
self.v[0xf] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// |`8xy3`| Performs bitwise xor of vX and vY, and stores the result in vX
|
|
||||||
///
|
|
||||||
/// # Quirk
|
|
||||||
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn xor(&mut self, x: Reg, y: Reg) {
|
|
||||||
self.v[x] ^= self.v[y];
|
|
||||||
if !self.flags.quirks.bin_ops {
|
|
||||||
self.v[0xf] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// |`8xy4`| Performs addition of vX and vY, and stores the result in vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn add(&mut self, x: Reg, y: Reg) {
|
|
||||||
let carry;
|
|
||||||
(self.v[x], carry) = self.v[x].overflowing_add(self.v[y]);
|
|
||||||
self.v[0xf] = carry.into();
|
|
||||||
}
|
|
||||||
/// |`8xy5`| Performs subtraction of vX and vY, and stores the result in vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn sub(&mut self, x: Reg, y: Reg) {
|
|
||||||
let carry;
|
|
||||||
(self.v[x], carry) = self.v[x].overflowing_sub(self.v[y]);
|
|
||||||
self.v[0xf] = (!carry).into();
|
|
||||||
}
|
|
||||||
/// |`8xy6`| Performs bitwise right shift of vX
|
|
||||||
///
|
|
||||||
/// # Quirk
|
|
||||||
/// On the original chip-8 interpreter, this shifts vY and stores the result in vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn shift_right(&mut self, x: Reg, y: Reg) {
|
|
||||||
let src: Reg = if self.flags.quirks.shift { x } else { y };
|
|
||||||
let shift_out = self.v[src] & 1;
|
|
||||||
self.v[x] = self.v[src] >> 1;
|
|
||||||
self.v[0xf] = shift_out;
|
|
||||||
}
|
|
||||||
/// |`8xy7`| Performs subtraction of vY and vX, and stores the result in vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn backwards_sub(&mut self, x: Reg, y: Reg) {
|
|
||||||
let carry;
|
|
||||||
(self.v[x], carry) = self.v[y].overflowing_sub(self.v[x]);
|
|
||||||
self.v[0xf] = (!carry).into();
|
|
||||||
}
|
|
||||||
/// 8X_E: Performs bitwise left shift of vX
|
|
||||||
///
|
|
||||||
/// # Quirk
|
|
||||||
/// On the original chip-8 interpreter, this would perform the operation on vY
|
|
||||||
/// and store the result in vX. This behavior was left out, for now.
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn shift_left(&mut self, x: Reg, y: Reg) {
|
|
||||||
let src: Reg = if self.flags.quirks.shift { x } else { y };
|
|
||||||
let shift_out: u8 = self.v[src] >> 7;
|
|
||||||
self.v[x] = self.v[src] << 1;
|
|
||||||
self.v[0xf] = shift_out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`9xyn`| Performs a register-register comparison
|
|
||||||
//
|
|
||||||
// |opcode| effect |
|
|
||||||
// |------|------------------------------------|
|
|
||||||
// |`9XY0`| Skip next instruction if vX != vY |
|
|
||||||
impl CPU {
|
|
||||||
/// |`9xy0`| Skip next instruction if X != y
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn skip_not_equals(&mut self, x: Reg, y: Reg) {
|
|
||||||
if self.v[x] != self.v[y] {
|
|
||||||
self.pc = self.pc.wrapping_add(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`Aaaa`| Load address #a into register I
|
|
||||||
impl CPU {
|
|
||||||
/// |`Aadr`| Load address #adr into register I
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn load_i_immediate(&mut self, a: Adr) {
|
|
||||||
self.i = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`Baaa`| Jump to &adr + v0
|
|
||||||
impl CPU {
|
|
||||||
/// |`Badr`| Jump to &adr + v0
|
|
||||||
///
|
|
||||||
/// Quirk:
|
|
||||||
/// On the Super-Chip, this does stupid shit
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn jump_indexed(&mut self, a: Adr) {
|
|
||||||
let reg = if self.flags.quirks.stupid_jumps {
|
|
||||||
a as usize >> 8
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
self.pc = a.wrapping_add(self.v[reg] as Adr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`Cxbb`| Stores a random number & the provided byte into vX
|
|
||||||
impl CPU {
|
|
||||||
/// |`Cxbb`| Stores a random number & the provided byte into vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn rand(&mut self, x: Reg, b: u8) {
|
|
||||||
self.v[x] = random::<u8>() & b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
|
|
||||||
impl CPU {
|
|
||||||
/// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
|
|
||||||
///
|
|
||||||
/// # Quirk
|
|
||||||
/// On the original chip-8 interpreter, this will wait for a VBI
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn draw(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
|
|
||||||
if !self.flags.quirks.draw_wait {
|
|
||||||
self.flags.draw_wait = true;
|
|
||||||
}
|
|
||||||
// self.draw_hires handles both hi-res mode and drawing 16x16 sprites
|
|
||||||
if self.flags.draw_mode || n == 0 {
|
|
||||||
self.draw_hires(x, y, n, bus);
|
|
||||||
} else {
|
|
||||||
self.draw_lores(x, y, n, bus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
|
|
||||||
self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, bus);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn draw_sprite(&mut self, x: u16, y: u16, n: Nib, w: u16, h: u16, bus: &mut Bus) {
|
|
||||||
let w_bytes = w / 8;
|
|
||||||
self.v[0xf] = 0;
|
|
||||||
if let Some(sprite) = bus.get(self.i as usize..(self.i + n as u16) as usize) {
|
|
||||||
let sprite = sprite.to_vec();
|
|
||||||
for (line, &sprite) in sprite.iter().enumerate() {
|
|
||||||
let line = line as u16;
|
|
||||||
if y + line >= h {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let sprite = (sprite as u16) << (8 - (x % 8))
|
|
||||||
& if (x % w) >= (w - 8) { 0xff00 } else { 0xffff };
|
|
||||||
let addr = |x, y| -> u16 { (y + line) * w_bytes + (x / 8) + self.screen };
|
|
||||||
let screen: u16 = bus.read(addr(x, y));
|
|
||||||
bus.write(addr(x, y), screen ^ sprite);
|
|
||||||
if screen & sprite != 0 {
|
|
||||||
self.v[0xf] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`Exbb`| Skips instruction on value of keypress
|
|
||||||
//
|
|
||||||
// |opcode| effect |
|
|
||||||
// |------|------------------------------------|
|
|
||||||
// |`eX9e`| Skip next instruction if key == vX |
|
|
||||||
// |`eXa1`| Skip next instruction if key != vX |
|
|
||||||
impl CPU {
|
|
||||||
/// |`Ex9E`| Skip next instruction if key == vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn skip_key_equals(&mut self, x: Reg) {
|
|
||||||
if self.keys[self.v[x] as usize & 0xf] {
|
|
||||||
self.pc += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// |`ExaE`| Skip next instruction if key != vX
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn skip_key_not_equals(&mut self, x: Reg) {
|
|
||||||
if !self.keys[self.v[x] as usize & 0xf] {
|
|
||||||
self.pc += 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// |`Fxbb`| Performs IO
|
|
||||||
//
|
|
||||||
// |opcode| effect |
|
|
||||||
// |------|------------------------------------|
|
|
||||||
// |`fX07`| Set vX to value in delay timer |
|
|
||||||
// |`fX0a`| Wait for input, store key in vX |
|
|
||||||
// |`fX15`| Set sound timer to the value in vX |
|
|
||||||
// |`fX18`| set delay timer to the value in vX |
|
|
||||||
// |`fX1e`| Add vX 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 |
|
|
||||||
impl CPU {
|
|
||||||
/// |`Fx07`| Get the current DT, and put it in vX
|
|
||||||
/// ```py
|
|
||||||
/// vX = DT
|
|
||||||
/// ```
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn load_delay_timer(&mut self, x: Reg) {
|
|
||||||
self.v[x] = self.delay as u8;
|
|
||||||
}
|
|
||||||
/// |`Fx0A`| Wait for key, then vX = K
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn wait_for_key(&mut self, x: Reg) {
|
|
||||||
if let Some(key) = self.flags.lastkey {
|
|
||||||
self.v[x] = key as u8;
|
|
||||||
self.flags.lastkey = None;
|
|
||||||
} else {
|
|
||||||
self.pc = self.pc.wrapping_sub(2);
|
|
||||||
self.flags.keypause = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// |`Fx15`| Load vX into DT
|
|
||||||
/// ```py
|
|
||||||
/// DT = vX
|
|
||||||
/// ```
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn store_delay_timer(&mut self, x: Reg) {
|
|
||||||
self.delay = self.v[x] as f64;
|
|
||||||
}
|
|
||||||
/// |`Fx18`| Load vX into ST
|
|
||||||
/// ```py
|
|
||||||
/// ST = vX;
|
|
||||||
/// ```
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn store_sound_timer(&mut self, x: Reg) {
|
|
||||||
self.sound = self.v[x] as f64;
|
|
||||||
}
|
|
||||||
/// |`Fx1e`| Add vX to I,
|
|
||||||
/// ```py
|
|
||||||
/// I += vX;
|
|
||||||
/// ```
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn add_i(&mut self, x: Reg) {
|
|
||||||
self.i += self.v[x] as u16;
|
|
||||||
}
|
|
||||||
/// |`Fx29`| Load sprite for character x into I
|
|
||||||
/// ```py
|
|
||||||
/// I = sprite(X);
|
|
||||||
/// ```
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn load_sprite(&mut self, x: Reg) {
|
|
||||||
self.i = self.font + (5 * (self.v[x] as Adr % 0x10));
|
|
||||||
}
|
|
||||||
/// |`Fx33`| BCD convert X into I`[0..3]`
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn bcd_convert(&mut self, x: Reg, bus: &mut Bus) {
|
|
||||||
let x = self.v[x];
|
|
||||||
bus.write(self.i.wrapping_add(2), x % 10);
|
|
||||||
bus.write(self.i.wrapping_add(1), x / 10 % 10);
|
|
||||||
bus.write(self.i, x / 100 % 10);
|
|
||||||
}
|
|
||||||
/// |`Fx55`| DMA Stor from I to registers 0..=X
|
|
||||||
///
|
|
||||||
/// # Quirk
|
|
||||||
/// The original chip-8 interpreter uses I to directly index memory,
|
|
||||||
/// with the side effect of leaving I as I+X+1 after the transfer is done.
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn store_dma(&mut self, x: Reg, bus: &mut Bus) {
|
|
||||||
let i = self.i as usize;
|
|
||||||
for (reg, value) in bus
|
|
||||||
.get_mut(i..=i + x)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.iter_mut()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
*value = self.v[reg]
|
|
||||||
}
|
|
||||||
if !self.flags.quirks.dma_inc {
|
|
||||||
self.i += x as Adr + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// |`Fx65`| DMA Load from I to registers 0..=X
|
|
||||||
///
|
|
||||||
/// # Quirk
|
|
||||||
/// The original chip-8 interpreter uses I to directly index memory,
|
|
||||||
/// with the side effect of leaving I as I+X+1 after the transfer is done.
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn load_dma(&mut self, x: Reg, bus: &mut Bus) {
|
|
||||||
let i = self.i as usize;
|
|
||||||
for (reg, value) in bus.get(i..=i + x).unwrap_or_default().iter().enumerate() {
|
|
||||||
self.v[reg] = *value;
|
|
||||||
}
|
|
||||||
if !self.flags.quirks.dma_inc {
|
|
||||||
self.i += x as Adr + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////////// SUPER CHIP ////////////////
|
|
||||||
|
|
||||||
impl CPU {
|
|
||||||
/// |`00cN`| Scroll the screen down N lines
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn scroll_down(&mut self, n: Nib, bus: &mut Bus) {
|
|
||||||
match self.flags.draw_mode {
|
|
||||||
true => {
|
|
||||||
// Get a line from the bus
|
|
||||||
for i in (0..16 * (64 - n as usize)).step_by(16).rev() {
|
|
||||||
let i = i + self.screen as usize;
|
|
||||||
let line: u128 = bus.read(i);
|
|
||||||
bus.write(i - (n as usize * 16), 0u128);
|
|
||||||
bus.write(i, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false => {
|
|
||||||
// Get a line from the bus
|
|
||||||
for i in (0..8 * (32 - n as usize)).step_by(8).rev() {
|
|
||||||
let i = i + self.screen as usize;
|
|
||||||
let line: u64 = bus.read(i);
|
|
||||||
bus.write(i, 0u64);
|
|
||||||
bus.write(i + (n as usize * 8), line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// |`00fb`| Scroll the screen right
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn scroll_right(&mut self, bus: &mut (impl Read<u128> + Write<u128>)) {
|
|
||||||
// Get a line from the bus
|
|
||||||
for i in (0..16 * 64).step_by(16) {
|
|
||||||
//let line: u128 = bus.read(self.screen + i) >> 4;
|
|
||||||
bus.write(self.screen + i, bus.read(self.screen + i) >> 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// |`00fc`| Scroll the screen right
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn scroll_left(&mut self, bus: &mut (impl Read<u128> + Write<u128>)) {
|
|
||||||
// Get a line from the bus
|
|
||||||
for i in (0..16 * 64).step_by(16) {
|
|
||||||
let line: u128 = (bus.read(self.screen + i) & !(0xf << 124)) << 4;
|
|
||||||
bus.write(self.screen + i, line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// |`Dxyn`|
|
|
||||||
/// Super-Chip extension high-resolution graphics mode
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, bus: &mut Bus) {
|
|
||||||
if !self.flags.quirks.draw_wait {
|
|
||||||
self.flags.draw_wait = true;
|
|
||||||
}
|
|
||||||
let (w, h) = match self.flags.draw_mode {
|
|
||||||
true => (128, 64),
|
|
||||||
false => (64, 32),
|
|
||||||
};
|
|
||||||
let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h);
|
|
||||||
match n {
|
|
||||||
0 => self.draw_schip_sprite(x, y, w, bus),
|
|
||||||
_ => self.draw_sprite(x, y, n, w, h, bus),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Draws a 16x16 Super Chip sprite
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, bus: &mut Bus) {
|
|
||||||
self.v[0xf] = 0;
|
|
||||||
let w_bytes = w / 8;
|
|
||||||
if let Some(sprite) = bus.get(self.i as usize..(self.i + 32) as usize) {
|
|
||||||
let sprite = sprite.to_owned();
|
|
||||||
for (line, sprite) in sprite.chunks(2).enumerate() {
|
|
||||||
let sprite = u16::from_be_bytes(
|
|
||||||
sprite
|
|
||||||
.try_into()
|
|
||||||
.expect("Chunks should only return 2 bytes"),
|
|
||||||
);
|
|
||||||
let addr = (y + line as u16) * w_bytes + x / 8 + self.screen;
|
|
||||||
let sprite = (sprite as u32) << (16 - (x % 8));
|
|
||||||
let screen: u32 = bus.read(addr);
|
|
||||||
bus.write(addr, screen ^ sprite);
|
|
||||||
if screen & sprite != 0 {
|
|
||||||
self.v[0xf] += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// |`Fx30`| (Super-Chip) 16x16 equivalent of Fx29
|
|
||||||
///
|
|
||||||
/// TODO: Actually make and import the 16x font
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn load_big_sprite(&mut self, x: Reg) {
|
|
||||||
self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// |`Fx75`| (Super-Chip) Save to "flag registers"
|
|
||||||
/// I just chuck it in 0x0..0xf. Screw it.
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn store_flags(&mut self, x: Reg, bus: &mut Bus) {
|
|
||||||
// TODO: Save these, maybe
|
|
||||||
for (reg, value) in bus
|
|
||||||
.get_mut(0..=x)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.iter_mut()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
*value = self.v[reg]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// |`Fx85`| (Super-Chip) Load from "flag registers"
|
|
||||||
/// I just chuck it in 0x0..0xf. Screw it.
|
|
||||||
#[inline(always)]
|
|
||||||
pub(super) fn load_flags(&mut self, x: Reg, bus: &mut Bus) {
|
|
||||||
for (reg, value) in bus.get(0..=x).unwrap_or_default().iter().enumerate() {
|
|
||||||
self.v[reg] = *value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize lores mode
|
|
||||||
pub(super) fn init_lores(&mut self, bus: &mut Bus) {
|
|
||||||
self.flags.draw_mode = false;
|
|
||||||
let scraddr = self.screen as usize;
|
|
||||||
bus.set_region(Region::Screen, scraddr..scraddr + 256);
|
|
||||||
self.clear_screen(bus);
|
|
||||||
}
|
|
||||||
/// Initialize hires mode
|
|
||||||
pub(super) fn init_hires(&mut self, bus: &mut Bus) {
|
|
||||||
self.flags.draw_mode = true;
|
|
||||||
let scraddr = self.screen as usize;
|
|
||||||
bus.set_region(Region::Screen, scraddr..scraddr + 1024);
|
|
||||||
self.clear_screen(bus);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
src/cpu/instruction/disassembler.rs
Normal file
41
src/cpu/instruction/disassembler.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
|
//! A disassembler for Chip-8 opcodes
|
||||||
|
use super::Insn;
|
||||||
|
use imperative_rs::InstructionSet;
|
||||||
|
use owo_colors::{OwoColorize, Style};
|
||||||
|
|
||||||
|
/// Disassembles Chip-8 instructions
|
||||||
|
pub trait Disassembler {
|
||||||
|
/// Disassemble a single instruction
|
||||||
|
fn once(&self, insn: u16) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disassembles Chip-8 instructions, printing them in the provided [owo_colors::Style]s
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Dis {
|
||||||
|
/// Styles invalid instructions
|
||||||
|
pub invalid: Style,
|
||||||
|
/// Styles valid instruction
|
||||||
|
pub normal: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Dis {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
invalid: Style::new().bold().red(),
|
||||||
|
normal: Style::new().green(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Disassembler for Dis {
|
||||||
|
fn once(&self, insn: u16) -> String {
|
||||||
|
if let Ok((_, insn)) = Insn::decode(&insn.to_be_bytes()) {
|
||||||
|
format!("{}", insn.style(self.normal))
|
||||||
|
} else {
|
||||||
|
format!("{}", format_args!("inval {insn:04x}").style(self.invalid))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
327
src/cpu/mem.rs
Normal file
327
src/cpu/mem.rs
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
|
//! The Mem represents the CPU's memory
|
||||||
|
//!
|
||||||
|
//! Contains some handy utils for reading and writing
|
||||||
|
|
||||||
|
use crate::{error::Result, traits::Grab};
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display, Formatter},
|
||||||
|
ops::Range,
|
||||||
|
slice::SliceIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a new [Mem], growing as needed
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::Region::*, *};
|
||||||
|
/// let mut mem = mem! {
|
||||||
|
/// Charset [0x0000..0x0800] = b"ABCDEF",
|
||||||
|
/// Program [0x0800..0xf000] = include_bytes!("mem.rs"),
|
||||||
|
/// };
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! mem {
|
||||||
|
($($name:path $(:)? [$range:expr] $(= $data:expr)?) ,* $(,)?) => {
|
||||||
|
$crate::cpu::mem::Mem::default()$(.add_region_owned($name, $range)$(.load_region_owned($name, $data))?)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traits Read and Write are here purely to make implementing other things more bearable
|
||||||
|
impl Grab for Mem {
|
||||||
|
/// Gets a slice of [Mem] memory
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mem = Mem::new()
|
||||||
|
/// .add_region_owned(Program, 0..10);
|
||||||
|
/// assert!([0;10].as_slice() == mem.grab(0..10).unwrap());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
fn grab<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
|
||||||
|
where
|
||||||
|
I: SliceIndex<[u8]>,
|
||||||
|
{
|
||||||
|
self.memory.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a mutable slice of [Mem] memory
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mut mem = Mem::new()
|
||||||
|
/// .add_region_owned(Program, 0..10);
|
||||||
|
/// assert!([0;10].as_slice() == mem.grab_mut(0..10).unwrap());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[u8]>>::Output>
|
||||||
|
where
|
||||||
|
I: SliceIndex<[u8]>,
|
||||||
|
{
|
||||||
|
self.memory.get_mut(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a named region in memory
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub enum Region {
|
||||||
|
/// Character ROM (but writable!)
|
||||||
|
Charset,
|
||||||
|
/// Hires character ROM
|
||||||
|
HiresCharset,
|
||||||
|
/// Program memory
|
||||||
|
Program,
|
||||||
|
#[doc(hidden)]
|
||||||
|
/// Total number of named regions
|
||||||
|
Count,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Region {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Region::HiresCharset => "HiresCharset",
|
||||||
|
Region::Charset => "Charset",
|
||||||
|
Region::Program => "Program",
|
||||||
|
_ => "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stores memory in a series of named regions with ranges
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
|
pub struct Mem {
|
||||||
|
memory: Vec<u8>,
|
||||||
|
region: [Option<Range<usize>>; Region::Count as usize],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mem {
|
||||||
|
// TODO: make mem::new() give a properly set up mem with a default memory map
|
||||||
|
/// Constructs a new mem
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mem = Mem::new();
|
||||||
|
/// assert!(mem.is_empty());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Mem::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the length of the mem' backing memory
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mem = Mem::new()
|
||||||
|
/// .add_region_owned(Program, 0..1234);
|
||||||
|
/// assert_eq!(1234, mem.len());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.memory.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the backing memory contains no elements
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mem = Mem::new();
|
||||||
|
/// assert!(mem.is_empty());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.memory.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grows the Mem backing memory to at least size bytes, but does not truncate
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mut mem = Mem::new();
|
||||||
|
/// mem.with_size(1234);
|
||||||
|
/// assert_eq!(1234, mem.len());
|
||||||
|
/// mem.with_size(0);
|
||||||
|
/// assert_eq!(1234, mem.len());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
pub fn with_size(&mut self, size: usize) {
|
||||||
|
if self.len() < size {
|
||||||
|
self.memory.resize(size, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new names range ([Region]) to an owned [Mem]
|
||||||
|
pub fn add_region_owned(mut self, name: Region, range: Range<usize>) -> Self {
|
||||||
|
self.add_region(name, range);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new named range ([Region]) to a [Mem]
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mut mem = Mem::new();
|
||||||
|
/// mem.add_region(Program, 0..1234);
|
||||||
|
/// assert_eq!(1234, mem.len());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
pub fn add_region(&mut self, name: Region, range: Range<usize>) -> &mut Self {
|
||||||
|
self.with_size(range.end);
|
||||||
|
if let Some(region) = self.region.get_mut(name as usize) {
|
||||||
|
*region = Some(range);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates an existing [Region]
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mut mem = Mem::new().add_region_owned(Program, 0..1234);
|
||||||
|
/// mem.set_region(Program, 1234..2345);
|
||||||
|
/// assert_eq!(2345, mem.len());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
pub fn set_region(&mut self, name: Region, range: Range<usize>) -> &mut Self {
|
||||||
|
self.with_size(range.end);
|
||||||
|
if let Some(region) = self.region.get_mut(name as usize) {
|
||||||
|
*region = Some(range);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads data into a [Region] on an *owned* [Mem], for use during initialization
|
||||||
|
pub fn load_region_owned(mut self, name: Region, data: &[u8]) -> Self {
|
||||||
|
self.load_region(name, data).ok();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads data into a named [Region]
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mem = Mem::new()
|
||||||
|
/// .add_region_owned(Program, 0..1234)
|
||||||
|
/// .load_region(Program, b"Hello, world!")?;
|
||||||
|
///# // TODO: Test if region actually contains "Hello, world!"
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
pub fn load_region(&mut self, name: Region, data: &[u8]) -> Result<&mut Self> {
|
||||||
|
use std::io::Write;
|
||||||
|
if let Some(mut region) = self.get_region_mut(name) {
|
||||||
|
assert_eq!(region.write(data)?, data.len());
|
||||||
|
}
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fills a [Region] with zeroes
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mem = Mem::new()
|
||||||
|
/// .add_region_owned(Program, 0..1234)
|
||||||
|
/// .clear_region(Program);
|
||||||
|
///# // TODO: test if region actually clear
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
/// If the region doesn't exist, that's okay.
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mem = Mem::new()
|
||||||
|
/// .add_region_owned(Program, 0..1234)
|
||||||
|
/// .clear_region(Program);
|
||||||
|
///# // TODO: test if region actually clear
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
pub fn clear_region(&mut self, name: Region) -> &mut Self {
|
||||||
|
if let Some(region) = self.get_region_mut(name) {
|
||||||
|
region.fill(0)
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a slice of a named [Region] of memory
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mem = Mem::new()
|
||||||
|
/// .add_region_owned(Program, 0..10);
|
||||||
|
/// assert!([0;10].as_slice() == mem.get_region(Program).unwrap());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_region(&self, name: Region) -> Option<&[u8]> {
|
||||||
|
debug_assert!(self.region.get(name as usize).is_some());
|
||||||
|
self.grab(self.region.get(name as usize)?.clone()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a mutable slice of a named region of memory
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
|
||||||
|
///# fn main() -> Result<()> {
|
||||||
|
/// let mut mem = Mem::new()
|
||||||
|
/// .add_region_owned(Program, 0..10);
|
||||||
|
/// assert!([0;10].as_slice() == mem.get_region_mut(Program).unwrap());
|
||||||
|
///# Ok(())
|
||||||
|
///# }
|
||||||
|
/// ```
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn get_region_mut(&mut self, name: Region) -> Option<&mut [u8]> {
|
||||||
|
debug_assert!(self.region.get(name as usize).is_some());
|
||||||
|
self.grab_mut(self.region.get(name as usize)?.clone()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_feature = "rhexdump")]
|
||||||
|
impl Display for Mem {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
use rhexdump::Rhexdump;
|
||||||
|
let mut rhx = Rhexdump::default();
|
||||||
|
rhx.set_bytes_per_group(2)
|
||||||
|
.expect("2 <= MAX_BYTES_PER_GROUP (8)");
|
||||||
|
rhx.display_duplicate_lines(false);
|
||||||
|
for (&name, range) in &self.region {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"[{name}]\n{}\n",
|
||||||
|
rhx.hexdump(&self.memory[range.clone()])
|
||||||
|
)?
|
||||||
|
}
|
||||||
|
write!(f, "")
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,23 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Selects the memory behavior of the [super::CPU]
|
//! Selects the memory behavior of the [super::CPU]
|
||||||
//!
|
//!
|
||||||
//! Since [super::Quirks] implements [From<Mode>],
|
//! Since [Quirks] implements [`From<Mode>`],
|
||||||
//! this can be used to select the appropriate quirk-set
|
//! this can be used to select the appropriate quirk-set
|
||||||
|
|
||||||
|
use super::Quirks;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// Selects the memory behavior of the interpreter
|
/// Selects the memory behavior of the interpreter
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub enum Mode {
|
pub enum Mode {
|
||||||
/// VIP emulation mode
|
/// VIP emulation mode
|
||||||
#[default]
|
#[default]
|
||||||
Chip8,
|
Chip8,
|
||||||
/// Chip-48 emulation mode
|
/// Super Chip emulation mode
|
||||||
SChip,
|
SChip,
|
||||||
/// XO-Chip emulation mode
|
/// XO-Chip emulation mode
|
||||||
XOChip,
|
XOChip,
|
||||||
@ -23,12 +28,54 @@ impl FromStr for Mode {
|
|||||||
|
|
||||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
match s.to_lowercase().as_str() {
|
match s.to_lowercase().as_str() {
|
||||||
"chip8" | "chip-8" => Ok(Mode::Chip8),
|
"chip" | "chip8" | "chip-8" => Ok(Mode::Chip8),
|
||||||
"schip" | "superchip" => Ok(Mode::SChip),
|
"s" | "schip" | "superchip" | "super chip" => Ok(Mode::SChip),
|
||||||
"xo-chip" | "xochip" => Ok(Mode::XOChip),
|
"xo" | "xochip" | "xo-chip" => Ok(Mode::XOChip),
|
||||||
_ => Err(Error::InvalidMode {
|
_ => Err(Error::InvalidMode { mode: s.into() }),
|
||||||
mode: s.to_string(),
|
}
|
||||||
}),
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for Mode {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Mode::Chip8 => "Chip-8",
|
||||||
|
Mode::SChip => "Super Chip",
|
||||||
|
Mode::XOChip => "XO-Chip",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for Mode {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
self.as_ref().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<usize> for Mode {
|
||||||
|
fn from(value: usize) -> Self {
|
||||||
|
match value {
|
||||||
|
0 => Self::Chip8,
|
||||||
|
1 => Self::SChip,
|
||||||
|
2 => Self::XOChip,
|
||||||
|
_ => Self::Chip8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Mode> for Quirks {
|
||||||
|
fn from(value: Mode) -> Self {
|
||||||
|
match value {
|
||||||
|
Mode::Chip8 => false.into(),
|
||||||
|
Mode::SChip => true.into(),
|
||||||
|
Mode::XOChip => Self {
|
||||||
|
bin_ops: true,
|
||||||
|
shift: false,
|
||||||
|
draw_wait: true,
|
||||||
|
screen_wrap: true,
|
||||||
|
dma_inc: false,
|
||||||
|
stupid_jumps: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Controls the [Quirks] behavior of the CPU on a granular level.
|
//! Controls the [Quirks] behavior of the CPU on a granular level.
|
||||||
use super::Mode;
|
|
||||||
/// Controls the authenticity behavior of the CPU on a granular level.
|
/// Controls the quirk behavior of the CPU on a granular level.
|
||||||
|
///
|
||||||
|
/// `false` is Cosmac-VIP-like behavior
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||||
pub struct Quirks {
|
pub struct Quirks {
|
||||||
/// Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0
|
/// Super Chip: Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0
|
||||||
pub bin_ops: bool,
|
pub bin_ops: bool,
|
||||||
/// Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX
|
/// Super Chip: Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX
|
||||||
pub shift: bool,
|
pub shift: bool,
|
||||||
/// Draw operations shouldn't pause execution until the next timer tick
|
/// Super Chip: Draw operations shouldn't pause execution until the next timer tick
|
||||||
pub draw_wait: bool,
|
pub draw_wait: bool,
|
||||||
/// DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1
|
/// XO-Chip: Draw operations should wrap from bottom to top and side to side
|
||||||
|
pub screen_wrap: bool,
|
||||||
|
/// Super Chip: DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1
|
||||||
pub dma_inc: bool,
|
pub dma_inc: bool,
|
||||||
/// Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`.
|
/// Super Chip: Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`.
|
||||||
pub stupid_jumps: bool,
|
pub stupid_jumps: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,6 +30,7 @@ impl From<bool> for Quirks {
|
|||||||
bin_ops: true,
|
bin_ops: true,
|
||||||
shift: true,
|
shift: true,
|
||||||
draw_wait: true,
|
draw_wait: true,
|
||||||
|
screen_wrap: false,
|
||||||
dma_inc: true,
|
dma_inc: true,
|
||||||
stupid_jumps: true,
|
stupid_jumps: true,
|
||||||
}
|
}
|
||||||
@ -30,6 +39,7 @@ impl From<bool> for Quirks {
|
|||||||
bin_ops: false,
|
bin_ops: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
draw_wait: false,
|
draw_wait: false,
|
||||||
|
screen_wrap: false,
|
||||||
dma_inc: false,
|
dma_inc: false,
|
||||||
stupid_jumps: false,
|
stupid_jumps: false,
|
||||||
}
|
}
|
||||||
@ -37,22 +47,6 @@ impl From<bool> for Quirks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Mode> for Quirks {
|
|
||||||
fn from(value: Mode) -> Self {
|
|
||||||
match value {
|
|
||||||
Mode::Chip8 => false.into(),
|
|
||||||
Mode::SChip => true.into(),
|
|
||||||
Mode::XOChip => Self {
|
|
||||||
bin_ops: true,
|
|
||||||
shift: false,
|
|
||||||
draw_wait: true,
|
|
||||||
dma_inc: false,
|
|
||||||
stupid_jumps: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Quirks {
|
impl Default for Quirks {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::from(false)
|
Self::from(false)
|
||||||
|
248
src/cpu/tests.rs
248
src/cpu/tests.rs
@ -1,5 +1,5 @@
|
|||||||
// (c) 2023 John A. Breaux
|
// (c) 2023 John A. Breaux
|
||||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Unit tests for [super::CPU]
|
//! Unit tests for [super::CPU]
|
||||||
//!
|
//!
|
||||||
@ -13,79 +13,72 @@
|
|||||||
//! Some of these tests run >16M times, which is very silly
|
//! Some of these tests run >16M times, which is very silly
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
pub(self) use crate::{
|
use crate::Screen;
|
||||||
bus,
|
use rand::random;
|
||||||
bus::{Bus, Region::*},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod decode;
|
mod decode;
|
||||||
|
|
||||||
fn setup_environment() -> (CPU, Bus) {
|
fn setup_environment() -> (CPU, Screen) {
|
||||||
(
|
let mut ch8 = (
|
||||||
CPU {
|
CPU {
|
||||||
flags: Flags {
|
flags: Flags {
|
||||||
debug: true,
|
debug: true,
|
||||||
pause: false,
|
pause: false,
|
||||||
monotonic: Some(8),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..CPU::default()
|
..CPU::default()
|
||||||
},
|
},
|
||||||
bus! {
|
Screen::default(),
|
||||||
// Load the charset into ROM
|
);
|
||||||
Charset [0x0050..0x00A0] = include_bytes!("../mem/charset.bin"),
|
ch8.0
|
||||||
// Load the ROM file into RAM (dummy binary which contains nothing but `jmp pc+2`)
|
.load_program_bytes(include_bytes!("tests/roms/jumptest.ch8"))
|
||||||
Program [0x0200..0x1000] = include_bytes!("tests/roms/jumptest.ch8"),
|
.unwrap();
|
||||||
// Create a screen
|
ch8
|
||||||
Screen [0x0F00..0x1000] = include_bytes!("../../chip8Archive/roms/1dcell.ch8"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_screen(bytes: &[u8]) {
|
fn print_screen(bytes: &[u8]) {
|
||||||
bus! {Screen [0..0x100] = bytes}
|
Screen::from(bytes).print_screen()
|
||||||
.print_screen()
|
|
||||||
.expect("Printing screen should not fail if Screen exists.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unused instructions
|
/// Unused instructions
|
||||||
mod unimplemented {
|
mod unimplemented {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
macro_rules! invalid {
|
||||||
fn ins_5xyn() {
|
($(
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
$(#[$attr:meta])* // Preserve doc comments, etc.
|
||||||
bus.write(0x200u16, 0x500fu16);
|
$pub:vis test $name:ident { $($insn:literal),+$(,)? }
|
||||||
cpu.tick(&mut bus)
|
);+ $(;)?) => {
|
||||||
.expect_err("0x500f is not an instruction");
|
$( $(#[$attr])* #[test] $pub fn $name () {$(
|
||||||
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
|
cpu.mem.write(0x200u16, $insn as u16);
|
||||||
|
cpu.tick(&mut screen)
|
||||||
|
.expect_err(stringify!($insn is not an instruction));
|
||||||
|
)*} )+
|
||||||
|
};
|
||||||
}
|
}
|
||||||
#[test]
|
|
||||||
fn ins_8xyn() {
|
invalid! {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
/// Invalid opcodes 5001, 0x5004-500f
|
||||||
bus.write(0x200u16, 0x800fu16);
|
test ins_5xyn {
|
||||||
cpu.tick(&mut bus)
|
0x5001,
|
||||||
.expect_err("0x800f is not an instruction");
|
0x5004, 0x5005, 0x5006, 0x5007, 0x5008, 0x5009,
|
||||||
}
|
0x500a, 0x500b, 0x500c, 0x500d, 0x500e, 0x500f,
|
||||||
#[test]
|
};
|
||||||
fn ins_9xyn() {
|
/// Invalid opcodes 8008-800d and 800f
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
test ins_8xyn {
|
||||||
bus.write(0x200u16, 0x900fu16);
|
0x8008, 0x8009, 0x800a, 0x800b, 0x800c, 0x800d,
|
||||||
cpu.tick(&mut bus)
|
0x800f,
|
||||||
.expect_err("0x900f is not an instruction");
|
};
|
||||||
}
|
/// Invalid opcodes 9001-900f
|
||||||
#[test]
|
test ins_9xyn {
|
||||||
fn ins_exbb() {
|
0x9001, 0x9002, 0x9003, 0x9004, 0x9005,
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
0x9006, 0x9007, 0x9008, 0x9009, 0x900a,
|
||||||
bus.write(0x200u16, 0xe00fu16);
|
0x900b, 0x900c, 0x900d, 0x900e, 0x900f,
|
||||||
cpu.tick(&mut bus)
|
};
|
||||||
.expect_err("0xe00f is not an instruction");
|
/// Invalid opcodes in e000 range
|
||||||
}
|
test ins_exbb { 0xe00f };
|
||||||
// Fxbb
|
/// Invalid opcodes in the 0xf000 range
|
||||||
#[test]
|
test ins_fxbb { 0xf001, 0xf00f };
|
||||||
fn ins_fxbb() {
|
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
|
||||||
bus.write(0x200u16, 0xf00fu16);
|
|
||||||
cpu.tick(&mut bus)
|
|
||||||
.expect_err("0xf00f is not an instruction");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,10 +87,11 @@ mod sys {
|
|||||||
/// 00e0: Clears the screen memory to 0
|
/// 00e0: Clears the screen memory to 0
|
||||||
#[test]
|
#[test]
|
||||||
fn clear_screen() {
|
fn clear_screen() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
cpu.clear_screen(&mut bus);
|
cpu.clear_screen(&mut screen);
|
||||||
bus.get_region(Screen)
|
screen
|
||||||
.expect("Expected screen, got None")
|
.grab(..)
|
||||||
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.for_each(|byte| assert_eq!(*byte, 0));
|
.for_each(|byte| assert_eq!(*byte, 0));
|
||||||
}
|
}
|
||||||
@ -106,17 +100,14 @@ mod sys {
|
|||||||
#[test]
|
#[test]
|
||||||
fn ret() {
|
fn ret() {
|
||||||
let test_addr = random::<u16>() & 0x7ff;
|
let test_addr = random::<u16>() & 0x7ff;
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, _) = setup_environment();
|
||||||
let sp_orig = cpu.sp;
|
|
||||||
// Place the address on the stack
|
// Place the address on the stack
|
||||||
bus.write(cpu.sp.wrapping_add(2), test_addr);
|
cpu.stack.push(test_addr);
|
||||||
|
|
||||||
cpu.ret(&bus);
|
cpu.ret();
|
||||||
|
|
||||||
// Verify the current address is the address from the stack
|
// Verify the current address is the address from the stack
|
||||||
assert_eq!(test_addr, cpu.pc);
|
assert_eq!(test_addr, cpu.pc);
|
||||||
// Verify the stack pointer has moved
|
|
||||||
assert!(dbg!(cpu.sp.wrapping_sub(sp_orig)) == 0x2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,9 +123,9 @@ mod cf {
|
|||||||
let (mut cpu, _) = setup_environment();
|
let (mut cpu, _) = setup_environment();
|
||||||
// Test all valid addresses
|
// Test all valid addresses
|
||||||
for addr in 0x000..0xffe {
|
for addr in 0x000..0xffe {
|
||||||
// Call an address
|
// Jump to an address
|
||||||
cpu.jump(addr);
|
cpu.jump(addr);
|
||||||
// Verify the current address is the called address
|
// Verify the current address is the jump target address
|
||||||
assert_eq!(addr, cpu.pc);
|
assert_eq!(addr, cpu.pc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -143,15 +134,15 @@ mod cf {
|
|||||||
#[test]
|
#[test]
|
||||||
fn call() {
|
fn call() {
|
||||||
let test_addr = random::<u16>();
|
let test_addr = random::<u16>();
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, _) = setup_environment();
|
||||||
// Save the current address
|
// Save the current address
|
||||||
let curr_addr = cpu.pc;
|
let curr_addr = cpu.pc;
|
||||||
// Call an address
|
// Call an address
|
||||||
cpu.call(test_addr, &mut bus);
|
cpu.call(test_addr);
|
||||||
// Verify the current address is the called address
|
// Verify the current address is the called address
|
||||||
assert_eq!(test_addr, cpu.pc);
|
assert_eq!(test_addr, cpu.pc);
|
||||||
// Verify the previous address was stored on the stack (sp+2)
|
// Verify the previous address was stored on the stack (sp+2)
|
||||||
let stack_addr: u16 = bus.read(cpu.sp.wrapping_add(2));
|
let stack_addr: u16 = cpu.stack.pop().expect("This should return test_addr");
|
||||||
assert_eq!(stack_addr, curr_addr);
|
assert_eq!(stack_addr, curr_addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -595,7 +586,7 @@ mod math {
|
|||||||
for reg in 0..=0xff {
|
for reg in 0..=0xff {
|
||||||
let (x, y) = (reg & 0xf, reg >> 4);
|
let (x, y) = (reg & 0xf, reg >> 4);
|
||||||
// set the register under test to `word`
|
// set the register under test to `word`
|
||||||
(cpu.v[x], cpu.v[y]) = dbg!(0, word);
|
(cpu.v[x], cpu.v[y]) = (0, word);
|
||||||
|
|
||||||
cpu.shift_left(x, y);
|
cpu.shift_left(x, y);
|
||||||
|
|
||||||
@ -674,9 +665,9 @@ mod i {
|
|||||||
/// - Random number generation
|
/// - Random number generation
|
||||||
/// - Drawing to the display
|
/// - Drawing to the display
|
||||||
mod io {
|
mod io {
|
||||||
|
use super::*;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use super::*;
|
|
||||||
/// Cxbb: Stores a random number & the provided byte into vX
|
/// Cxbb: Stores a random number & the provided byte into vX
|
||||||
#[test]
|
#[test]
|
||||||
fn rand() {
|
fn rand() {
|
||||||
@ -707,11 +698,12 @@ mod io {
|
|||||||
ScreenTest {
|
ScreenTest {
|
||||||
program: include_bytes!("../../chip-8/IBM Logo.ch8"),
|
program: include_bytes!("../../chip-8/IBM Logo.ch8"),
|
||||||
screen: include_bytes!("tests/screens/IBM Logo.ch8/20.bin"),
|
screen: include_bytes!("tests/screens/IBM Logo.ch8/20.bin"),
|
||||||
steps: 56,
|
steps: 21,
|
||||||
quirks: Quirks {
|
quirks: Quirks {
|
||||||
bin_ops: false,
|
bin_ops: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
draw_wait: false,
|
draw_wait: false,
|
||||||
|
screen_wrap: true,
|
||||||
dma_inc: false,
|
dma_inc: false,
|
||||||
stupid_jumps: false,
|
stupid_jumps: false,
|
||||||
},
|
},
|
||||||
@ -727,6 +719,7 @@ mod io {
|
|||||||
bin_ops: false,
|
bin_ops: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
draw_wait: true,
|
draw_wait: true,
|
||||||
|
screen_wrap: true,
|
||||||
dma_inc: false,
|
dma_inc: false,
|
||||||
stupid_jumps: false,
|
stupid_jumps: false,
|
||||||
},
|
},
|
||||||
@ -740,6 +733,7 @@ mod io {
|
|||||||
bin_ops: false,
|
bin_ops: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
draw_wait: true,
|
draw_wait: true,
|
||||||
|
screen_wrap: true,
|
||||||
dma_inc: false,
|
dma_inc: false,
|
||||||
stupid_jumps: false,
|
stupid_jumps: false,
|
||||||
},
|
},
|
||||||
@ -750,26 +744,21 @@ mod io {
|
|||||||
#[test]
|
#[test]
|
||||||
fn draw() {
|
fn draw() {
|
||||||
for test in SCREEN_TESTS {
|
for test in SCREEN_TESTS {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
cpu.flags.quirks = test.quirks;
|
cpu.flags.quirks = test.quirks;
|
||||||
// Debug mode is 5x slower
|
// Debug mode is 5x slower
|
||||||
cpu.flags.debug = false;
|
cpu.flags.debug = false;
|
||||||
// Load the test program
|
// Load the test program
|
||||||
bus = bus.load_region(Program, test.program);
|
cpu.mem.load_region(Program, test.program).unwrap();
|
||||||
// Run the test program for the specified number of steps
|
// Run the test program for the specified number of steps
|
||||||
while cpu.cycle() < test.steps {
|
while cpu.cycle() < test.steps {
|
||||||
cpu.multistep(&mut bus, test.steps - cpu.cycle())
|
cpu.multistep(&mut screen, 10.min(test.steps - cpu.cycle()))
|
||||||
.expect("Draw tests should not contain undefined instructions");
|
.expect("Draw tests should not contain undefined instructions");
|
||||||
}
|
}
|
||||||
// Compare the screen to the reference screen buffer
|
// Compare the screen to the reference screen buffer
|
||||||
bus.print_screen()
|
screen.print_screen();
|
||||||
.expect("Printing screen should not fail if screen exists");
|
|
||||||
print_screen(test.screen);
|
print_screen(test.screen);
|
||||||
assert_eq!(
|
assert_eq!(screen.grab(..).unwrap(), test.screen);
|
||||||
bus.get_region(Screen)
|
|
||||||
.expect("Getting screen should not fail if screen exists"),
|
|
||||||
test.screen
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -860,7 +849,7 @@ mod io {
|
|||||||
assert!(cpu.release(key).expect("Key should be released"));
|
assert!(cpu.release(key).expect("Key should be released"));
|
||||||
assert!(!cpu.release(key).expect("Key shouldn't be released again"));
|
assert!(!cpu.release(key).expect("Key shouldn't be released again"));
|
||||||
assert!(!cpu.flags.keypause);
|
assert!(!cpu.flags.keypause);
|
||||||
assert_eq!(Some(key), cpu.flags.lastkey);
|
assert_eq!(Some(key), cpu.lastkey);
|
||||||
cpu.wait_for_key(x);
|
cpu.wait_for_key(x);
|
||||||
assert_eq!(key as u8, cpu.v[x]);
|
assert_eq!(key as u8, cpu.v[x]);
|
||||||
}
|
}
|
||||||
@ -875,8 +864,7 @@ mod io {
|
|||||||
for word in 0..=0xff {
|
for word in 0..=0xff {
|
||||||
for x in 0..=0xf {
|
for x in 0..=0xf {
|
||||||
// set the register under test to `word`
|
// set the register under test to `word`
|
||||||
cpu.delay = word as f64;
|
cpu.delay = word;
|
||||||
|
|
||||||
cpu.load_delay_timer(x);
|
cpu.load_delay_timer(x);
|
||||||
|
|
||||||
assert_eq!(cpu.v[x], word);
|
assert_eq!(cpu.v[x], word);
|
||||||
@ -895,7 +883,7 @@ mod io {
|
|||||||
|
|
||||||
cpu.store_delay_timer(x);
|
cpu.store_delay_timer(x);
|
||||||
|
|
||||||
assert_eq!(cpu.delay, word as f64);
|
assert_eq!(cpu.delay, word);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -911,7 +899,7 @@ mod io {
|
|||||||
|
|
||||||
cpu.store_sound_timer(x);
|
cpu.store_sound_timer(x);
|
||||||
|
|
||||||
assert_eq!(cpu.sound, word as f64);
|
assert_eq!(cpu.sound, word);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -948,7 +936,7 @@ mod io {
|
|||||||
/// Fx29: Load sprite for character vX into I
|
/// Fx29: Load sprite for character vX into I
|
||||||
#[test]
|
#[test]
|
||||||
fn load_sprite() {
|
fn load_sprite() {
|
||||||
let (mut cpu, bus) = setup_environment();
|
let (mut cpu, _) = setup_environment();
|
||||||
for test in TESTS {
|
for test in TESTS {
|
||||||
let reg = 0xf & random::<usize>();
|
let reg = 0xf & random::<usize>();
|
||||||
// load number into CPU register
|
// load number into CPU register
|
||||||
@ -958,7 +946,8 @@ mod io {
|
|||||||
|
|
||||||
let addr = cpu.i as usize;
|
let addr = cpu.i as usize;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
bus.get(addr..addr.wrapping_add(5))
|
cpu.mem
|
||||||
|
.grab(addr..addr.wrapping_add(5))
|
||||||
.expect("Region at addr should exist!"),
|
.expect("Region at addr should exist!"),
|
||||||
test.output,
|
test.output,
|
||||||
);
|
);
|
||||||
@ -967,7 +956,7 @@ mod io {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod bcdtest {
|
mod bcdtest {
|
||||||
pub(self) use super::*;
|
use super::*;
|
||||||
|
|
||||||
struct BCDTest {
|
struct BCDTest {
|
||||||
// value to test
|
// value to test
|
||||||
@ -995,15 +984,18 @@ mod io {
|
|||||||
#[test]
|
#[test]
|
||||||
fn bcd_convert() {
|
fn bcd_convert() {
|
||||||
for test in BCD_TESTS {
|
for test in BCD_TESTS {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, _) = setup_environment();
|
||||||
let addr = 0xff0 & random::<u16>() as usize;
|
let addr = 0xff0 & random::<u16>() as usize;
|
||||||
// load CPU registers
|
// load CPU registers
|
||||||
cpu.i = addr as u16;
|
cpu.i = addr as u16;
|
||||||
cpu.v[5] = test.input;
|
cpu.v[5] = test.input;
|
||||||
|
|
||||||
cpu.bcd_convert(5, &mut bus);
|
cpu.bcd_convert(5);
|
||||||
|
|
||||||
assert_eq!(bus.get(addr..addr.saturating_add(3)), Some(test.output))
|
assert_eq!(
|
||||||
|
cpu.mem.grab(addr..addr.saturating_add(3)),
|
||||||
|
Some(test.output)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1012,7 +1004,7 @@ mod io {
|
|||||||
// TODO: Test with dma_inc quirk set
|
// TODO: Test with dma_inc quirk set
|
||||||
#[test]
|
#[test]
|
||||||
fn dma_store() {
|
fn dma_store() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, _) = setup_environment();
|
||||||
const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
|
const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
|
||||||
// Load some test data into memory
|
// Load some test data into memory
|
||||||
let addr = 0x456;
|
let addr = 0x456;
|
||||||
@ -1023,15 +1015,16 @@ mod io {
|
|||||||
for len in 0..16 {
|
for len in 0..16 {
|
||||||
// Perform DMA store
|
// Perform DMA store
|
||||||
cpu.i = addr as u16;
|
cpu.i = addr as u16;
|
||||||
cpu.store_dma(len, &mut bus);
|
cpu.store_dma(len);
|
||||||
// Check that bus grabbed the correct data
|
// Check that screen grabbed the correct data
|
||||||
let bus = bus
|
let screen = cpu
|
||||||
.get_mut(addr..addr + DATA.len())
|
.mem
|
||||||
|
.grab_mut(addr..addr + DATA.len())
|
||||||
.expect("Getting a mutable slice at addr 0x0456 should not fail");
|
.expect("Getting a mutable slice at addr 0x0456 should not fail");
|
||||||
assert_eq!(bus[0..=len], DATA[0..=len]);
|
assert_eq!(screen[0..=len], DATA[0..=len]);
|
||||||
assert_eq!(bus[len + 1..], [0; 16][len + 1..]);
|
assert_eq!(screen[len + 1..], [0; 16][len + 1..]);
|
||||||
// clear
|
// clear
|
||||||
bus.fill(0);
|
screen.fill(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1039,18 +1032,19 @@ mod io {
|
|||||||
// TODO: Test with dma_inc quirk set
|
// TODO: Test with dma_inc quirk set
|
||||||
#[test]
|
#[test]
|
||||||
fn dma_load() {
|
fn dma_load() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, _) = setup_environment();
|
||||||
const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
|
const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
|
||||||
// Load some test data into memory
|
// Load some test data into memory
|
||||||
let addr = 0x456;
|
let addr = 0x456;
|
||||||
bus.get_mut(addr..addr + DATA.len())
|
cpu.mem
|
||||||
|
.grab_mut(addr..addr + DATA.len())
|
||||||
.expect("Getting a mutable slice at addr 0x0456..0x0466 should not fail")
|
.expect("Getting a mutable slice at addr 0x0456..0x0466 should not fail")
|
||||||
.write_all(DATA)
|
.write_all(DATA)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for len in 0..16 {
|
for len in 0..16 {
|
||||||
// Perform DMA load
|
// Perform DMA load
|
||||||
cpu.i = addr as u16;
|
cpu.i = addr as u16;
|
||||||
cpu.load_dma(len, &mut bus);
|
cpu.load_dma(len);
|
||||||
// Check that registers grabbed the correct data
|
// Check that registers grabbed the correct data
|
||||||
assert_eq!(cpu.v[0..=len], DATA[0..=len]);
|
assert_eq!(cpu.v[0..=len], DATA[0..=len]);
|
||||||
assert_eq!(cpu.v[len + 1..], [0; 16][len + 1..]);
|
assert_eq!(cpu.v[len + 1..], [0; 16][len + 1..]);
|
||||||
@ -1068,37 +1062,37 @@ mod behavior {
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
#[test]
|
#[test]
|
||||||
fn delay() {
|
fn delay() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
cpu.flags.monotonic = None;
|
cpu.flags.monotonic = false;
|
||||||
cpu.delay = 10.0;
|
cpu.delay = 10;
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
cpu.multistep(&mut bus, 8)
|
cpu.multistep(&mut screen, 8)
|
||||||
.expect("Running valid instructions should always succeed");
|
.expect("Running valid instructions should always succeed");
|
||||||
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
||||||
}
|
}
|
||||||
// time is within 1 frame deviance over a theoretical 2 frame pause
|
// time is within 1 frame deviance over a theoretical 2 frame pause
|
||||||
assert!(7.0 <= cpu.delay && cpu.delay <= 9.0);
|
assert_eq!(cpu.delay, 8);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn sound() {
|
fn sound() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
cpu.flags.monotonic = None; // disable monotonic timing
|
cpu.flags.monotonic = false; // disable monotonic timing
|
||||||
cpu.sound = 10.0;
|
cpu.sound = 10;
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
cpu.multistep(&mut bus, 8)
|
cpu.multistep(&mut screen, 8)
|
||||||
.expect("Running valid instructions should always succeed");
|
.expect("Running valid instructions should always succeed");
|
||||||
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
||||||
}
|
}
|
||||||
// time is within 1 frame deviance over a theoretical 2 frame pause
|
// time is within 1 frame deviance over a theoretical 2 frame pause
|
||||||
assert!(7.0 <= cpu.sound && cpu.sound <= 9.0);
|
assert_eq!(cpu.sound, 8);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn vbi_wait() {
|
fn vbi_wait() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
cpu.flags.monotonic = None; // disable monotonic timing
|
cpu.flags.monotonic = false; // disable monotonic timing
|
||||||
cpu.flags.draw_wait = true;
|
cpu.flags.draw_wait = true;
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
cpu.multistep(&mut bus, 8)
|
cpu.multistep(&mut screen, 8)
|
||||||
.expect("Running valid instructions should always succeed");
|
.expect("Running valid instructions should always succeed");
|
||||||
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
||||||
}
|
}
|
||||||
@ -1112,9 +1106,9 @@ mod behavior {
|
|||||||
#[test]
|
#[test]
|
||||||
#[cfg_attr(feature = "unstable", no_coverage)]
|
#[cfg_attr(feature = "unstable", no_coverage)]
|
||||||
fn hit_break() {
|
fn hit_break() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
cpu.set_break(0x202);
|
cpu.set_break(0x202);
|
||||||
match cpu.multistep(&mut bus, 10) {
|
match cpu.multistep(&mut screen, 10) {
|
||||||
Err(crate::error::Error::BreakpointHit { addr, next }) => {
|
Err(crate::error::Error::BreakpointHit { addr, next }) => {
|
||||||
assert_eq!(0x202, addr); // current address is 202
|
assert_eq!(0x202, addr); // current address is 202
|
||||||
assert_eq!(0x1204, next); // next insn is `jmp 204`
|
assert_eq!(0x1204, next); // next insn is `jmp 204`
|
||||||
@ -1127,9 +1121,9 @@ mod behavior {
|
|||||||
#[test]
|
#[test]
|
||||||
#[cfg_attr(feature = "unstable", no_coverage)]
|
#[cfg_attr(feature = "unstable", no_coverage)]
|
||||||
fn hit_break_singlestep() {
|
fn hit_break_singlestep() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
cpu.set_break(0x202);
|
cpu.set_break(0x202);
|
||||||
match cpu.singlestep(&mut bus) {
|
match cpu.singlestep(&mut screen) {
|
||||||
Err(crate::error::Error::BreakpointHit { addr, next }) => {
|
Err(crate::error::Error::BreakpointHit { addr, next }) => {
|
||||||
assert_eq!(0x202, addr); // current address is 202
|
assert_eq!(0x202, addr); // current address is 202
|
||||||
assert_eq!(0x1204, next); // next insn is `jmp 204`
|
assert_eq!(0x1204, next); // next insn is `jmp 204`
|
||||||
@ -1144,12 +1138,12 @@ mod behavior {
|
|||||||
#[test]
|
#[test]
|
||||||
#[cfg_attr(feature = "unstable", no_coverage)]
|
#[cfg_attr(feature = "unstable", no_coverage)]
|
||||||
fn invalid_pc() {
|
fn invalid_pc() {
|
||||||
let (mut cpu, mut bus) = setup_environment();
|
let (mut cpu, mut screen) = setup_environment();
|
||||||
// The bus extends from 0x0..0x1000
|
// The screen extends from 0x0..0x1000
|
||||||
cpu.pc = 0xfff;
|
cpu.pc = 0x1001;
|
||||||
match cpu.tick(&mut bus) {
|
match cpu.tick(&mut screen) {
|
||||||
Err(Error::InvalidBusRange { range }) => {
|
Err(Error::InvalidAddressRange { range }) => {
|
||||||
eprintln!("InvalidBusRange {{ {range:04x?} }}")
|
eprintln!("InvalidAddressRange {{ {range:04x?} }}")
|
||||||
}
|
}
|
||||||
other => unreachable!("{other:04x?}"),
|
other => unreachable!("{other:04x?}"),
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Exercises the instruction decode logic.
|
//! Exercises the instruction decode logic.
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -6,15 +9,15 @@ const INDX: &[u8; 16] = b"\0\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d
|
|||||||
/// runs one arbitrary operation on a brand new CPU
|
/// runs one arbitrary operation on a brand new CPU
|
||||||
/// returns the CPU for inspection
|
/// returns the CPU for inspection
|
||||||
fn run_single_op(op: &[u8]) -> CPU {
|
fn run_single_op(op: &[u8]) -> CPU {
|
||||||
let (mut cpu, mut bus) = (
|
let (mut cpu, mut screen) = (
|
||||||
CPU::default(),
|
CPU::default(),
|
||||||
bus! {
|
Screen::default(),
|
||||||
Program[0x200..0x240] = op,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
cpu.mem
|
||||||
|
.load_region(Program, op).unwrap();
|
||||||
cpu.v = *INDX;
|
cpu.v = *INDX;
|
||||||
cpu.flags.quirks = Quirks::from(false);
|
cpu.flags.quirks = Quirks::from(false);
|
||||||
cpu.tick(&mut bus).unwrap(); // will panic if unimplemented
|
cpu.tick(&mut screen).unwrap(); // will panic if unimplemented
|
||||||
cpu
|
cpu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
Binary file not shown.
29
src/error.rs
29
src/error.rs
@ -1,11 +1,12 @@
|
|||||||
// (c) 2023 John A. Breaux
|
// (c) 2023 John A. Breaux
|
||||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
//! Error type for Chirp
|
//! Error type for Chirp
|
||||||
|
|
||||||
use std::ops::Range;
|
pub mod any_range;
|
||||||
|
use any_range::AnyRange;
|
||||||
|
|
||||||
use crate::bus::Region;
|
use crate::cpu::mem::Region;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Result type, equivalent to [std::result::Result]<T, [enum@Error]>
|
/// Result type, equivalent to [std::result::Result]<T, [enum@Error]>
|
||||||
@ -15,7 +16,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// Represents a breakpoint being hit
|
/// Represents a breakpoint being hit
|
||||||
#[error("Breakpoint hit: {addr:03x} ({next:04x})")]
|
#[error("breakpoint hit: {addr:03x} ({next:04x})")]
|
||||||
BreakpointHit {
|
BreakpointHit {
|
||||||
/// The address of the breakpoint
|
/// The address of the breakpoint
|
||||||
addr: u16,
|
addr: u16,
|
||||||
@ -23,37 +24,37 @@ pub enum Error {
|
|||||||
next: u16,
|
next: u16,
|
||||||
},
|
},
|
||||||
/// Represents an unimplemented operation
|
/// Represents an unimplemented operation
|
||||||
#[error("Unrecognized opcode: {word:04x}")]
|
#[error("opcode {word:04x} not recognized")]
|
||||||
UnimplementedInstruction {
|
UnimplementedInstruction {
|
||||||
/// The offending word
|
/// The offending word
|
||||||
word: u16,
|
word: u16,
|
||||||
},
|
},
|
||||||
/// The region you asked for was not defined
|
/// The region you asked for was not defined
|
||||||
#[error("No {region} found on bus")]
|
#[error("region {region} is not present on bus")]
|
||||||
MissingRegion {
|
MissingRegion {
|
||||||
/// The offending [Region]
|
/// The offending [Region]
|
||||||
region: Region,
|
region: Region,
|
||||||
},
|
},
|
||||||
/// Tried to fetch [Range] from bus, received nothing
|
/// Tried to fetch data at [AnyRange] from bus, received nothing
|
||||||
#[error("Invalid range {range:04x?} for bus")]
|
#[error("range {range:04x?} is not present on bus")]
|
||||||
InvalidBusRange {
|
InvalidAddressRange {
|
||||||
/// The offending [Range]
|
/// The offending [AnyRange]
|
||||||
range: Range<usize>,
|
range: AnyRange<usize>,
|
||||||
},
|
},
|
||||||
/// Tried to press a key that doesn't exist
|
/// Tried to press a key that doesn't exist
|
||||||
#[error("Invalid key: {key:X}")]
|
#[error("tried to press key {key:X} which does not exist")]
|
||||||
InvalidKey {
|
InvalidKey {
|
||||||
/// The offending key
|
/// The offending key
|
||||||
key: usize,
|
key: usize,
|
||||||
},
|
},
|
||||||
/// Tried to get/set an out-of-bounds register
|
/// Tried to get/set an out-of-bounds register
|
||||||
#[error("Invalid register: v{reg:X}")]
|
#[error("tried to access register v{reg:X} which does not exist")]
|
||||||
InvalidRegister {
|
InvalidRegister {
|
||||||
/// The offending register
|
/// The offending register
|
||||||
reg: usize,
|
reg: usize,
|
||||||
},
|
},
|
||||||
/// Tried to convert string into mode, but it did not match.
|
/// Tried to convert string into mode, but it did not match.
|
||||||
#[error("Invalid mode: {mode}")]
|
#[error("no suitable conversion of \"{mode}\" into Mode")]
|
||||||
InvalidMode {
|
InvalidMode {
|
||||||
/// The string which failed to become a mode
|
/// The string which failed to become a mode
|
||||||
mode: String,
|
mode: String,
|
||||||
|
69
src/error/any_range.rs
Normal file
69
src/error/any_range.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
|
//! Holder for any [Range]
|
||||||
|
|
||||||
|
// This is super over-engineered, considering it was originally meant to help with only one LOC
|
||||||
|
use std::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod macros;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error, PartialEq, Eq, Hash)]
|
||||||
|
#[error("Failed to convert variant {0} back into range.")]
|
||||||
|
/// Emitted when conversion back into a [std::ops]::Range\* fails.
|
||||||
|
pub struct AnyRangeError<Idx>(AnyRange<Idx>);
|
||||||
|
|
||||||
|
/// Holder for any [Range]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum AnyRange<Idx> {
|
||||||
|
/// Bounded exclusive [Range] i.e. `0..10`
|
||||||
|
Range(Range<Idx>),
|
||||||
|
/// Unbounded [RangeFrom], i.e. `0..`
|
||||||
|
RangeFrom(RangeFrom<Idx>),
|
||||||
|
/// Unbounded [RangeFull], i.e. `..`
|
||||||
|
RangeFull(RangeFull),
|
||||||
|
/// Bounded inclusive [RangeInclusive], i.e. `0..=10`
|
||||||
|
RangeInclusive(RangeInclusive<Idx>),
|
||||||
|
/// Unbounded [RangeTo], i.e. `..10`
|
||||||
|
RangeTo(RangeTo<Idx>),
|
||||||
|
/// Unbounded inclusive [RangeToInclusive], i.e. `..=10`
|
||||||
|
RangeToInclusive(RangeToInclusive<Idx>),
|
||||||
|
}
|
||||||
|
|
||||||
|
variant_from! {
|
||||||
|
match impl<Idx> (From, TryInto) for AnyRange<Idx> {
|
||||||
|
type Error = AnyRangeError<Idx>;
|
||||||
|
Range<Idx> => AnyRange::Range,
|
||||||
|
RangeFrom<Idx> => AnyRange::RangeFrom,
|
||||||
|
RangeFull => AnyRange::RangeFull,
|
||||||
|
RangeInclusive<Idx> => AnyRange::RangeInclusive,
|
||||||
|
RangeTo<Idx> => AnyRange::RangeTo,
|
||||||
|
RangeToInclusive<Idx> => AnyRange::RangeToInclusive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenient conversion functions from [AnyRange] to the inner type
|
||||||
|
impl<Idx> AnyRange<Idx> {
|
||||||
|
try_into_fn! {
|
||||||
|
/// Converts from [AnyRange::Range] into a [Range], else [None]
|
||||||
|
pub fn range(self) -> Option<Range<Idx>>;
|
||||||
|
/// Converts from [AnyRange::RangeFrom] into a [RangeFrom], else [None]
|
||||||
|
pub fn range_from(self) -> Option<RangeFrom<Idx>>;
|
||||||
|
/// Converts from [AnyRange::RangeFull] into a [RangeFull], else [None]
|
||||||
|
pub fn range_full(self) -> Option<RangeFull>;
|
||||||
|
/// Converts from [AnyRange::RangeInclusive] into a [RangeInclusive], else [None]
|
||||||
|
pub fn range_inclusive(self) -> Option<RangeInclusive<Idx>>;
|
||||||
|
/// Converts from [AnyRange::RangeTo] into a [RangeTo], else [None]
|
||||||
|
pub fn range_to(self) -> Option<RangeTo<Idx>>;
|
||||||
|
/// Converts from [AnyRange::RangeToInclusive] into a [RangeToInclusive], else [None]
|
||||||
|
pub fn range_to_inclusive(self) -> Option<RangeToInclusive<Idx>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Idx> From<AnyRange<Idx>> for AnyRangeError<Idx> {
|
||||||
|
fn from(value: AnyRange<Idx>) -> Self {
|
||||||
|
AnyRangeError(value)
|
||||||
|
}
|
||||||
|
}
|
33
src/error/any_range/macros.rs
Normal file
33
src/error/any_range/macros.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
//! Macros for AnyRange
|
||||||
|
|
||||||
|
/// Generate From and TryFrom impls for each variant
|
||||||
|
macro_rules! variant_from {(
|
||||||
|
match impl<$T:ident> $_:tt for $enum:path {
|
||||||
|
type Error = $error:path;
|
||||||
|
$($src:path => $variant:path),+$(,)?
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
// The forward direction is infallible
|
||||||
|
$(impl<$T> From<$src> for $enum {
|
||||||
|
fn from(value: $src) -> Self { $variant(value) }
|
||||||
|
})+
|
||||||
|
// The reverse direction could fail if the $variant doesn't hold $src
|
||||||
|
$(impl<$T> TryFrom<$enum> for $src {
|
||||||
|
type Error = $error;
|
||||||
|
fn try_from(value: $enum) -> Result<Self, Self::Error> {
|
||||||
|
if let $variant(r) = value { Ok(r) } else { Err(value.into()) }
|
||||||
|
}
|
||||||
|
})+
|
||||||
|
}}
|
||||||
|
|
||||||
|
/// Turns a list of function prototypes into functions which use [TryInto], returning [Option]
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// try_into_fn! { fn range(self) -> Option<Other>; }
|
||||||
|
/// ```
|
||||||
|
macro_rules! try_into_fn {
|
||||||
|
($($(#[$doc:meta])? $pub:vis fn $name:ident $args:tt -> $ret:ty);+ $(;)?) => {
|
||||||
|
$($(#[$doc])? $pub fn $name $args -> $ret { $args.try_into().ok() })+
|
||||||
|
}
|
||||||
|
}
|
21
src/lib.rs
21
src/lib.rs
@ -1,6 +1,6 @@
|
|||||||
// (c) 2023 John A. Breaux
|
// (c) 2023 John A. Breaux
|
||||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
#![cfg_attr(feature = "unstable", feature(no_coverage))]
|
#![cfg_attr(feature = "nightly", feature(no_coverage))]
|
||||||
#![deny(missing_docs, clippy::all)]
|
#![deny(missing_docs, clippy::all)]
|
||||||
//! This crate implements a Chip-8 interpreter as if it were a real CPU architecture,
|
//! 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
|
//! to the best of my current knowledge. As it's the first emulator project I've
|
||||||
@ -8,26 +8,19 @@
|
|||||||
//!
|
//!
|
||||||
//! Hopefully, though, you'll find some use in it.
|
//! Hopefully, though, you'll find some use in it.
|
||||||
|
|
||||||
pub mod bus;
|
|
||||||
pub mod cpu;
|
pub mod cpu;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod screen;
|
||||||
|
pub mod traits;
|
||||||
|
|
||||||
// Common imports for Chirp
|
// Common imports for Chirp
|
||||||
pub use bus::{Bus, Read, Region::*, Write};
|
|
||||||
pub use cpu::{
|
pub use cpu::{
|
||||||
disassembler::{Dis, Disassembler},
|
|
||||||
flags::Flags,
|
flags::Flags,
|
||||||
|
instruction::disassembler::{Dis, Disassembler},
|
||||||
mode::Mode,
|
mode::Mode,
|
||||||
quirks::Quirks,
|
quirks::Quirks,
|
||||||
CPU,
|
CPU,
|
||||||
};
|
};
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
pub use screen::Screen;
|
||||||
/// Holds the state of a Chip-8
|
pub use traits::{AutoCast, FallibleAutoCast, Grab};
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
|
||||||
pub struct Chip8 {
|
|
||||||
/// Contains the registers, flags, and operating state for a single Chip-8
|
|
||||||
pub cpu: cpu::CPU,
|
|
||||||
/// Contains the memory of a chip-8
|
|
||||||
pub bus: bus::Bus,
|
|
||||||
}
|
|
||||||
|
BIN
src/mem/hires.bin
Normal file
BIN
src/mem/hires.bin
Normal file
Binary file not shown.
132
src/screen.rs
Normal file
132
src/screen.rs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
//! Contains the raw screen bytes, and an iterator over individual bits of those bytes.
|
||||||
|
|
||||||
|
use crate::traits::Grab;
|
||||||
|
|
||||||
|
/// Stores the screen bytes
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Screen {
|
||||||
|
/// The screen bytes
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]>> From<T> for Screen {
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Screen {
|
||||||
|
bytes: value.as_ref().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Screen {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Grab for Screen {
|
||||||
|
fn grab<I>(&self, index: I) -> Option<&<I as std::slice::SliceIndex<[u8]>>::Output>
|
||||||
|
where
|
||||||
|
I: std::slice::SliceIndex<[u8]>,
|
||||||
|
{
|
||||||
|
self.bytes.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as std::slice::SliceIndex<[u8]>>::Output>
|
||||||
|
where
|
||||||
|
I: std::slice::SliceIndex<[u8]>,
|
||||||
|
{
|
||||||
|
self.bytes.get_mut(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Screen {
|
||||||
|
/// Creates a new [Screen]
|
||||||
|
pub fn new(size: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
bytes: vec![0; size],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the screen has 0 elements
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.bytes.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the length of the [Screen]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.bytes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a slice of the whole [Screen]
|
||||||
|
pub fn as_slice(&self) -> &[u8] {
|
||||||
|
self.bytes.as_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the [Screen] to 0
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.bytes.fill(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grows the [Screen] memory to at least size bytes, but does not truncate
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
///# use chirp::screen::Screen;
|
||||||
|
/// let mut screen = Screen::new(256);
|
||||||
|
/// assert_eq!(256, screen.len());
|
||||||
|
/// screen.with_size(0);
|
||||||
|
/// assert_eq!(256, screen.len());
|
||||||
|
/// ```
|
||||||
|
pub fn with_size(&mut self, size: usize) {
|
||||||
|
if self.len() < size {
|
||||||
|
self.bytes.resize(size, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prints the [Screen] using either [drawille] or Box Drawing Characters
|
||||||
|
/// # Examples
|
||||||
|
/// [Screen::print_screen] will print the screen
|
||||||
|
/// ```rust
|
||||||
|
///# use chirp::screen::Screen;
|
||||||
|
/// let screen = Screen::default();
|
||||||
|
/// screen.print_screen();
|
||||||
|
/// ```
|
||||||
|
pub fn print_screen(&self) {
|
||||||
|
let len_log2 = self.bytes.len().ilog2() / 2;
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
let (width, height) = (2u32.pow(len_log2 - 1), 2u32.pow(len_log2 + 1) - 1);
|
||||||
|
// draw with the drawille library, if available
|
||||||
|
#[cfg(feature = "drawille")]
|
||||||
|
{
|
||||||
|
use drawille::Canvas;
|
||||||
|
let mut canvas = Canvas::new(width * 8, height);
|
||||||
|
let width = width * 8;
|
||||||
|
self.bytes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(bytei, byte)| {
|
||||||
|
(0..8).enumerate().filter_map(move |(biti, bit)| {
|
||||||
|
if (byte << bit) & 0x80 != 0 {
|
||||||
|
Some(bytei * 8 + biti)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.for_each(|index| canvas.set(index as u32 % (width), index as u32 / (width)));
|
||||||
|
println!("{}", canvas.frame());
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "drawille"))]
|
||||||
|
for (index, byte) in self.bytes.iter().enumerate() {
|
||||||
|
if index % width as usize == 0 {
|
||||||
|
print!("{index:03x}|");
|
||||||
|
}
|
||||||
|
print!(
|
||||||
|
"{}",
|
||||||
|
format!("{byte:08b}").replace('0', " ").replace('1', "█")
|
||||||
|
);
|
||||||
|
if index % width as usize == width as usize - 1 {
|
||||||
|
println!("|");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
src/traits.rs
Normal file
7
src/traits.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
|
//! Traits useful for Chirp
|
||||||
|
|
||||||
|
mod auto_cast;
|
||||||
|
pub use auto_cast::{AutoCast, FallibleAutoCast, Grab};
|
106
src/traits/auto_cast.rs
Normal file
106
src/traits/auto_cast.rs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
|
||||||
|
//! Traits for automatically serializing and deserializing Rust primitive types.
|
||||||
|
//!
|
||||||
|
//! Users of this module should impl [Grab]`<u8>` for their type, which notably returns `&[u8]` and `&mut [u8]`
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use core::mem::size_of;
|
||||||
|
use std::{fmt::Debug, slice::SliceIndex};
|
||||||
|
|
||||||
|
/// Get Raw Bytes at [SliceIndex] `I`.
|
||||||
|
///
|
||||||
|
/// This is similar to the [SliceIndex] method `.get(...)`, however implementing this
|
||||||
|
/// trait will auto-impl [AutoCast]<([i8], [u8], [i16], [u16] ... [i128], [u128])>
|
||||||
|
pub trait Grab {
|
||||||
|
/// Gets the slice of Self at [SliceIndex] I
|
||||||
|
fn grab<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
|
||||||
|
where
|
||||||
|
I: SliceIndex<[u8]>;
|
||||||
|
|
||||||
|
/// Gets a mutable slice of Self at [SliceIndex] I
|
||||||
|
fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[u8]>>::Output>
|
||||||
|
where
|
||||||
|
I: SliceIndex<[u8]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read or Write a T at address `addr`
|
||||||
|
pub trait AutoCast<T>: FallibleAutoCast<T> {
|
||||||
|
/// Reads a T from address `addr`
|
||||||
|
///
|
||||||
|
/// # May Panic
|
||||||
|
///
|
||||||
|
/// This will panic on error. For a non-panicking implementation, do it yourself.
|
||||||
|
fn read(&self, addr: impl Into<usize>) -> T {
|
||||||
|
self.read_fallible(addr).unwrap_or_else(|e| panic!("{e:?}"))
|
||||||
|
}
|
||||||
|
/// Writes a T to address `addr`
|
||||||
|
///
|
||||||
|
/// # Will Panic
|
||||||
|
///
|
||||||
|
/// This will panic on error. For a non-panicking implementation, do it yourself.
|
||||||
|
fn write(&mut self, addr: impl Into<usize>, data: T) {
|
||||||
|
self.write_fallible(addr, data)
|
||||||
|
.unwrap_or_else(|e| panic!("{e:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a T from address `addr`, and return the value as a [Result]
|
||||||
|
pub trait FallibleAutoCast<T>: Grab {
|
||||||
|
/// The [Err] type
|
||||||
|
type Error: Debug;
|
||||||
|
/// Read a T from address `addr`, returning the value as a [Result]
|
||||||
|
fn read_fallible(&self, addr: impl Into<usize>) -> Result<T, Self::Error>;
|
||||||
|
/// Write a T to address `addr`, returning the value as a [Result]
|
||||||
|
fn write_fallible(&mut self, addr: impl Into<usize>, data: T) -> Result<(), Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements Read and Write for the provided types
|
||||||
|
///
|
||||||
|
/// Relies on inherent methods of Rust numeric types:
|
||||||
|
/// - `Self::from_be_bytes`
|
||||||
|
/// - `Self::to_be_bytes`
|
||||||
|
macro_rules! impl_rw {($($t:ty) ,* $(,)?) =>{
|
||||||
|
$(
|
||||||
|
#[doc = concat!("Read or Write [`", stringify!($t), "`] at address `addr`, *discarding errors*.\n\nThis will never panic.")]
|
||||||
|
impl<T: Grab + FallibleAutoCast<$t>> AutoCast<$t> for T {
|
||||||
|
#[inline(always)]
|
||||||
|
fn read(&self, addr: impl Into<usize>) -> $t {
|
||||||
|
self.read_fallible(addr).ok().unwrap_or_default()
|
||||||
|
}
|
||||||
|
#[inline(always)]
|
||||||
|
fn write(&mut self, addr: impl Into<usize>, data: $t) {
|
||||||
|
self.write_fallible(addr, data).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T: Grab> FallibleAutoCast<$t> for T {
|
||||||
|
type Error = $crate::error::Error;
|
||||||
|
#[inline(always)]
|
||||||
|
fn read_fallible(&self, addr: impl Into<usize>) -> $crate::error::Result<$t> {
|
||||||
|
let addr: usize = addr.into();
|
||||||
|
let top = addr + core::mem::size_of::<$t>();
|
||||||
|
if let Some(bytes) = self.grab(addr..top) {
|
||||||
|
// Chip-8 is a big-endian system
|
||||||
|
Ok(<$t>::from_be_bytes(bytes.try_into()?))
|
||||||
|
} else {
|
||||||
|
Err($crate::error::Error::InvalidAddressRange{range: (addr..top).into()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline(always)]
|
||||||
|
fn write_fallible(&mut self, addr: impl Into<usize>, data: $t) -> std::result::Result<(), Self::Error> {
|
||||||
|
let addr: usize = addr.into();
|
||||||
|
if let Some(slice) = self.grab_mut(addr..addr + core::mem::size_of::<$t>()) {
|
||||||
|
// Chip-8 is a big-endian system
|
||||||
|
data.to_be_bytes().as_mut().swap_with_slice(slice);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Using macro to be "generic" over types without traits in common
|
||||||
|
impl_rw!(i8, i16, i32, i64, i128);
|
||||||
|
impl_rw!(u8, u16, u32, u64, u128);
|
||||||
|
impl_rw!(f32, f64);
|
@ -1,46 +1,42 @@
|
|||||||
|
// (c) 2023 John A. Breaux
|
||||||
|
// This code is licensed under MIT license (see LICENSE for details)
|
||||||
|
// When compiled, the resulting binary is licensed under version 3 of the GNU General Public License (see chip8-test-suite/LICENSE for details)
|
||||||
|
|
||||||
//! These are a series of interpreter tests using Timendus's incredible test suite
|
//! These are a series of interpreter tests using Timendus's incredible test suite
|
||||||
|
|
||||||
pub use chirp::*;
|
pub use chirp::*;
|
||||||
|
|
||||||
fn setup_environment() -> (CPU, Bus) {
|
fn setup_environment() -> (CPU, Screen) {
|
||||||
let mut cpu = CPU::default();
|
let mut cpu = CPU::default();
|
||||||
cpu.flags = Flags {
|
cpu.flags = Flags {
|
||||||
debug: true,
|
debug: true,
|
||||||
pause: false,
|
pause: false,
|
||||||
monotonic: Some(8),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
(
|
(cpu, Screen::default())
|
||||||
cpu,
|
|
||||||
bus! {
|
|
||||||
// Load the charset into ROM
|
|
||||||
Charset [0x0050..0x00A0] = include_bytes!("../src/mem/charset.bin"),
|
|
||||||
// Load the ROM file into RAM
|
|
||||||
Program [0x0200..0x1000] = include_bytes!("../chip8-test-suite/bin/chip8-test-suite.ch8"),
|
|
||||||
// Create a screen, and fill it with
|
|
||||||
Screen [0x0F00..0x1000] = include_bytes!("chip8_test_suite.rs"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SuiteTest {
|
struct SuiteTest {
|
||||||
test: u16,
|
test: u8,
|
||||||
|
data: &'static [u8],
|
||||||
screen: &'static [u8],
|
screen: &'static [u8],
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_screentest(test: SuiteTest, mut cpu: CPU, mut bus: Bus) {
|
fn run_screentest(test: SuiteTest, mut cpu: CPU, mut screen: Screen) {
|
||||||
// Set the test to run
|
// Set the test to run
|
||||||
bus.write(0x1feu16, test.test);
|
cpu.poke(0x1ffu16, test.test);
|
||||||
|
cpu.load_program_bytes(test.data).unwrap();
|
||||||
// The test suite always initiates a keypause on test completion
|
// The test suite always initiates a keypause on test completion
|
||||||
while !cpu.flags.keypause {
|
while !(cpu.flags.is_paused()) {
|
||||||
cpu.multistep(&mut bus, 8).unwrap();
|
cpu.multistep(&mut screen, 10).unwrap();
|
||||||
|
if cpu.cycle() > 1000000 {
|
||||||
|
panic!("test {} took too long", test.test)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Compare the screen to the reference screen buffer
|
// Compare the screen to the reference screen buffer
|
||||||
bus.print_screen().unwrap();
|
screen.print_screen();
|
||||||
bus! {crate::bus::Region::Screen [0..256] = test.screen}
|
Screen::from(test.screen).print_screen();
|
||||||
.print_screen()
|
assert_eq!(screen.grab(..).unwrap(), test.screen);
|
||||||
.unwrap();
|
|
||||||
assert_eq!(bus.get_region(Screen).unwrap(), test.screen);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -49,6 +45,7 @@ fn splash_screen() {
|
|||||||
run_screentest(
|
run_screentest(
|
||||||
SuiteTest {
|
SuiteTest {
|
||||||
test: 0,
|
test: 0,
|
||||||
|
data: include_bytes!("../chip8-test-suite/bin/1-chip8-logo.ch8"),
|
||||||
screen: include_bytes!("screens/chip8-test-suite/splash.bin"),
|
screen: include_bytes!("screens/chip8-test-suite/splash.bin"),
|
||||||
},
|
},
|
||||||
cpu,
|
cpu,
|
||||||
@ -61,7 +58,8 @@ fn ibm_logo() {
|
|||||||
let (cpu, bus) = setup_environment();
|
let (cpu, bus) = setup_environment();
|
||||||
run_screentest(
|
run_screentest(
|
||||||
SuiteTest {
|
SuiteTest {
|
||||||
test: 0x01,
|
test: 0x00,
|
||||||
|
data: include_bytes!("../chip8-test-suite/bin/2-ibm-logo.ch8"),
|
||||||
screen: include_bytes!("screens/chip8-test-suite/IBM.bin"),
|
screen: include_bytes!("screens/chip8-test-suite/IBM.bin"),
|
||||||
},
|
},
|
||||||
cpu,
|
cpu,
|
||||||
@ -69,12 +67,27 @@ fn ibm_logo() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn corax_test() {
|
||||||
|
let (cpu, bus) = setup_environment();
|
||||||
|
run_screentest(
|
||||||
|
SuiteTest {
|
||||||
|
test: 0x00,
|
||||||
|
data: include_bytes!("../chip8-test-suite/bin/3-corax+.ch8"),
|
||||||
|
screen: include_bytes!("screens/chip8-test-suite/corax+.bin"),
|
||||||
|
},
|
||||||
|
cpu,
|
||||||
|
bus,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn flags_test() {
|
fn flags_test() {
|
||||||
let (cpu, bus) = setup_environment();
|
let (cpu, bus) = setup_environment();
|
||||||
run_screentest(
|
run_screentest(
|
||||||
SuiteTest {
|
SuiteTest {
|
||||||
test: 0x03,
|
test: 0x00,
|
||||||
|
data: include_bytes!("../chip8-test-suite/bin/4-flags.ch8"),
|
||||||
screen: include_bytes!("screens/chip8-test-suite/flags.bin"),
|
screen: include_bytes!("screens/chip8-test-suite/flags.bin"),
|
||||||
},
|
},
|
||||||
cpu,
|
cpu,
|
||||||
@ -87,7 +100,8 @@ fn quirks_test() {
|
|||||||
let (cpu, bus) = setup_environment();
|
let (cpu, bus) = setup_environment();
|
||||||
run_screentest(
|
run_screentest(
|
||||||
SuiteTest {
|
SuiteTest {
|
||||||
test: 0x0104,
|
test: 0x01,
|
||||||
|
data: include_bytes!("../chip8-test-suite/bin/5-quirks.ch8"),
|
||||||
screen: include_bytes!("screens/chip8-test-suite/quirks.bin"),
|
screen: include_bytes!("screens/chip8-test-suite/quirks.bin"),
|
||||||
},
|
},
|
||||||
cpu,
|
cpu,
|
||||||
|
@ -1,56 +1,50 @@
|
|||||||
//! Testing methods on Chirp's public API
|
//! Testing methods on Chirp's public API
|
||||||
|
use chirp::cpu::mem::Region::*;
|
||||||
use chirp::*;
|
use chirp::*;
|
||||||
use std::{collections::hash_map::DefaultHasher, hash::Hash};
|
use std::{collections::hash_map::DefaultHasher, hash::Hash};
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn chip8() {
|
|
||||||
let ch8 = Chip8::default(); // Default
|
|
||||||
let ch82 = ch8.clone(); // Clone
|
|
||||||
assert_eq!(ch8, ch82); // PartialEq
|
|
||||||
println!("{ch8:?}"); // Debug
|
|
||||||
}
|
|
||||||
|
|
||||||
mod bus {
|
mod bus {
|
||||||
use super::*;
|
use super::*;
|
||||||
mod region {
|
mod region {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
// #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
// #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
#[test]
|
#[test]
|
||||||
fn copy() {
|
fn copy() {
|
||||||
let r1 = Screen;
|
let r1 = Charset;
|
||||||
let r2 = r1;
|
let r2 = r1;
|
||||||
assert_eq!(r1, r2);
|
assert_eq!(r1, r2);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(clippy::clone_on_copy)]
|
#[allow(clippy::clone_on_copy)]
|
||||||
fn clone() {
|
fn clone() {
|
||||||
let r1 = Screen;
|
let r1 = Charset;
|
||||||
let r2 = r1.clone();
|
let r2 = r1.clone();
|
||||||
assert_eq!(r1, r2);
|
assert_eq!(r1, r2);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn display() {
|
fn display() {
|
||||||
println!("{Charset}{Program}{Screen}{Stack}{Count}");
|
println!("{Charset}{Program}{Count}");
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn debug() {
|
fn debug() {
|
||||||
println!("{Charset:?}{Program:?}{Screen:?}{Stack:?}{Count:?}");
|
println!("{Charset:?}{Program:?}{Count:?}");
|
||||||
}
|
}
|
||||||
// lmao the things you do for test coverage
|
// lmao the things you do for test coverage
|
||||||
#[test]
|
#[test]
|
||||||
fn eq() {
|
fn eq() {
|
||||||
assert_eq!(Screen, Screen);
|
assert_eq!(Charset, Charset);
|
||||||
assert_ne!(Charset, Program);
|
assert_ne!(Charset, Program);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn ord() {
|
fn ord() {
|
||||||
assert_eq!(Stack, Charset.max(Program).max(Screen).max(Stack));
|
assert_eq!(Program, Charset.max(Program));
|
||||||
assert!(Charset < Program && Program < Screen && Screen < Stack);
|
assert!(Charset < Program);
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn hash() {
|
fn hash() {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
Stack.hash(&mut hasher);
|
Program.hash(&mut hasher);
|
||||||
println!("{hasher:?}");
|
println!("{hasher:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -58,7 +52,7 @@ mod bus {
|
|||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn bus_missing_region() {
|
fn bus_missing_region() {
|
||||||
// Print the screen of a bus with no screen
|
// Print the screen of a bus with no screen
|
||||||
bus! {}.print_screen().unwrap()
|
mem! {}.get_region(Charset).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,14 +119,13 @@ mod cpu {
|
|||||||
use super::*;
|
use super::*;
|
||||||
//#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
//#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(clippy::redundant_clone)]
|
||||||
fn clone() {
|
fn clone() {
|
||||||
let cf1 = Flags {
|
let cf1 = Flags {
|
||||||
debug: false,
|
debug: false,
|
||||||
pause: false,
|
pause: false,
|
||||||
keypause: false,
|
keypause: false,
|
||||||
draw_wait: false,
|
draw_wait: false,
|
||||||
lastkey: None,
|
|
||||||
monotonic: None,
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let cf2 = cf1.clone();
|
let cf2 = cf1.clone();
|
||||||
@ -190,7 +183,7 @@ mod cpu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod dis {
|
mod dis {
|
||||||
use chirp::cpu::disassembler::Insn;
|
use chirp::cpu::instruction::Insn;
|
||||||
use imperative_rs::InstructionSet;
|
use imperative_rs::InstructionSet;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -208,7 +201,7 @@ mod dis {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error() {
|
fn error() {
|
||||||
let error = chirp::error::Error::MissingRegion { region: Screen };
|
let error = chirp::error::Error::InvalidAddressRange { range: (..).into() };
|
||||||
// Print it with Display and Debug
|
// Print it with Display and Debug
|
||||||
println!("{error} {error:?}");
|
println!("{error} {error:?}");
|
||||||
}
|
}
|
||||||
@ -226,6 +219,7 @@ mod quirks {
|
|||||||
bin_ops: true,
|
bin_ops: true,
|
||||||
shift: true,
|
shift: true,
|
||||||
draw_wait: true,
|
draw_wait: true,
|
||||||
|
screen_wrap: false,
|
||||||
dma_inc: true,
|
dma_inc: true,
|
||||||
stupid_jumps: true,
|
stupid_jumps: true,
|
||||||
}
|
}
|
||||||
@ -241,6 +235,7 @@ mod quirks {
|
|||||||
bin_ops: false,
|
bin_ops: false,
|
||||||
shift: false,
|
shift: false,
|
||||||
draw_wait: false,
|
draw_wait: false,
|
||||||
|
screen_wrap: false,
|
||||||
dma_inc: false,
|
dma_inc: false,
|
||||||
stupid_jumps: false,
|
stupid_jumps: false,
|
||||||
}
|
}
|
||||||
@ -253,9 +248,11 @@ mod quirks {
|
|||||||
bin_ops: false,
|
bin_ops: false,
|
||||||
shift: true,
|
shift: true,
|
||||||
draw_wait: false,
|
draw_wait: false,
|
||||||
|
screen_wrap: false,
|
||||||
dma_inc: true,
|
dma_inc: true,
|
||||||
stupid_jumps: false,
|
stupid_jumps: false,
|
||||||
};
|
};
|
||||||
|
#[allow(clippy::clone_on_copy)]
|
||||||
let q2 = q1.clone();
|
let q2 = q1.clone();
|
||||||
assert_eq!(q1, q2);
|
assert_eq!(q1, q2);
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
BIN
tests/screens/chip8-test-suite/corax+.bin
Normal file
BIN
tests/screens/chip8-test-suite/corax+.bin
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user