Compare commits
No commits in common. "mem-refactor" and "main" have entirely different histories.
mem-refact
...
main
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,7 +1,5 @@
|
||||
|
||||
# Rust files
|
||||
**/target
|
||||
**/Cargo.lock
|
||||
/target
|
||||
Cargo.lock
|
||||
flamegraph.svg
|
||||
|
54
Cargo.toml
54
Cargo.toml
@ -1,21 +1,22 @@
|
||||
[workspace]
|
||||
members = ["chirp-imgui", "chirp-minifb"]
|
||||
# default-members = ["chirp-imgui"]
|
||||
|
||||
[workspace.package]
|
||||
[package]
|
||||
name = "chirp"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
ignore = ["justfile", ".gitmodules", "chip8-test-suite", "chip8Archive"]
|
||||
default-run = "chirp"
|
||||
authors = ["John Breaux"]
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[package]
|
||||
name = "chirp"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["unstable", "drawille", "minifb"]
|
||||
unstable = []
|
||||
drawille = ["dep:drawille"]
|
||||
iced = ["dep:iced"]
|
||||
minifb = ["dep:minifb"]
|
||||
rhexdump = ["dep:rhexdump"]
|
||||
serde = ["dep:serde"]
|
||||
|
||||
[[bin]]
|
||||
name = "chirp"
|
||||
@ -26,17 +27,14 @@ required-features = ["minifb"]
|
||||
name = "chirp-disasm"
|
||||
required-features = ["default"]
|
||||
|
||||
[[bin]]
|
||||
name = "chirp-iced"
|
||||
required-features = ["iced"]
|
||||
|
||||
[[bin]]
|
||||
name = "chirp-shot-viewer"
|
||||
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
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
@ -51,14 +49,14 @@ overflow-checks = false
|
||||
|
||||
|
||||
[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 }
|
||||
|
||||
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"
|
||||
gumdrop = "^0.8.1"
|
||||
imperative-rs = "0.3.1"
|
||||
owo-colors = "3"
|
||||
rand = "0.8.5"
|
||||
thiserror = "1.0.39"
|
||||
owo-colors = "^3"
|
||||
rand = "^0.8.5"
|
||||
thiserror = "^1.0.39"
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 253f5de130a6cc4c7e07c6801174717efb0ea9cb
|
||||
Subproject commit af7829841b4c9b043febbd37a4d4114e0ac948ec
|
@ -1 +1 @@
|
||||
Subproject commit 6e1f4025df7c46ef5bdc222262b1e38208cf7c0f
|
||||
Subproject commit 483a642595b025a863f2e4304384a370098a3344
|
@ -1,23 +0,0 @@
|
||||
[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" }
|
@ -1,52 +0,0 @@
|
||||
//! 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
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
//! 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");
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
//! 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),
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
//! 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!(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
//! 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"));
|
||||
});
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
//! 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))
|
||||
}
|
||||
}
|
@ -1,205 +0,0 @@
|
||||
#![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;
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
[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"
|
@ -1,18 +0,0 @@
|
||||
//! 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),
|
||||
}
|
5
justfile
5
justfile
@ -1,6 +1,3 @@
|
||||
# (c) 2023 John A. Breaux
|
||||
# This code is licensed under MIT license (see LICENSE for details)
|
||||
|
||||
# Some common commands for working on this stuff
|
||||
|
||||
# Run All Tests
|
||||
@ -17,7 +14,7 @@ debug rom:
|
||||
cargo run -- -d '{{rom}}'
|
||||
# Run at 2100000 instructions per frame, and output per-frame runtime statistics
|
||||
bench:
|
||||
cargo run --release --bin chirp --features="minifb" -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip
|
||||
cargo run --release -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip
|
||||
|
||||
flame rom:
|
||||
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10
|
||||
|
@ -3,48 +3,24 @@ use gumdrop::*;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::{fs::read, path::PathBuf};
|
||||
|
||||
mod tree;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut options = Arguments::parse_args_default_or_exit();
|
||||
while let Some(file) = options.file.pop() {
|
||||
println!("{file:?}");
|
||||
let contents = &read(&file)?;
|
||||
if options.tree || options.traverse {
|
||||
let loadaddr = options.loadaddr as usize;
|
||||
let mem = mem! {
|
||||
cpu::mem::Region::Program [loadaddr..loadaddr + contents.len()] = contents
|
||||
};
|
||||
let mut nodes = Default::default();
|
||||
let tree = tree::DisNode::traverse(
|
||||
mem.grab(..).expect("grabbing [..] should never fail"),
|
||||
&mut nodes,
|
||||
options.loadaddr as usize + options.offset,
|
||||
);
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
let options = Arguments::parse_args_default_or_exit();
|
||||
let contents = &read(&options.file)?;
|
||||
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!(
|
||||
"{}",
|
||||
format_args!(
|
||||
"{:03x}: {} {:04x}",
|
||||
2 * addr + 0x200 + options.offset,
|
||||
disassembler.once(insn),
|
||||
insn.bright_black(),
|
||||
)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -54,19 +30,11 @@ struct Arguments {
|
||||
#[options(help = "Show help text")]
|
||||
help: bool,
|
||||
#[options(help = "Load a ROM to run on Chirp", free, required)]
|
||||
pub file: Vec<PathBuf>,
|
||||
#[options(
|
||||
help = "Load address (usually 200)",
|
||||
parse(try_from_str = "parse_hex"),
|
||||
default = "200"
|
||||
)]
|
||||
pub file: PathBuf,
|
||||
#[options(help = "Load address (usually 200)", parse(try_from_str = "parse_hex"))]
|
||||
pub loadaddr: u16,
|
||||
#[options(help = "Start disassembling at offset...")]
|
||||
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> {
|
||||
|
@ -1,183 +0,0 @@
|
||||
#![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),
|
||||
}
|
||||
}
|
||||
}
|
26
src/bin/chirp-iced/main.rs
Normal file
26
src/bin/chirp-iced/main.rs
Normal file
@ -0,0 +1,26 @@
|
||||
#![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,
|
||||
}
|
@ -1,20 +1,18 @@
|
||||
// (c) 2023 John A. Breaux
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
||||
|
||||
//! Chirp: A chip-8 interpreter in Rust
|
||||
//! Hello, world!
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod error;
|
||||
mod ui;
|
||||
|
||||
use chirp::error::Error::BreakpointHit;
|
||||
use chirp::*;
|
||||
use error::Result;
|
||||
use chirp::{error::Result, *};
|
||||
use gumdrop::*;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::fs::read;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
@ -49,7 +47,7 @@ struct Arguments {
|
||||
#[options(help = "Enable pause mode at startup.")]
|
||||
pub pause: bool,
|
||||
|
||||
#[options(help = "Set the instructions-per-delay rate. If unspecified, use realtime.")]
|
||||
#[options(help = "Set the instructions-per-delay rate, or use realtime.")]
|
||||
pub speed: Option<usize>,
|
||||
#[options(help = "Set the instructions-per-frame rate.")]
|
||||
pub step: Option<usize>,
|
||||
@ -78,19 +76,16 @@ struct Arguments {
|
||||
help = "Use CHIP-48 style DMA instructions, which don't touch I."
|
||||
)]
|
||||
pub memory: bool,
|
||||
|
||||
#[options(
|
||||
short = "v",
|
||||
help = "Use CHIP-48 style bit-shifts, which don't touch vY."
|
||||
)]
|
||||
pub shift: bool,
|
||||
|
||||
#[options(
|
||||
short = "b",
|
||||
help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]."
|
||||
)]
|
||||
pub jumping: bool,
|
||||
|
||||
#[options(
|
||||
long = "break",
|
||||
help = "Set breakpoints for the emulator to stop at.",
|
||||
@ -98,23 +93,16 @@ struct Arguments {
|
||||
meta = "BP"
|
||||
)]
|
||||
pub breakpoints: Vec<u16>,
|
||||
|
||||
#[options(
|
||||
help = "Load additional word at address 0x1fe",
|
||||
parse(try_from_str = "parse_hex"),
|
||||
meta = "WORD"
|
||||
)]
|
||||
pub data: u16,
|
||||
|
||||
#[options(help = "Set the target framerate.", default = "60", meta = "FR")]
|
||||
pub frame_rate: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Chip8 {
|
||||
pub cpu: CPU,
|
||||
pub screen: Screen,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
pub speed: usize,
|
||||
@ -134,20 +122,31 @@ impl State {
|
||||
rate: options.frame_rate,
|
||||
perf: options.perf,
|
||||
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(
|
||||
Some(&options.file),
|
||||
0x1000,
|
||||
0x50,
|
||||
0x200,
|
||||
0xefe,
|
||||
Dis::default(),
|
||||
options.breakpoints,
|
||||
Flags {
|
||||
quirks: options.mode.unwrap_or_default().into(),
|
||||
debug: options.debug,
|
||||
pause: options.pause,
|
||||
monotonic: options.speed,
|
||||
..Default::default()
|
||||
},
|
||||
)?,
|
||||
screen: Screen::default(),
|
||||
),
|
||||
},
|
||||
ui: UIBuilder::new(128, 64, &options.file).build()?,
|
||||
ft: Instant::now(),
|
||||
@ -158,14 +157,14 @@ impl State {
|
||||
state.ch8.cpu.flags.quirks.draw_wait ^= options.drawsync;
|
||||
state.ch8.cpu.flags.quirks.shift ^= options.shift;
|
||||
state.ch8.cpu.flags.quirks.stupid_jumps ^= options.jumping;
|
||||
state.ch8.screen.write(0x1feu16, options.data);
|
||||
state.ch8.bus.write(0x1feu16, options.data);
|
||||
Ok(state)
|
||||
}
|
||||
fn keys(&mut self) -> Result<bool> {
|
||||
self.ui.keys(&mut self.ch8)
|
||||
}
|
||||
fn frame(&mut self) -> Result<bool> {
|
||||
self.ui.frame(&self.ch8)
|
||||
self.ui.frame(&mut self.ch8)
|
||||
}
|
||||
fn tick_cpu(&mut self) -> Result<()> {
|
||||
if !self.ch8.cpu.flags.pause {
|
||||
@ -173,7 +172,7 @@ impl State {
|
||||
match self.step {
|
||||
Some(ticks) => {
|
||||
let time = Instant::now();
|
||||
self.ch8.cpu.multistep(&mut self.ch8.screen, ticks)?;
|
||||
self.ch8.cpu.multistep(&mut self.ch8.bus, ticks)?;
|
||||
if self.perf {
|
||||
let time = time.elapsed();
|
||||
let nspt = time.as_secs_f64() / ticks as f64;
|
||||
@ -186,16 +185,16 @@ impl State {
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.ch8.cpu.multistep(&mut self.ch8.screen, rate)?;
|
||||
self.ch8.cpu.multistep(&mut self.ch8.bus, rate)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn wait_for_next_frame(&mut self) {
|
||||
let rate = Duration::from_nanos(1_000_000_000 / self.rate + 1);
|
||||
std::thread::sleep(rate.saturating_sub(self.ft.elapsed()));
|
||||
self.ft += rate;
|
||||
let rate = 1_000_000_000 / self.rate + 1;
|
||||
std::thread::sleep(Duration::from_nanos(rate).saturating_sub(self.ft.elapsed()));
|
||||
self.ft = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,7 +211,7 @@ impl Iterator for State {
|
||||
}
|
||||
// Allow breakpoint hit messages
|
||||
match self.tick_cpu() {
|
||||
Err(error::Error::Chirp(BreakpointHit { addr, next })) => {
|
||||
Err(BreakpointHit { addr, next }) => {
|
||||
eprintln!("Breakpoint hit: {:3x} ({:4x})", addr, next);
|
||||
}
|
||||
Err(e) => return Some(Err(e)),
|
@ -1,9 +1,6 @@
|
||||
//! Tests for chirp-minifb
|
||||
#![allow(clippy::redundant_clone)]
|
||||
|
||||
use super::ui::*;
|
||||
use super::Chip8;
|
||||
use crate::error::Result;
|
||||
use chirp::*;
|
||||
use std::{collections::hash_map::DefaultHasher, hash::Hash};
|
||||
|
||||
@ -32,14 +29,14 @@ mod ui {
|
||||
fn new_chip8() -> Chip8 {
|
||||
Chip8 {
|
||||
cpu: CPU::default(),
|
||||
screen: Screen::default(),
|
||||
bus: bus! {},
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn frame() -> Result<()> {
|
||||
let mut ui = UIBuilder::new(32, 64, "dummy.ch8").build()?;
|
||||
let ch8 = new_chip8();
|
||||
ui.frame(&ch8).unwrap();
|
||||
let mut ch8 = new_chip8();
|
||||
ui.frame(&mut ch8).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
#[test]
|
@ -1,18 +1,22 @@
|
||||
// (c) 2023 John A. Breaux
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
||||
#![allow(missing_docs)]
|
||||
//! Platform-specific IO/UI code, and some debug functionality.
|
||||
//! TODO: Destroy this all.
|
||||
|
||||
use super::Chip8;
|
||||
use crate::error::Result;
|
||||
use chirp::screen::Screen;
|
||||
use minifb::*;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use chirp::{
|
||||
bus::{Bus, Region},
|
||||
error::Result,
|
||||
Chip8,
|
||||
};
|
||||
use minifb::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UIBuilder {
|
||||
pub width: usize,
|
||||
@ -77,10 +81,8 @@ pub struct FrameBufferFormat {
|
||||
impl Default for FrameBufferFormat {
|
||||
fn default() -> Self {
|
||||
FrameBufferFormat {
|
||||
// fg: 0x0011a434,
|
||||
// bg: 0x001E2431,
|
||||
fg: 0x00FFFF00,
|
||||
bg: 0x00623701,
|
||||
fg: 0x0011a434,
|
||||
bg: 0x001E2431,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,25 +104,27 @@ impl FrameBuffer {
|
||||
format: Default::default(),
|
||||
}
|
||||
}
|
||||
pub fn render(&mut self, window: &mut Window, screen: &Screen) -> Result<()> {
|
||||
// Resizing the buffer does not unmap memory.
|
||||
// After the first use of high-res mode, this is pretty cheap
|
||||
(self.width, self.height) = match screen.len() {
|
||||
256 => (64, 32),
|
||||
1024 => (128, 64),
|
||||
_ => {
|
||||
unimplemented!("Screen must be 64*32 or 128*64");
|
||||
}
|
||||
};
|
||||
self.buffer.resize(self.width * self.height, 0);
|
||||
for (idx, byte) in screen.as_slice().iter().enumerate() {
|
||||
for bit in 0..8 {
|
||||
self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 {
|
||||
self.format.fg
|
||||
} else {
|
||||
self.format.bg
|
||||
// .wrapping_add(0x001104 * (idx / self.width) as u32)
|
||||
// .wrapping_add(0x141000 * (idx & 3) as u32)
|
||||
pub fn render(&mut self, window: &mut Window, bus: &Bus) -> Result<()> {
|
||||
if let Some(screen) = bus.get_region(Region::Screen) {
|
||||
// Resizing the buffer does not unmap memory.
|
||||
// After the first use of high-res mode, this is pretty cheap
|
||||
(self.width, self.height) = match screen.len() {
|
||||
256 => (64, 32),
|
||||
1024 => (128, 64),
|
||||
_ => {
|
||||
unimplemented!("Screen must be 64*32 or 128*64");
|
||||
}
|
||||
};
|
||||
self.buffer.resize(self.width * self.height, 0);
|
||||
for (idx, byte) in screen.iter().enumerate() {
|
||||
for bit in 0..8 {
|
||||
self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 {
|
||||
self.format.fg
|
||||
} else {
|
||||
self.format.bg
|
||||
// .wrapping_add(0x001104 * (idx / self.width) as u32)
|
||||
// .wrapping_add(0x141000 * (idx & 3) as u32)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -145,7 +149,7 @@ pub struct UI {
|
||||
}
|
||||
|
||||
impl UI {
|
||||
pub fn frame(&mut self, ch8: &Chip8) -> Result<bool> {
|
||||
pub fn frame(&mut self, ch8: &mut Chip8) -> Result<bool> {
|
||||
if ch8.cpu.flags.pause {
|
||||
self.window.set_title("Chirp ⏸")
|
||||
} else {
|
||||
@ -159,7 +163,7 @@ impl UI {
|
||||
}
|
||||
self.time = Instant::now();
|
||||
// update framebuffer
|
||||
self.fb.render(&mut self.window, &ch8.screen)?;
|
||||
self.fb.render(&mut self.window, &ch8.bus)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@ -177,6 +181,7 @@ impl UI {
|
||||
.into_iter()
|
||||
.filter(|key| !self.window.get_keys().contains(key))
|
||||
};
|
||||
use crate::ui::Region::*;
|
||||
for key in get_keys_released() {
|
||||
if let Some(key) = identify_key(key) {
|
||||
ch8.cpu.release(key)?;
|
||||
@ -187,7 +192,7 @@ impl UI {
|
||||
use Key::*;
|
||||
match key {
|
||||
F1 | Comma => ch8.cpu.dump(),
|
||||
F2 | Period => ch8.screen.print_screen(),
|
||||
F2 | Period => ch8.bus.print_screen()?,
|
||||
F3 => {
|
||||
debug_dump_screen(ch8, &self.rom).expect("Unable to write debug screen dump");
|
||||
}
|
||||
@ -211,7 +216,7 @@ impl UI {
|
||||
}),
|
||||
F6 | Enter => {
|
||||
eprintln!("Step");
|
||||
ch8.cpu.singlestep(&mut ch8.screen)?;
|
||||
ch8.cpu.singlestep(&mut ch8.bus)?;
|
||||
}
|
||||
F7 => {
|
||||
eprintln!("Set breakpoint {:03x}.", ch8.cpu.pc());
|
||||
@ -224,7 +229,7 @@ impl UI {
|
||||
F9 | Delete => {
|
||||
eprintln!("Soft reset state.cpu {:03x}", ch8.cpu.pc());
|
||||
ch8.cpu.soft_reset();
|
||||
ch8.screen.clear();
|
||||
ch8.bus.clear_region(Screen);
|
||||
}
|
||||
Escape => return Ok(false),
|
||||
key => {
|
||||
@ -262,18 +267,29 @@ pub fn identify_key(key: Key) -> Option<usize> {
|
||||
}
|
||||
|
||||
pub fn debug_dump_screen(ch8: &Chip8, rom: &Path) -> Result<()> {
|
||||
let mut path = PathBuf::new().join(format!(
|
||||
"{}_{}.bin",
|
||||
rom.file_stem().unwrap_or_default().to_string_lossy(),
|
||||
ch8.cpu.cycle()
|
||||
));
|
||||
path.set_extension("bin");
|
||||
if std::fs::write(&path, ch8.screen.as_slice()).is_ok() {
|
||||
eprintln!("Saved to {}", &path.display());
|
||||
} else if std::fs::write("screen_dump.bin", ch8.screen.as_slice()).is_ok() {
|
||||
eprintln!("Saved to screen_dump.bin");
|
||||
} else {
|
||||
eprintln!("Failed to dump screen to file.")
|
||||
}
|
||||
let path = PathBuf::new()
|
||||
.join("src/cpu/tests/screens/")
|
||||
.join(if rom.is_absolute() {
|
||||
Path::new("unknown/")
|
||||
} else {
|
||||
rom.file_name().unwrap_or(OsStr::new("unknown")).as_ref()
|
||||
})
|
||||
.join(format!("{}.bin", ch8.cpu.cycle()));
|
||||
std::fs::write(
|
||||
&path,
|
||||
ch8.bus
|
||||
.get_region(Region::Screen)
|
||||
.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(())
|
||||
}
|
@ -4,7 +4,7 @@ use std::{env::args, fs::read};
|
||||
fn main() -> Result<()> {
|
||||
for screen in args().skip(1).inspect(|screen| println!("{screen}")) {
|
||||
let screen = read(screen)?;
|
||||
Screen::from(screen).print_screen();
|
||||
bus! {Screen [0..screen.len()] = &screen}.print_screen()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
497
src/bus.rs
Normal file
497
src/bus.rs
Normal file
@ -0,0 +1,497 @@
|
||||
// (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,141 +1,114 @@
|
||||
// (c) 2023 John A. Breaux
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
||||
|
||||
//! Decodes and runs instructions
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod behavior;
|
||||
pub mod disassembler;
|
||||
pub mod flags;
|
||||
pub mod instruction;
|
||||
#[macro_use]
|
||||
pub mod mem;
|
||||
pub mod mode;
|
||||
pub mod quirks;
|
||||
|
||||
use self::{
|
||||
disassembler::{Dis, Disassembler, Insn},
|
||||
flags::Flags,
|
||||
instruction::{
|
||||
disassembler::{Dis, Disassembler},
|
||||
Insn,
|
||||
},
|
||||
mem::{Mem, Region::*},
|
||||
mode::Mode,
|
||||
quirks::Quirks,
|
||||
};
|
||||
use crate::{
|
||||
bus::{Bus, Read, Region, Write},
|
||||
error::{Error, Result},
|
||||
screen::Screen,
|
||||
traits::{AutoCast, Grab},
|
||||
};
|
||||
use imperative_rs::InstructionSet;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::fmt::Debug;
|
||||
use rand::random;
|
||||
use std::time::Instant;
|
||||
|
||||
type Reg = usize;
|
||||
type Adr = u16;
|
||||
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
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CPU {
|
||||
/// Flags that control how the CPU behaves, but which aren't inherent to the
|
||||
/// chip-8. Includes [Quirks], target IPF, etc.
|
||||
pub flags: Flags,
|
||||
// memory map info
|
||||
mem: Mem,
|
||||
screen: Adr,
|
||||
font: Adr,
|
||||
// memory
|
||||
stack: Vec<Adr>,
|
||||
// registers
|
||||
pc: Adr,
|
||||
sp: Adr,
|
||||
i: Adr,
|
||||
v: [u8; 16],
|
||||
delay: u8,
|
||||
sound: u8,
|
||||
delay: f64,
|
||||
sound: f64,
|
||||
// I/O
|
||||
keys: [bool; 16],
|
||||
/// Set to the last key that's been *released* after a keypause
|
||||
pub lastkey: Option<usize>,
|
||||
// Execution data
|
||||
timers: Timers,
|
||||
cycle: usize,
|
||||
breakpoints: Vec<Adr>,
|
||||
#[cfg_attr(feature = "serde", serde(skip))]
|
||||
disassembler: Dis,
|
||||
}
|
||||
|
||||
// public interface
|
||||
impl CPU {
|
||||
// TODO: implement From<&bus> for CPU
|
||||
/// Constructs a new CPU, taking all configurable parameters
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use chirp::*;
|
||||
/// let cpu = CPU::new(
|
||||
/// None::<&str>, // ROM to load
|
||||
/// 0x50, // font location
|
||||
/// 0x200, // start of program
|
||||
/// 0xf00, // screen location
|
||||
/// 0x50, // font location
|
||||
/// 0x200, // start of program
|
||||
/// 0xefe, // top of stack
|
||||
/// Dis::default(),
|
||||
/// vec![], // Breakpoints
|
||||
/// Flags::default()
|
||||
/// vec![], // Breakpoints
|
||||
/// ControlFlags::default()
|
||||
/// );
|
||||
/// dbg!(cpu);
|
||||
/// ```
|
||||
pub fn new(
|
||||
rom: Option<impl AsRef<std::path::Path>>,
|
||||
screen: Adr,
|
||||
font: Adr,
|
||||
pc: Adr,
|
||||
sp: Adr,
|
||||
disassembler: Dis,
|
||||
breakpoints: Vec<Adr>,
|
||||
flags: Flags,
|
||||
) -> Result<Self> {
|
||||
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 {
|
||||
) -> Self {
|
||||
CPU {
|
||||
disassembler,
|
||||
screen,
|
||||
font,
|
||||
pc,
|
||||
sp,
|
||||
breakpoints,
|
||||
flags,
|
||||
mem,
|
||||
..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.
|
||||
@ -170,8 +143,8 @@ impl CPU {
|
||||
/// Releases a key, and reports whether the key's state changed.
|
||||
/// If key is outside range `0..=0xF`, returns [Error::InvalidKey].
|
||||
///
|
||||
/// If [Flags::keypause] was enabled, it is disabled,
|
||||
/// and the lastkey is recorded.
|
||||
/// If [ControlFlags::keypause] was enabled, it is disabled,
|
||||
/// and the [ControlFlags::lastkey] is recorded.
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use chirp::*;
|
||||
@ -190,7 +163,7 @@ impl CPU {
|
||||
if *keyref {
|
||||
*keyref = false;
|
||||
if self.flags.keypause {
|
||||
self.lastkey = Some(key);
|
||||
self.flags.lastkey = Some(key);
|
||||
self.flags.keypause = false;
|
||||
}
|
||||
return Ok(true);
|
||||
@ -267,7 +240,7 @@ impl CPU {
|
||||
/// assert_eq!(0, cpu.sound());
|
||||
/// ```
|
||||
pub fn sound(&self) -> u8 {
|
||||
self.sound
|
||||
self.sound as u8
|
||||
}
|
||||
|
||||
/// Gets the value in the Delay Timer register
|
||||
@ -278,7 +251,7 @@ impl CPU {
|
||||
/// assert_eq!(0, cpu.delay());
|
||||
/// ```
|
||||
pub fn delay(&self) -> u8 {
|
||||
self.delay
|
||||
self.delay as u8
|
||||
}
|
||||
|
||||
/// Gets the number of cycles the CPU has executed
|
||||
@ -301,13 +274,14 @@ impl CPU {
|
||||
/// ```rust
|
||||
/// # use chirp::*;
|
||||
/// let mut cpu = CPU::new(
|
||||
/// None::<&str>,
|
||||
/// 0xf00,
|
||||
/// 0x50,
|
||||
/// 0x340,
|
||||
/// 0xefe,
|
||||
/// Dis::default(),
|
||||
/// vec![],
|
||||
/// Flags::default()
|
||||
/// ).unwrap();
|
||||
/// ControlFlags::default()
|
||||
/// );
|
||||
/// cpu.flags.keypause = true;
|
||||
/// cpu.flags.draw_wait = true;
|
||||
/// assert_eq!(0x340, cpu.pc());
|
||||
@ -322,35 +296,6 @@ impl CPU {
|
||||
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
|
||||
// TODO: Unit test this
|
||||
pub fn set_break(&mut self, point: Adr) -> &mut Self {
|
||||
@ -399,13 +344,18 @@ impl CPU {
|
||||
/// ```rust
|
||||
/// # use chirp::*;
|
||||
/// let mut cpu = CPU::default();
|
||||
/// let mut screen = Screen::default();
|
||||
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
|
||||
/// cpu.singlestep(&mut screen).unwrap();
|
||||
/// let mut bus = bus!{
|
||||
/// Program [0x0200..0x0f00] = &[
|
||||
/// 0x00, 0xe0, // cls
|
||||
/// 0x22, 0x02, // jump 0x202 (pc)
|
||||
/// ],
|
||||
/// Screen [0x0f00..0x1000],
|
||||
/// };
|
||||
/// cpu.singlestep(&mut bus).unwrap();
|
||||
/// assert_eq!(0x202, cpu.pc());
|
||||
/// assert_eq!(1, cpu.cycle());
|
||||
/// ```
|
||||
pub fn singlestep(&mut self, bus: &mut Screen) -> Result<&mut Self> {
|
||||
pub fn singlestep(&mut self, bus: &mut Bus) -> Result<&mut Self> {
|
||||
self.flags.pause = false;
|
||||
self.tick(bus)?;
|
||||
self.flags.draw_wait = false;
|
||||
@ -420,96 +370,147 @@ impl CPU {
|
||||
/// ```rust
|
||||
/// # use chirp::*;
|
||||
/// let mut cpu = CPU::default();
|
||||
/// let mut screen = Screen::default();
|
||||
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
|
||||
/// cpu.multistep(&mut screen, 0x20)
|
||||
/// let mut bus = bus!{
|
||||
/// Program [0x0200..0x0f00] = &[
|
||||
/// 0x00, 0xe0, // cls
|
||||
/// 0x22, 0x02, // jump 0x202 (pc)
|
||||
/// ],
|
||||
/// Screen [0x0f00..0x1000],
|
||||
/// };
|
||||
/// cpu.multistep(&mut bus, 0x20)
|
||||
/// .expect("The program should only have valid opcodes.");
|
||||
/// assert_eq!(0x202, cpu.pc());
|
||||
/// assert_eq!(0x20, cpu.cycle());
|
||||
/// ```
|
||||
pub fn multistep(&mut self, screen: &mut Screen, steps: usize) -> Result<&mut Self> {
|
||||
//let speed = 1.0 / steps as f64;
|
||||
pub fn multistep(&mut self, bus: &mut Bus, steps: usize) -> Result<&mut Self> {
|
||||
for _ in 0..steps {
|
||||
self.tick(screen)?;
|
||||
if self.flags.is_paused() {
|
||||
break;
|
||||
}
|
||||
self.tick(bus)?;
|
||||
self.vertical_blank();
|
||||
}
|
||||
self.delay = self.delay.saturating_sub(1);
|
||||
self.sound = self.sound.saturating_sub(1);
|
||||
self.flags.draw_wait = false;
|
||||
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 -= time;
|
||||
}
|
||||
if self.sound > 0.0 {
|
||||
self.sound -= time;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Executes a single instruction
|
||||
///
|
||||
/// Returns [Error::BreakpointHit] if a breakpoint was hit after the instruction executed.
|
||||
/// This result contains information about the breakpoint, but can be safely ignored.
|
||||
///
|
||||
/// Returns [Error::UnimplementedInstruction] if the instruction at `pc` is unimplemented.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use chirp::*;
|
||||
/// let mut cpu = CPU::default();
|
||||
/// let mut screen = Screen::default();
|
||||
/// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
|
||||
/// cpu.tick(&mut screen)
|
||||
/// let mut bus = bus!{
|
||||
/// Program [0x0200..0x0f00] = &[
|
||||
/// 0x00, 0xe0, // cls
|
||||
/// 0x22, 0x02, // jump 0x202 (pc)
|
||||
/// ],
|
||||
/// Screen [0x0f00..0x1000],
|
||||
/// };
|
||||
/// cpu.tick(&mut bus)
|
||||
/// .expect("0x00e0 (cls) should be a valid opcode.");
|
||||
/// assert_eq!(0x202, cpu.pc());
|
||||
/// assert_eq!(1, cpu.cycle());
|
||||
/// ```
|
||||
|
||||
/// Returns [Error::UnimplementedInstruction] if the instruction is not implemented.
|
||||
/// ```rust
|
||||
/// # use chirp::*;
|
||||
/// # use chirp::error::Error;
|
||||
/// let mut cpu = CPU::default();
|
||||
/// # cpu.flags.debug = true; // enable live disassembly
|
||||
/// # cpu.flags.monotonic = true; // enable monotonic/test timing
|
||||
/// let mut bus = Screen::default();
|
||||
/// cpu.load_program_bytes(&[
|
||||
/// 0xff, 0xff, // invalid!
|
||||
/// 0x22, 0x02, // jump 0x202
|
||||
/// ]);
|
||||
/// # cpu.flags.monotonic = Some(8); // enable monotonic/test timing
|
||||
/// let mut bus = bus!{
|
||||
/// Program [0x0200..0x0f00] = &[
|
||||
/// 0xff, 0xff, // invalid!
|
||||
/// 0x22, 0x02, // jump 0x202 (pc)
|
||||
/// ],
|
||||
/// Screen [0x0f00..0x1000],
|
||||
/// };
|
||||
/// dbg!(cpu.tick(&mut bus))
|
||||
/// .expect_err("Should return Error::InvalidInstruction { 0xffff }");
|
||||
/// ```
|
||||
pub fn tick(&mut self, screen: &mut Screen) -> Result<&mut Self> {
|
||||
pub fn tick(&mut self, bus: &mut Bus) -> Result<&mut Self> {
|
||||
// Do nothing if paused
|
||||
if self.flags.is_paused() {
|
||||
// always tick in test mode
|
||||
if self.flags.monotonic {
|
||||
if self.flags.monotonic.is_some() {
|
||||
self.cycle += 1;
|
||||
}
|
||||
return Ok(self);
|
||||
}
|
||||
self.cycle += 1;
|
||||
// Fetch slice of memory starting at pc, for var-width opcode 0xf000_iiii
|
||||
let opchunk = self
|
||||
.mem
|
||||
.grab(self.pc as usize..)
|
||||
.ok_or(Error::InvalidAddressRange {
|
||||
range: (self.pc as usize..).into(),
|
||||
})?;
|
||||
// fetch opcode
|
||||
let opcode: &[u8; 2] = if let Some(slice) = bus.get(self.pc as usize..self.pc as usize + 2)
|
||||
{
|
||||
slice
|
||||
.try_into()
|
||||
.expect("`slice` should be exactly 2 bytes.")
|
||||
} else {
|
||||
return Err(Error::InvalidBusRange {
|
||||
range: self.pc as usize..self.pc as usize + 2,
|
||||
});
|
||||
};
|
||||
|
||||
// Print opcode disassembly:
|
||||
if self.flags.debug {
|
||||
std::println!(
|
||||
println!("{:?}", self.timers.insn.elapsed().bright_black());
|
||||
self.timers.insn = Instant::now();
|
||||
std::print!(
|
||||
"{:3} {:03x}: {:<36}",
|
||||
self.cycle.bright_black(),
|
||||
self.pc,
|
||||
self.disassembler.once(self.mem.read(self.pc))
|
||||
self.disassembler.once(u16::from_be_bytes(*opcode))
|
||||
);
|
||||
}
|
||||
|
||||
// decode opcode
|
||||
if let Ok((inc, insn)) = Insn::decode(opchunk) {
|
||||
if let Ok((inc, insn)) = Insn::decode(opcode) {
|
||||
self.pc = self.pc.wrapping_add(inc as u16);
|
||||
self.execute(screen, insn);
|
||||
self.execute(bus, insn);
|
||||
} else {
|
||||
return Err(Error::UnimplementedInstruction {
|
||||
word: self.mem.read(self.pc),
|
||||
word: u16::from_be_bytes(*opcode),
|
||||
});
|
||||
}
|
||||
|
||||
@ -518,7 +519,7 @@ impl CPU {
|
||||
self.flags.pause = true;
|
||||
return Err(Error::BreakpointHit {
|
||||
addr: self.pc,
|
||||
next: self.mem.read(self.pc),
|
||||
next: bus.read(self.pc),
|
||||
});
|
||||
}
|
||||
Ok(self)
|
||||
@ -542,52 +543,31 @@ impl CPU {
|
||||
/// ```
|
||||
pub fn dump(&self) {
|
||||
//let dumpstyle = owo_colors::Style::new().bright_black();
|
||||
use std::fmt::Write;
|
||||
std::println!(
|
||||
"PC: {:04x}, SP: {:04x}, I: {:04x}\n{}DLY: {}, SND: {}, CYC: {:6}",
|
||||
self.pc,
|
||||
self.stack.len(),
|
||||
self.sp,
|
||||
self.i,
|
||||
self.v
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.fold(String::new(), |mut s, (i, gpr)| {
|
||||
let _ = write!(
|
||||
s,
|
||||
"{}v{i:X}: {gpr:02x}",
|
||||
.map(|(i, gpr)| {
|
||||
format!(
|
||||
"v{i:X}: {gpr:02x} {}",
|
||||
match i % 4 {
|
||||
0 => "\n",
|
||||
3 => "\n",
|
||||
_ => "",
|
||||
}
|
||||
);
|
||||
s
|
||||
}),
|
||||
self.delay,
|
||||
self.sound,
|
||||
)
|
||||
})
|
||||
.collect::<String>(),
|
||||
self.delay as u8,
|
||||
self.sound as u8,
|
||||
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 {
|
||||
/// Constructs a new CPU with sane defaults and debug mode ON
|
||||
///
|
||||
@ -596,6 +576,8 @@ impl Default for CPU {
|
||||
/// | screen |`0x0f00` | Location of screen memory.
|
||||
/// | font |`0x0050` | Location of font memory.
|
||||
/// | pc |`0x0200` | Start location. Generally 0x200 or 0x600.
|
||||
/// | sp |`0x0efe` | Initial top of stack.
|
||||
///
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
@ -604,25 +586,21 @@ impl Default for CPU {
|
||||
/// ```
|
||||
fn default() -> Self {
|
||||
CPU {
|
||||
stack: vec![],
|
||||
mem: mem! {
|
||||
Charset [0x0050..0x00a0] = include_bytes!("mem/charset.bin"),
|
||||
HiresCharset [0x00a0..0x0140] = include_bytes!("mem/hires.bin"),
|
||||
Program [0x0200..0x1000],
|
||||
},
|
||||
screen: 0xf00,
|
||||
font: 0x050,
|
||||
pc: 0x200,
|
||||
sp: 0xefe,
|
||||
i: 0,
|
||||
v: [0; 16],
|
||||
delay: 0,
|
||||
sound: 0,
|
||||
delay: 0.0,
|
||||
sound: 0.0,
|
||||
cycle: 0,
|
||||
keys: [false; 16],
|
||||
lastkey: None,
|
||||
flags: Flags {
|
||||
debug: true,
|
||||
..Default::default()
|
||||
},
|
||||
timers: Default::default(),
|
||||
breakpoints: vec![],
|
||||
disassembler: Dis::default(),
|
||||
}
|
||||
|
@ -1,750 +0,0 @@
|
||||
// (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;
|
||||
}
|
||||
}
|
||||
}
|
230
src/cpu/disassembler.rs
Normal file
230
src/cpu/disassembler.rs
Normal file
@ -0,0 +1,230 @@
|
||||
//! 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,13 +1,9 @@
|
||||
// (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
|
||||
|
||||
use super::{Mode, Quirks};
|
||||
|
||||
/// 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)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Flags {
|
||||
/// Set when debug (live disassembly) mode enabled
|
||||
pub debug: bool,
|
||||
@ -19,12 +15,14 @@ pub struct Flags {
|
||||
pub draw_wait: bool,
|
||||
/// Set when the emulator is in high-res mode
|
||||
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]
|
||||
pub mode: Mode,
|
||||
/// Represents the set of emulator [Quirks] to enable, independent of the [Mode]
|
||||
pub quirks: Quirks,
|
||||
/// Set when the interpreter should increase the cycle count during a pause
|
||||
pub monotonic: bool,
|
||||
/// Represents the number of instructions to run per tick of the internal timer
|
||||
pub monotonic: Option<usize>,
|
||||
}
|
||||
|
||||
impl Flags {
|
||||
|
@ -1,231 +1,625 @@
|
||||
// (c) 2023 John A. Breaux
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
||||
|
||||
//! Contains the definition of a Chip-8 [Insn]
|
||||
#![allow(clippy::bad_bit_mask)]
|
||||
//! Contains implementations for each [Insn] as private member functions of [CPU]
|
||||
|
||||
pub mod disassembler;
|
||||
use super::*;
|
||||
|
||||
use imperative_rs::InstructionSet;
|
||||
use std::fmt::Display;
|
||||
|
||||
|
||||
#[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 {
|
||||
impl CPU {
|
||||
/// Executes a single [Insn]
|
||||
#[inline(always)]
|
||||
#[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}"),
|
||||
// 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}"),
|
||||
Insn::long { i } => write!(f, "long ${i:04x}"),
|
||||
}
|
||||
pub(super) fn execute(&mut self, bus: &mut Bus, instruction: Insn) {
|
||||
match instruction {
|
||||
// Core Chip-8 instructions
|
||||
Insn::cls => self.clear_screen(bus),
|
||||
Insn::ret => self.ret(bus),
|
||||
Insn::jmp { A } => self.jump(A),
|
||||
Insn::call { A } => self.call(A, bus),
|
||||
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, bus),
|
||||
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, bus),
|
||||
Insn::dmao { x } => self.store_dma(x, bus),
|
||||
Insn::dmai { x } => self.load_dma(x, bus),
|
||||
// Super-Chip extensions
|
||||
Insn::scd { n } => self.scroll_down(n, bus),
|
||||
Insn::scr => self.scroll_right(bus),
|
||||
Insn::scl => self.scroll_left(bus),
|
||||
Insn::halt => self.flags.pause(),
|
||||
Insn::lores => self.init_lores(bus),
|
||||
Insn::hires => self.init_hires(bus),
|
||||
Insn::hfont { x } => self.load_big_sprite(x),
|
||||
Insn::flgo { x } => self.store_flags(x, bus),
|
||||
Insn::flgi { x } => self.load_flags(x, bus),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// |`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
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
// (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
327
src/cpu/mem.rs
@ -1,327 +0,0 @@
|
||||
// (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,23 +1,18 @@
|
||||
// (c) 2023 John A. Breaux
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
|
||||
//! Selects the memory behavior of the [super::CPU]
|
||||
//!
|
||||
//! Since [Quirks] implements [`From<Mode>`],
|
||||
//! Since [super::Quirks] implements [From<Mode>],
|
||||
//! this can be used to select the appropriate quirk-set
|
||||
|
||||
use super::Quirks;
|
||||
use crate::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Selects the memory behavior of the interpreter
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Mode {
|
||||
/// VIP emulation mode
|
||||
#[default]
|
||||
Chip8,
|
||||
/// Super Chip emulation mode
|
||||
/// Chip-48 emulation mode
|
||||
SChip,
|
||||
/// XO-Chip emulation mode
|
||||
XOChip,
|
||||
@ -28,54 +23,12 @@ impl FromStr for Mode {
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"chip" | "chip8" | "chip-8" => Ok(Mode::Chip8),
|
||||
"s" | "schip" | "superchip" | "super chip" => Ok(Mode::SChip),
|
||||
"xo" | "xochip" | "xo-chip" => Ok(Mode::XOChip),
|
||||
_ => Err(Error::InvalidMode { mode: s.into() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
"chip8" | "chip-8" => Ok(Mode::Chip8),
|
||||
"schip" | "superchip" => Ok(Mode::SChip),
|
||||
"xo-chip" | "xochip" => Ok(Mode::XOChip),
|
||||
_ => Err(Error::InvalidMode {
|
||||
mode: s.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,17 @@
|
||||
// (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 quirk behavior of the CPU on a granular level.
|
||||
///
|
||||
/// `false` is Cosmac-VIP-like behavior
|
||||
use super::Mode;
|
||||
/// Controls the authenticity behavior of the CPU on a granular level.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Quirks {
|
||||
/// Super Chip: Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0
|
||||
/// Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0
|
||||
pub bin_ops: bool,
|
||||
/// Super Chip: Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX
|
||||
/// Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX
|
||||
pub shift: bool,
|
||||
/// Super Chip: Draw operations shouldn't pause execution until the next timer tick
|
||||
/// Draw operations shouldn't pause execution until the next timer tick
|
||||
pub draw_wait: bool,
|
||||
/// 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
|
||||
/// DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1
|
||||
pub dma_inc: bool,
|
||||
/// Super Chip: Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`.
|
||||
/// Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`.
|
||||
pub stupid_jumps: bool,
|
||||
}
|
||||
|
||||
@ -30,7 +22,6 @@ impl From<bool> for Quirks {
|
||||
bin_ops: true,
|
||||
shift: true,
|
||||
draw_wait: true,
|
||||
screen_wrap: false,
|
||||
dma_inc: true,
|
||||
stupid_jumps: true,
|
||||
}
|
||||
@ -39,7 +30,6 @@ impl From<bool> for Quirks {
|
||||
bin_ops: false,
|
||||
shift: false,
|
||||
draw_wait: false,
|
||||
screen_wrap: false,
|
||||
dma_inc: false,
|
||||
stupid_jumps: false,
|
||||
}
|
||||
@ -47,6 +37,22 @@ 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 {
|
||||
fn default() -> Self {
|
||||
Self::from(false)
|
||||
|
248
src/cpu/tests.rs
248
src/cpu/tests.rs
@ -1,5 +1,5 @@
|
||||
// (c) 2023 John A. Breaux
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
||||
|
||||
//! Unit tests for [super::CPU]
|
||||
//!
|
||||
@ -13,72 +13,79 @@
|
||||
//! Some of these tests run >16M times, which is very silly
|
||||
|
||||
use super::*;
|
||||
use crate::Screen;
|
||||
use rand::random;
|
||||
pub(self) use crate::{
|
||||
bus,
|
||||
bus::{Bus, Region::*},
|
||||
};
|
||||
|
||||
mod decode;
|
||||
|
||||
fn setup_environment() -> (CPU, Screen) {
|
||||
let mut ch8 = (
|
||||
fn setup_environment() -> (CPU, Bus) {
|
||||
(
|
||||
CPU {
|
||||
flags: Flags {
|
||||
debug: true,
|
||||
pause: false,
|
||||
monotonic: Some(8),
|
||||
..Default::default()
|
||||
},
|
||||
..CPU::default()
|
||||
},
|
||||
Screen::default(),
|
||||
);
|
||||
ch8.0
|
||||
.load_program_bytes(include_bytes!("tests/roms/jumptest.ch8"))
|
||||
.unwrap();
|
||||
ch8
|
||||
bus! {
|
||||
// Load the charset into ROM
|
||||
Charset [0x0050..0x00A0] = include_bytes!("../mem/charset.bin"),
|
||||
// Load the ROM file into RAM (dummy binary which contains nothing but `jmp pc+2`)
|
||||
Program [0x0200..0x1000] = include_bytes!("tests/roms/jumptest.ch8"),
|
||||
// Create a screen
|
||||
Screen [0x0F00..0x1000] = include_bytes!("../../chip8Archive/roms/1dcell.ch8"),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn print_screen(bytes: &[u8]) {
|
||||
Screen::from(bytes).print_screen()
|
||||
bus! {Screen [0..0x100] = bytes}
|
||||
.print_screen()
|
||||
.expect("Printing screen should not fail if Screen exists.")
|
||||
}
|
||||
|
||||
/// Unused instructions
|
||||
mod unimplemented {
|
||||
use super::*;
|
||||
macro_rules! invalid {
|
||||
($(
|
||||
$(#[$attr:meta])* // Preserve doc comments, etc.
|
||||
$pub:vis test $name:ident { $($insn:literal),+$(,)? }
|
||||
);+ $(;)?) => {
|
||||
$( $(#[$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_5xyn() {
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
bus.write(0x200u16, 0x500fu16);
|
||||
cpu.tick(&mut bus)
|
||||
.expect_err("0x500f is not an instruction");
|
||||
}
|
||||
|
||||
invalid! {
|
||||
/// Invalid opcodes 5001, 0x5004-500f
|
||||
test ins_5xyn {
|
||||
0x5001,
|
||||
0x5004, 0x5005, 0x5006, 0x5007, 0x5008, 0x5009,
|
||||
0x500a, 0x500b, 0x500c, 0x500d, 0x500e, 0x500f,
|
||||
};
|
||||
/// Invalid opcodes 8008-800d and 800f
|
||||
test ins_8xyn {
|
||||
0x8008, 0x8009, 0x800a, 0x800b, 0x800c, 0x800d,
|
||||
0x800f,
|
||||
};
|
||||
/// Invalid opcodes 9001-900f
|
||||
test ins_9xyn {
|
||||
0x9001, 0x9002, 0x9003, 0x9004, 0x9005,
|
||||
0x9006, 0x9007, 0x9008, 0x9009, 0x900a,
|
||||
0x900b, 0x900c, 0x900d, 0x900e, 0x900f,
|
||||
};
|
||||
/// Invalid opcodes in e000 range
|
||||
test ins_exbb { 0xe00f };
|
||||
/// Invalid opcodes in the 0xf000 range
|
||||
test ins_fxbb { 0xf001, 0xf00f };
|
||||
#[test]
|
||||
fn ins_8xyn() {
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
bus.write(0x200u16, 0x800fu16);
|
||||
cpu.tick(&mut bus)
|
||||
.expect_err("0x800f is not an instruction");
|
||||
}
|
||||
#[test]
|
||||
fn ins_9xyn() {
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
bus.write(0x200u16, 0x900fu16);
|
||||
cpu.tick(&mut bus)
|
||||
.expect_err("0x900f is not an instruction");
|
||||
}
|
||||
#[test]
|
||||
fn ins_exbb() {
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
bus.write(0x200u16, 0xe00fu16);
|
||||
cpu.tick(&mut bus)
|
||||
.expect_err("0xe00f is not an instruction");
|
||||
}
|
||||
// Fxbb
|
||||
#[test]
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,11 +94,10 @@ mod sys {
|
||||
/// 00e0: Clears the screen memory to 0
|
||||
#[test]
|
||||
fn clear_screen() {
|
||||
let (mut cpu, mut screen) = setup_environment();
|
||||
cpu.clear_screen(&mut screen);
|
||||
screen
|
||||
.grab(..)
|
||||
.unwrap()
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
cpu.clear_screen(&mut bus);
|
||||
bus.get_region(Screen)
|
||||
.expect("Expected screen, got None")
|
||||
.iter()
|
||||
.for_each(|byte| assert_eq!(*byte, 0));
|
||||
}
|
||||
@ -100,14 +106,17 @@ mod sys {
|
||||
#[test]
|
||||
fn ret() {
|
||||
let test_addr = random::<u16>() & 0x7ff;
|
||||
let (mut cpu, _) = setup_environment();
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
let sp_orig = cpu.sp;
|
||||
// Place the address on the stack
|
||||
cpu.stack.push(test_addr);
|
||||
bus.write(cpu.sp.wrapping_add(2), test_addr);
|
||||
|
||||
cpu.ret();
|
||||
cpu.ret(&bus);
|
||||
|
||||
// Verify the current address is the address from the stack
|
||||
assert_eq!(test_addr, cpu.pc);
|
||||
// Verify the stack pointer has moved
|
||||
assert!(dbg!(cpu.sp.wrapping_sub(sp_orig)) == 0x2);
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,9 +132,9 @@ mod cf {
|
||||
let (mut cpu, _) = setup_environment();
|
||||
// Test all valid addresses
|
||||
for addr in 0x000..0xffe {
|
||||
// Jump to an address
|
||||
// Call an address
|
||||
cpu.jump(addr);
|
||||
// Verify the current address is the jump target address
|
||||
// Verify the current address is the called address
|
||||
assert_eq!(addr, cpu.pc);
|
||||
}
|
||||
}
|
||||
@ -134,15 +143,15 @@ mod cf {
|
||||
#[test]
|
||||
fn call() {
|
||||
let test_addr = random::<u16>();
|
||||
let (mut cpu, _) = setup_environment();
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
// Save the current address
|
||||
let curr_addr = cpu.pc;
|
||||
// Call an address
|
||||
cpu.call(test_addr);
|
||||
cpu.call(test_addr, &mut bus);
|
||||
// Verify the current address is the called address
|
||||
assert_eq!(test_addr, cpu.pc);
|
||||
// Verify the previous address was stored on the stack (sp+2)
|
||||
let stack_addr: u16 = cpu.stack.pop().expect("This should return test_addr");
|
||||
let stack_addr: u16 = bus.read(cpu.sp.wrapping_add(2));
|
||||
assert_eq!(stack_addr, curr_addr);
|
||||
}
|
||||
|
||||
@ -586,7 +595,7 @@ mod math {
|
||||
for reg in 0..=0xff {
|
||||
let (x, y) = (reg & 0xf, reg >> 4);
|
||||
// set the register under test to `word`
|
||||
(cpu.v[x], cpu.v[y]) = (0, word);
|
||||
(cpu.v[x], cpu.v[y]) = dbg!(0, word);
|
||||
|
||||
cpu.shift_left(x, y);
|
||||
|
||||
@ -665,9 +674,9 @@ mod i {
|
||||
/// - Random number generation
|
||||
/// - Drawing to the display
|
||||
mod io {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
use super::*;
|
||||
/// Cxbb: Stores a random number & the provided byte into vX
|
||||
#[test]
|
||||
fn rand() {
|
||||
@ -698,12 +707,11 @@ mod io {
|
||||
ScreenTest {
|
||||
program: include_bytes!("../../chip-8/IBM Logo.ch8"),
|
||||
screen: include_bytes!("tests/screens/IBM Logo.ch8/20.bin"),
|
||||
steps: 21,
|
||||
steps: 56,
|
||||
quirks: Quirks {
|
||||
bin_ops: false,
|
||||
shift: false,
|
||||
draw_wait: false,
|
||||
screen_wrap: true,
|
||||
dma_inc: false,
|
||||
stupid_jumps: false,
|
||||
},
|
||||
@ -719,7 +727,6 @@ mod io {
|
||||
bin_ops: false,
|
||||
shift: false,
|
||||
draw_wait: true,
|
||||
screen_wrap: true,
|
||||
dma_inc: false,
|
||||
stupid_jumps: false,
|
||||
},
|
||||
@ -733,7 +740,6 @@ mod io {
|
||||
bin_ops: false,
|
||||
shift: false,
|
||||
draw_wait: true,
|
||||
screen_wrap: true,
|
||||
dma_inc: false,
|
||||
stupid_jumps: false,
|
||||
},
|
||||
@ -744,21 +750,26 @@ mod io {
|
||||
#[test]
|
||||
fn draw() {
|
||||
for test in SCREEN_TESTS {
|
||||
let (mut cpu, mut screen) = setup_environment();
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
cpu.flags.quirks = test.quirks;
|
||||
// Debug mode is 5x slower
|
||||
cpu.flags.debug = false;
|
||||
// Load the test program
|
||||
cpu.mem.load_region(Program, test.program).unwrap();
|
||||
bus = bus.load_region(Program, test.program);
|
||||
// Run the test program for the specified number of steps
|
||||
while cpu.cycle() < test.steps {
|
||||
cpu.multistep(&mut screen, 10.min(test.steps - cpu.cycle()))
|
||||
cpu.multistep(&mut bus, test.steps - cpu.cycle())
|
||||
.expect("Draw tests should not contain undefined instructions");
|
||||
}
|
||||
// Compare the screen to the reference screen buffer
|
||||
screen.print_screen();
|
||||
bus.print_screen()
|
||||
.expect("Printing screen should not fail if screen exists");
|
||||
print_screen(test.screen);
|
||||
assert_eq!(screen.grab(..).unwrap(), test.screen);
|
||||
assert_eq!(
|
||||
bus.get_region(Screen)
|
||||
.expect("Getting screen should not fail if screen exists"),
|
||||
test.screen
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -849,7 +860,7 @@ mod io {
|
||||
assert!(cpu.release(key).expect("Key should be released"));
|
||||
assert!(!cpu.release(key).expect("Key shouldn't be released again"));
|
||||
assert!(!cpu.flags.keypause);
|
||||
assert_eq!(Some(key), cpu.lastkey);
|
||||
assert_eq!(Some(key), cpu.flags.lastkey);
|
||||
cpu.wait_for_key(x);
|
||||
assert_eq!(key as u8, cpu.v[x]);
|
||||
}
|
||||
@ -864,7 +875,8 @@ mod io {
|
||||
for word in 0..=0xff {
|
||||
for x in 0..=0xf {
|
||||
// set the register under test to `word`
|
||||
cpu.delay = word;
|
||||
cpu.delay = word as f64;
|
||||
|
||||
cpu.load_delay_timer(x);
|
||||
|
||||
assert_eq!(cpu.v[x], word);
|
||||
@ -883,7 +895,7 @@ mod io {
|
||||
|
||||
cpu.store_delay_timer(x);
|
||||
|
||||
assert_eq!(cpu.delay, word);
|
||||
assert_eq!(cpu.delay, word as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -899,7 +911,7 @@ mod io {
|
||||
|
||||
cpu.store_sound_timer(x);
|
||||
|
||||
assert_eq!(cpu.sound, word);
|
||||
assert_eq!(cpu.sound, word as f64);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -936,7 +948,7 @@ mod io {
|
||||
/// Fx29: Load sprite for character vX into I
|
||||
#[test]
|
||||
fn load_sprite() {
|
||||
let (mut cpu, _) = setup_environment();
|
||||
let (mut cpu, bus) = setup_environment();
|
||||
for test in TESTS {
|
||||
let reg = 0xf & random::<usize>();
|
||||
// load number into CPU register
|
||||
@ -946,8 +958,7 @@ mod io {
|
||||
|
||||
let addr = cpu.i as usize;
|
||||
assert_eq!(
|
||||
cpu.mem
|
||||
.grab(addr..addr.wrapping_add(5))
|
||||
bus.get(addr..addr.wrapping_add(5))
|
||||
.expect("Region at addr should exist!"),
|
||||
test.output,
|
||||
);
|
||||
@ -956,7 +967,7 @@ mod io {
|
||||
}
|
||||
|
||||
mod bcdtest {
|
||||
use super::*;
|
||||
pub(self) use super::*;
|
||||
|
||||
struct BCDTest {
|
||||
// value to test
|
||||
@ -984,18 +995,15 @@ mod io {
|
||||
#[test]
|
||||
fn bcd_convert() {
|
||||
for test in BCD_TESTS {
|
||||
let (mut cpu, _) = setup_environment();
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
let addr = 0xff0 & random::<u16>() as usize;
|
||||
// load CPU registers
|
||||
cpu.i = addr as u16;
|
||||
cpu.v[5] = test.input;
|
||||
|
||||
cpu.bcd_convert(5);
|
||||
cpu.bcd_convert(5, &mut bus);
|
||||
|
||||
assert_eq!(
|
||||
cpu.mem.grab(addr..addr.saturating_add(3)),
|
||||
Some(test.output)
|
||||
)
|
||||
assert_eq!(bus.get(addr..addr.saturating_add(3)), Some(test.output))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1004,7 +1012,7 @@ mod io {
|
||||
// TODO: Test with dma_inc quirk set
|
||||
#[test]
|
||||
fn dma_store() {
|
||||
let (mut cpu, _) = setup_environment();
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
|
||||
// Load some test data into memory
|
||||
let addr = 0x456;
|
||||
@ -1015,16 +1023,15 @@ mod io {
|
||||
for len in 0..16 {
|
||||
// Perform DMA store
|
||||
cpu.i = addr as u16;
|
||||
cpu.store_dma(len);
|
||||
// Check that screen grabbed the correct data
|
||||
let screen = cpu
|
||||
.mem
|
||||
.grab_mut(addr..addr + DATA.len())
|
||||
cpu.store_dma(len, &mut bus);
|
||||
// Check that bus grabbed the correct data
|
||||
let bus = bus
|
||||
.get_mut(addr..addr + DATA.len())
|
||||
.expect("Getting a mutable slice at addr 0x0456 should not fail");
|
||||
assert_eq!(screen[0..=len], DATA[0..=len]);
|
||||
assert_eq!(screen[len + 1..], [0; 16][len + 1..]);
|
||||
assert_eq!(bus[0..=len], DATA[0..=len]);
|
||||
assert_eq!(bus[len + 1..], [0; 16][len + 1..]);
|
||||
// clear
|
||||
screen.fill(0);
|
||||
bus.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1032,19 +1039,18 @@ mod io {
|
||||
// TODO: Test with dma_inc quirk set
|
||||
#[test]
|
||||
fn dma_load() {
|
||||
let (mut cpu, _) = setup_environment();
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
|
||||
// Load some test data into memory
|
||||
let addr = 0x456;
|
||||
cpu.mem
|
||||
.grab_mut(addr..addr + DATA.len())
|
||||
bus.get_mut(addr..addr + DATA.len())
|
||||
.expect("Getting a mutable slice at addr 0x0456..0x0466 should not fail")
|
||||
.write_all(DATA)
|
||||
.unwrap();
|
||||
for len in 0..16 {
|
||||
// Perform DMA load
|
||||
cpu.i = addr as u16;
|
||||
cpu.load_dma(len);
|
||||
cpu.load_dma(len, &mut bus);
|
||||
// Check that registers grabbed the correct data
|
||||
assert_eq!(cpu.v[0..=len], DATA[0..=len]);
|
||||
assert_eq!(cpu.v[len + 1..], [0; 16][len + 1..]);
|
||||
@ -1062,37 +1068,37 @@ mod behavior {
|
||||
use std::time::Duration;
|
||||
#[test]
|
||||
fn delay() {
|
||||
let (mut cpu, mut screen) = setup_environment();
|
||||
cpu.flags.monotonic = false;
|
||||
cpu.delay = 10;
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
cpu.flags.monotonic = None;
|
||||
cpu.delay = 10.0;
|
||||
for _ in 0..2 {
|
||||
cpu.multistep(&mut screen, 8)
|
||||
cpu.multistep(&mut bus, 8)
|
||||
.expect("Running valid instructions should always succeed");
|
||||
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
||||
}
|
||||
// time is within 1 frame deviance over a theoretical 2 frame pause
|
||||
assert_eq!(cpu.delay, 8);
|
||||
assert!(7.0 <= cpu.delay && cpu.delay <= 9.0);
|
||||
}
|
||||
#[test]
|
||||
fn sound() {
|
||||
let (mut cpu, mut screen) = setup_environment();
|
||||
cpu.flags.monotonic = false; // disable monotonic timing
|
||||
cpu.sound = 10;
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
cpu.flags.monotonic = None; // disable monotonic timing
|
||||
cpu.sound = 10.0;
|
||||
for _ in 0..2 {
|
||||
cpu.multistep(&mut screen, 8)
|
||||
cpu.multistep(&mut bus, 8)
|
||||
.expect("Running valid instructions should always succeed");
|
||||
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
||||
}
|
||||
// time is within 1 frame deviance over a theoretical 2 frame pause
|
||||
assert_eq!(cpu.sound, 8);
|
||||
assert!(7.0 <= cpu.sound && cpu.sound <= 9.0);
|
||||
}
|
||||
#[test]
|
||||
fn vbi_wait() {
|
||||
let (mut cpu, mut screen) = setup_environment();
|
||||
cpu.flags.monotonic = false; // disable monotonic timing
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
cpu.flags.monotonic = None; // disable monotonic timing
|
||||
cpu.flags.draw_wait = true;
|
||||
for _ in 0..2 {
|
||||
cpu.multistep(&mut screen, 8)
|
||||
cpu.multistep(&mut bus, 8)
|
||||
.expect("Running valid instructions should always succeed");
|
||||
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
|
||||
}
|
||||
@ -1106,9 +1112,9 @@ mod behavior {
|
||||
#[test]
|
||||
#[cfg_attr(feature = "unstable", no_coverage)]
|
||||
fn hit_break() {
|
||||
let (mut cpu, mut screen) = setup_environment();
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
cpu.set_break(0x202);
|
||||
match cpu.multistep(&mut screen, 10) {
|
||||
match cpu.multistep(&mut bus, 10) {
|
||||
Err(crate::error::Error::BreakpointHit { addr, next }) => {
|
||||
assert_eq!(0x202, addr); // current address is 202
|
||||
assert_eq!(0x1204, next); // next insn is `jmp 204`
|
||||
@ -1121,9 +1127,9 @@ mod behavior {
|
||||
#[test]
|
||||
#[cfg_attr(feature = "unstable", no_coverage)]
|
||||
fn hit_break_singlestep() {
|
||||
let (mut cpu, mut screen) = setup_environment();
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
cpu.set_break(0x202);
|
||||
match cpu.singlestep(&mut screen) {
|
||||
match cpu.singlestep(&mut bus) {
|
||||
Err(crate::error::Error::BreakpointHit { addr, next }) => {
|
||||
assert_eq!(0x202, addr); // current address is 202
|
||||
assert_eq!(0x1204, next); // next insn is `jmp 204`
|
||||
@ -1138,12 +1144,12 @@ mod behavior {
|
||||
#[test]
|
||||
#[cfg_attr(feature = "unstable", no_coverage)]
|
||||
fn invalid_pc() {
|
||||
let (mut cpu, mut screen) = setup_environment();
|
||||
// The screen extends from 0x0..0x1000
|
||||
cpu.pc = 0x1001;
|
||||
match cpu.tick(&mut screen) {
|
||||
Err(Error::InvalidAddressRange { range }) => {
|
||||
eprintln!("InvalidAddressRange {{ {range:04x?} }}")
|
||||
let (mut cpu, mut bus) = setup_environment();
|
||||
// The bus extends from 0x0..0x1000
|
||||
cpu.pc = 0xfff;
|
||||
match cpu.tick(&mut bus) {
|
||||
Err(Error::InvalidBusRange { range }) => {
|
||||
eprintln!("InvalidBusRange {{ {range:04x?} }}")
|
||||
}
|
||||
other => unreachable!("{other:04x?}"),
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
// (c) 2023 John A. Breaux
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
|
||||
//! Exercises the instruction decode logic.
|
||||
use super::*;
|
||||
|
||||
@ -9,15 +6,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
|
||||
/// returns the CPU for inspection
|
||||
fn run_single_op(op: &[u8]) -> CPU {
|
||||
let (mut cpu, mut screen) = (
|
||||
let (mut cpu, mut bus) = (
|
||||
CPU::default(),
|
||||
Screen::default(),
|
||||
bus! {
|
||||
Program[0x200..0x240] = op,
|
||||
},
|
||||
);
|
||||
cpu.mem
|
||||
.load_region(Program, op).unwrap();
|
||||
cpu.v = *INDX;
|
||||
cpu.flags.quirks = Quirks::from(false);
|
||||
cpu.tick(&mut screen).unwrap(); // will panic if unimplemented
|
||||
cpu.tick(&mut bus).unwrap(); // will panic if unimplemented
|
||||
cpu
|
||||
}
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
29
src/error.rs
29
src/error.rs
@ -1,12 +1,11 @@
|
||||
// (c) 2023 John A. Breaux
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
||||
|
||||
//! Error type for Chirp
|
||||
|
||||
pub mod any_range;
|
||||
use any_range::AnyRange;
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::cpu::mem::Region;
|
||||
use crate::bus::Region;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Result type, equivalent to [std::result::Result]<T, [enum@Error]>
|
||||
@ -16,7 +15,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Represents a breakpoint being hit
|
||||
#[error("breakpoint hit: {addr:03x} ({next:04x})")]
|
||||
#[error("Breakpoint hit: {addr:03x} ({next:04x})")]
|
||||
BreakpointHit {
|
||||
/// The address of the breakpoint
|
||||
addr: u16,
|
||||
@ -24,37 +23,37 @@ pub enum Error {
|
||||
next: u16,
|
||||
},
|
||||
/// Represents an unimplemented operation
|
||||
#[error("opcode {word:04x} not recognized")]
|
||||
#[error("Unrecognized opcode: {word:04x}")]
|
||||
UnimplementedInstruction {
|
||||
/// The offending word
|
||||
word: u16,
|
||||
},
|
||||
/// The region you asked for was not defined
|
||||
#[error("region {region} is not present on bus")]
|
||||
#[error("No {region} found on bus")]
|
||||
MissingRegion {
|
||||
/// The offending [Region]
|
||||
region: Region,
|
||||
},
|
||||
/// Tried to fetch data at [AnyRange] from bus, received nothing
|
||||
#[error("range {range:04x?} is not present on bus")]
|
||||
InvalidAddressRange {
|
||||
/// The offending [AnyRange]
|
||||
range: AnyRange<usize>,
|
||||
/// Tried to fetch [Range] from bus, received nothing
|
||||
#[error("Invalid range {range:04x?} for bus")]
|
||||
InvalidBusRange {
|
||||
/// The offending [Range]
|
||||
range: Range<usize>,
|
||||
},
|
||||
/// Tried to press a key that doesn't exist
|
||||
#[error("tried to press key {key:X} which does not exist")]
|
||||
#[error("Invalid key: {key:X}")]
|
||||
InvalidKey {
|
||||
/// The offending key
|
||||
key: usize,
|
||||
},
|
||||
/// Tried to get/set an out-of-bounds register
|
||||
#[error("tried to access register v{reg:X} which does not exist")]
|
||||
#[error("Invalid register: v{reg:X}")]
|
||||
InvalidRegister {
|
||||
/// The offending register
|
||||
reg: usize,
|
||||
},
|
||||
/// Tried to convert string into mode, but it did not match.
|
||||
#[error("no suitable conversion of \"{mode}\" into Mode")]
|
||||
#[error("Invalid mode: {mode}")]
|
||||
InvalidMode {
|
||||
/// The string which failed to become a mode
|
||||
mode: String,
|
||||
|
@ -1,69 +0,0 @@
|
||||
// (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)
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
//! 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
|
||||
// This code is licensed under MIT license (see LICENSE for details)
|
||||
#![cfg_attr(feature = "nightly", feature(no_coverage))]
|
||||
// This code is licensed under MIT license (see LICENSE.txt for details)
|
||||
#![cfg_attr(feature = "unstable", feature(no_coverage))]
|
||||
#![deny(missing_docs, clippy::all)]
|
||||
//! 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
|
||||
@ -8,19 +8,26 @@
|
||||
//!
|
||||
//! Hopefully, though, you'll find some use in it.
|
||||
|
||||
pub mod bus;
|
||||
pub mod cpu;
|
||||
pub mod error;
|
||||
pub mod screen;
|
||||
pub mod traits;
|
||||
|
||||
// Common imports for Chirp
|
||||
pub use bus::{Bus, Read, Region::*, Write};
|
||||
pub use cpu::{
|
||||
disassembler::{Dis, Disassembler},
|
||||
flags::Flags,
|
||||
instruction::disassembler::{Dis, Disassembler},
|
||||
mode::Mode,
|
||||
quirks::Quirks,
|
||||
CPU,
|
||||
};
|
||||
pub use error::{Error, Result};
|
||||
pub use screen::Screen;
|
||||
pub use traits::{AutoCast, FallibleAutoCast, Grab};
|
||||
|
||||
/// Holds the state of a Chip-8
|
||||
#[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,
|
||||
}
|
||||
|
Binary file not shown.
132
src/screen.rs
132
src/screen.rs
@ -1,132 +0,0 @@
|
||||
//! 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!("|");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
// (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};
|
@ -1,106 +0,0 @@
|
||||
// (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,42 +1,46 @@
|
||||
// (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
|
||||
|
||||
pub use chirp::*;
|
||||
|
||||
fn setup_environment() -> (CPU, Screen) {
|
||||
fn setup_environment() -> (CPU, Bus) {
|
||||
let mut cpu = CPU::default();
|
||||
cpu.flags = Flags {
|
||||
debug: true,
|
||||
pause: false,
|
||||
monotonic: Some(8),
|
||||
..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 {
|
||||
test: u8,
|
||||
data: &'static [u8],
|
||||
test: u16,
|
||||
screen: &'static [u8],
|
||||
}
|
||||
|
||||
fn run_screentest(test: SuiteTest, mut cpu: CPU, mut screen: Screen) {
|
||||
fn run_screentest(test: SuiteTest, mut cpu: CPU, mut bus: Bus) {
|
||||
// Set the test to run
|
||||
cpu.poke(0x1ffu16, test.test);
|
||||
cpu.load_program_bytes(test.data).unwrap();
|
||||
bus.write(0x1feu16, test.test);
|
||||
// The test suite always initiates a keypause on test completion
|
||||
while !(cpu.flags.is_paused()) {
|
||||
cpu.multistep(&mut screen, 10).unwrap();
|
||||
if cpu.cycle() > 1000000 {
|
||||
panic!("test {} took too long", test.test)
|
||||
}
|
||||
while !cpu.flags.keypause {
|
||||
cpu.multistep(&mut bus, 8).unwrap();
|
||||
}
|
||||
// Compare the screen to the reference screen buffer
|
||||
screen.print_screen();
|
||||
Screen::from(test.screen).print_screen();
|
||||
assert_eq!(screen.grab(..).unwrap(), test.screen);
|
||||
bus.print_screen().unwrap();
|
||||
bus! {crate::bus::Region::Screen [0..256] = test.screen}
|
||||
.print_screen()
|
||||
.unwrap();
|
||||
assert_eq!(bus.get_region(Screen).unwrap(), test.screen);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -45,7 +49,6 @@ fn splash_screen() {
|
||||
run_screentest(
|
||||
SuiteTest {
|
||||
test: 0,
|
||||
data: include_bytes!("../chip8-test-suite/bin/1-chip8-logo.ch8"),
|
||||
screen: include_bytes!("screens/chip8-test-suite/splash.bin"),
|
||||
},
|
||||
cpu,
|
||||
@ -58,8 +61,7 @@ fn ibm_logo() {
|
||||
let (cpu, bus) = setup_environment();
|
||||
run_screentest(
|
||||
SuiteTest {
|
||||
test: 0x00,
|
||||
data: include_bytes!("../chip8-test-suite/bin/2-ibm-logo.ch8"),
|
||||
test: 0x01,
|
||||
screen: include_bytes!("screens/chip8-test-suite/IBM.bin"),
|
||||
},
|
||||
cpu,
|
||||
@ -67,27 +69,12 @@ 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]
|
||||
fn flags_test() {
|
||||
let (cpu, bus) = setup_environment();
|
||||
run_screentest(
|
||||
SuiteTest {
|
||||
test: 0x00,
|
||||
data: include_bytes!("../chip8-test-suite/bin/4-flags.ch8"),
|
||||
test: 0x03,
|
||||
screen: include_bytes!("screens/chip8-test-suite/flags.bin"),
|
||||
},
|
||||
cpu,
|
||||
@ -100,8 +87,7 @@ fn quirks_test() {
|
||||
let (cpu, bus) = setup_environment();
|
||||
run_screentest(
|
||||
SuiteTest {
|
||||
test: 0x01,
|
||||
data: include_bytes!("../chip8-test-suite/bin/5-quirks.ch8"),
|
||||
test: 0x0104,
|
||||
screen: include_bytes!("screens/chip8-test-suite/quirks.bin"),
|
||||
},
|
||||
cpu,
|
||||
|
@ -1,50 +1,56 @@
|
||||
//! Testing methods on Chirp's public API
|
||||
use chirp::cpu::mem::Region::*;
|
||||
use chirp::*;
|
||||
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 {
|
||||
use super::*;
|
||||
mod region {
|
||||
|
||||
use super::*;
|
||||
// #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[test]
|
||||
fn copy() {
|
||||
let r1 = Charset;
|
||||
let r1 = Screen;
|
||||
let r2 = r1;
|
||||
assert_eq!(r1, r2);
|
||||
}
|
||||
#[test]
|
||||
#[allow(clippy::clone_on_copy)]
|
||||
fn clone() {
|
||||
let r1 = Charset;
|
||||
let r1 = Screen;
|
||||
let r2 = r1.clone();
|
||||
assert_eq!(r1, r2);
|
||||
}
|
||||
#[test]
|
||||
fn display() {
|
||||
println!("{Charset}{Program}{Count}");
|
||||
println!("{Charset}{Program}{Screen}{Stack}{Count}");
|
||||
}
|
||||
#[test]
|
||||
fn debug() {
|
||||
println!("{Charset:?}{Program:?}{Count:?}");
|
||||
println!("{Charset:?}{Program:?}{Screen:?}{Stack:?}{Count:?}");
|
||||
}
|
||||
// lmao the things you do for test coverage
|
||||
#[test]
|
||||
fn eq() {
|
||||
assert_eq!(Charset, Charset);
|
||||
assert_eq!(Screen, Screen);
|
||||
assert_ne!(Charset, Program);
|
||||
}
|
||||
#[test]
|
||||
fn ord() {
|
||||
assert_eq!(Program, Charset.max(Program));
|
||||
assert!(Charset < Program);
|
||||
assert_eq!(Stack, Charset.max(Program).max(Screen).max(Stack));
|
||||
assert!(Charset < Program && Program < Screen && Screen < Stack);
|
||||
}
|
||||
#[test]
|
||||
fn hash() {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
Program.hash(&mut hasher);
|
||||
Stack.hash(&mut hasher);
|
||||
println!("{hasher:?}");
|
||||
}
|
||||
}
|
||||
@ -52,7 +58,7 @@ mod bus {
|
||||
#[should_panic]
|
||||
fn bus_missing_region() {
|
||||
// Print the screen of a bus with no screen
|
||||
mem! {}.get_region(Charset).unwrap();
|
||||
bus! {}.print_screen().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,13 +125,14 @@ mod cpu {
|
||||
use super::*;
|
||||
//#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[test]
|
||||
#[allow(clippy::redundant_clone)]
|
||||
fn clone() {
|
||||
let cf1 = Flags {
|
||||
debug: false,
|
||||
pause: false,
|
||||
keypause: false,
|
||||
draw_wait: false,
|
||||
lastkey: None,
|
||||
monotonic: None,
|
||||
..Default::default()
|
||||
};
|
||||
let cf2 = cf1.clone();
|
||||
@ -183,7 +190,7 @@ mod cpu {
|
||||
}
|
||||
|
||||
mod dis {
|
||||
use chirp::cpu::instruction::Insn;
|
||||
use chirp::cpu::disassembler::Insn;
|
||||
use imperative_rs::InstructionSet;
|
||||
|
||||
#[test]
|
||||
@ -201,7 +208,7 @@ mod dis {
|
||||
|
||||
#[test]
|
||||
fn error() {
|
||||
let error = chirp::error::Error::InvalidAddressRange { range: (..).into() };
|
||||
let error = chirp::error::Error::MissingRegion { region: Screen };
|
||||
// Print it with Display and Debug
|
||||
println!("{error} {error:?}");
|
||||
}
|
||||
@ -219,7 +226,6 @@ mod quirks {
|
||||
bin_ops: true,
|
||||
shift: true,
|
||||
draw_wait: true,
|
||||
screen_wrap: false,
|
||||
dma_inc: true,
|
||||
stupid_jumps: true,
|
||||
}
|
||||
@ -235,7 +241,6 @@ mod quirks {
|
||||
bin_ops: false,
|
||||
shift: false,
|
||||
draw_wait: false,
|
||||
screen_wrap: false,
|
||||
dma_inc: false,
|
||||
stupid_jumps: false,
|
||||
}
|
||||
@ -248,11 +253,9 @@ mod quirks {
|
||||
bin_ops: false,
|
||||
shift: true,
|
||||
draw_wait: false,
|
||||
screen_wrap: false,
|
||||
dma_inc: true,
|
||||
stupid_jumps: false,
|
||||
};
|
||||
#[allow(clippy::clone_on_copy)]
|
||||
let q2 = q1.clone();
|
||||
assert_eq!(q1, q2);
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user