Compare commits

...

45 Commits

Author SHA1 Message Date
8d2399e431 mem.rs: Add Display for HiresCharset 2023-08-31 22:43:01 -05:00
39fe4411d6 cpu.rs: Properly pause during multistep 2023-08-31 22:41:24 -05:00
7ab0a596c9 cpu.rs, tests.rs: fix clippy lints (thanks clippy) 2023-08-31 22:40:30 -05:00
8de4f42ff6 docs: Fix examples (now passes all doc tests again) 2023-08-31 22:39:14 -05:00
31c904f89c chirp-disasm: Implement a graph parser for better output 2023-08-31 22:34:55 -05:00
f886aadc63 chirp: Break frontends into separate projects 2023-08-31 18:16:38 -05:00
bcd5499833 justfile: chirp is no longer the default 2023-04-30 00:56:55 -05:00
5ae98346a0 chirp-imgui: Make sure ROM file exists. 2023-04-30 00:55:56 -05:00
e4f8001a16 CPU: Add hires font 2023-04-30 00:54:58 -05:00
37b8b96683 instruction.rs: Clarify the purpose of Insn 2023-04-29 23:34:28 -05:00
861020a8ab chirp-imgui: Argument parsing + colors in Settings 2023-04-29 23:34:06 -05:00
dcbdd1383d lib.rs: Remove Chip8 unused Chip8 struct (was moved) 2023-04-29 23:33:11 -05:00
96b6038bbe Chirp: Bus Schism: Split into Mem (internal) and Screen (external) 2023-04-29 23:32:14 -05:00
f4d7e514bc auto_cast: Move out of bus module 2023-04-29 19:52:38 -05:00
beab7c968b Fix copy paste errors and read-out-of-bounds check 2023-04-29 18:44:00 -05:00
33b6d0218e cpu/tests: Test invalid instructions more exhaustively 2023-04-29 18:43:08 -05:00
c7226bf9cc cpu.rs: Fix up ROM loading in new(), and doctests 2023-04-29 18:42:19 -05:00
4ec9e2cb71 cpu/behavior: Tag each Insn implementation 2023-04-29 18:39:44 -05:00
8b4d5be49d tests: Update tests for memory being CPU-internal 2023-04-29 18:38:38 -05:00
cb69af048f cpu: Change ST and DT back to u8 2023-04-29 18:37:29 -05:00
57c2ac681c cpu: fix renaming mistake "screen" -> "mem" 2023-04-29 18:34:24 -05:00
ea357be477 Monotonic: This flag is being deprecated soon, switch it for bool 2023-04-29 18:20:49 -05:00
a16d7fe732 chirp-imgui: Refactor menu definitions 2023-04-29 18:15:19 -05:00
7d5718f384 cpu.rs: Separate lastkey from flags 2023-04-29 12:08:26 -05:00
05fee3fb75 Begin work on graph-traversal disassembler? 2023-04-23 12:16:50 -05:00
16a5e6a2a4 cpu.rs: Actually derive (De)Serialize if feature=serde 2023-04-23 12:16:31 -05:00
5b5c5b41ab chirp: Update chirp for changes in cpu.rs 2023-04-23 12:15:36 -05:00
04736f1153 mode.rs: Implement more traits for use in frontend 2023-04-23 12:14:46 -05:00
59ba8ac20b bus/read.rs: Improve template code somewhat 2023-04-23 12:14:04 -05:00
e9f8d917a4 chirp-imgui: First prototype, using pixels demo as base 2023-04-23 12:13:22 -05:00
c1219e60f0 cpu.rs: Refactor for modularity
- Break into submodules
  - Move bus into submodule of CPU
  - Keep program and charset rom inside CPU
  - Take only the screen on the external Bus
  - Refactor the disassembler into an instruction definition and the actual "Dis" item
2023-04-23 12:10:02 -05:00
33da1089a2 AnyRange: Add AnyRange for taking any range in error 2023-04-23 12:01:47 -05:00
92dc899510 Update copyright notices 2023-04-23 11:58:57 -05:00
3839e90b15 chip8Archive: Update Submodule (adds wdl and octojam9!) 2023-04-18 22:44:47 -05:00
1c1d4dafaf cpu.rs: Adjust doctests for new stack behavior 2023-04-17 06:39:05 -05:00
0113dd95f6 ui.rs: Refactor debug screen dump: dumps to pwd with good name 2023-04-17 06:35:16 -05:00
45adf0a2b8 cpu.rs: Remove stack from main memory 2023-04-17 06:34:48 -05:00
e842755d77 quirks: Fix description for screen_wrap 2023-04-17 05:39:18 -05:00
88693f6c72 screens: Fix last-byte-bug in regression test cases 2023-04-17 05:27:23 -05:00
381b2a69bd tests: load_region asserts that all data must be copied 2023-04-17 05:26:49 -05:00
1573e00928 instruction.rs: Add edge wrapping for draw_lores_sprite, and fix the Last Byte Bug 2023-04-17 05:15:12 -05:00
401b247c05 cpu: Remove reference to nonexistent "init" module 2023-04-17 05:13:41 -05:00
95d4751cdd bus: Major refactor: auto-impl implicit casting for all numerics 2023-04-17 05:12:37 -05:00
7d25a9f5f1 quirks.rs: Prepare screen_wrap quirk for future xochip compat 2023-04-17 05:09:16 -05:00
bafb576705 chip8-test-suite: Update to 4.0 2023-04-17 05:04:23 -05:00
50 changed files with 3417 additions and 1919 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
# Rust files # Rust files
**/target
**/Cargo.lock
/target /target
Cargo.lock Cargo.lock
flamegraph.svg flamegraph.svg

View File

@ -1,22 +1,21 @@
[package] [workspace]
name = "chirp" members = ["chirp-imgui", "chirp-minifb"]
# default-members = ["chirp-imgui"]
[workspace.package]
version = "0.1.1" version = "0.1.1"
edition = "2021" edition = "2021"
ignore = ["justfile", ".gitmodules", "chip8-test-suite", "chip8Archive"]
default-run = "chirp"
authors = ["John Breaux"] authors = ["John Breaux"]
license = "MIT" license = "MIT"
publish = false publish = false
[package]
[features] name = "chirp"
default = ["unstable", "drawille", "minifb"] version.workspace = true
unstable = [] edition.workspace = true
drawille = ["dep:drawille"] authors.workspace = true
iced = ["dep:iced"] license.workspace = true
minifb = ["dep:minifb"] publish.workspace = true
rhexdump = ["dep:rhexdump"]
serde = ["dep:serde"]
[[bin]] [[bin]]
name = "chirp" name = "chirp"
@ -27,14 +26,17 @@ required-features = ["minifb"]
name = "chirp-disasm" name = "chirp-disasm"
required-features = ["default"] required-features = ["default"]
[[bin]]
name = "chirp-iced"
required-features = ["iced"]
[[bin]] [[bin]]
name = "chirp-shot-viewer" name = "chirp-shot-viewer"
required-features = ["default", "drawille"] required-features = ["default", "drawille"]
[features]
default = ["drawille", "serde"]
nightly = []
drawille = ["dep:drawille"]
minifb = ["dep:minifb"]
rhexdump = ["dep:rhexdump"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
@ -49,14 +51,14 @@ overflow-checks = false
[dependencies] [dependencies]
drawille = {version = "0.3.0", optional = true}
iced = {version = "0.8.0", optional = true}
rhexdump = {version = "^0.1.1", optional = true }
serde = { version = "^1.0", features = ["derive"], optional = true }
minifb = { version = "^0.24.0", optional = true }
gumdrop = "^0.8.1" drawille = { version = "0.3.0", optional = true }
rhexdump = { version = "0.1.1", optional = true }
serde = { version = "1.0.0", features = ["derive"], optional = true }
minifb = { version = "0.24.0", optional = true }
gumdrop = "0.8.1"
imperative-rs = "0.3.1" imperative-rs = "0.3.1"
owo-colors = "^3" owo-colors = "3"
rand = "^0.8.5" rand = "0.8.5"
thiserror = "^1.0.39" thiserror = "1.0.39"

@ -1 +1 @@
Subproject commit af7829841b4c9b043febbd37a4d4114e0ac948ec Subproject commit 253f5de130a6cc4c7e07c6801174717efb0ea9cb

@ -1 +1 @@
Subproject commit 483a642595b025a863f2e4304384a370098a3344 Subproject commit 6e1f4025df7c46ef5bdc222262b1e38208cf7c0f

23
chirp-imgui/Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "chirp-imgui"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
chirp = { path = ".." }
gumdrop = "0.8.1"
owo-colors = "3"
thiserror = "1.0.39"
imgui = { version = "^0.11" }
imgui-winit-support = { version = "^0.11" }
imgui-wgpu = { version = "^0.23" }
pixels = { version = "^0" }
# TODO: When imgui-winit-support updates to 0.28 (Soon:tm:), update winit and winit_input_helper
winit = { version = "0.27.5", features = ["default", "x11"] }
winit_input_helper = { version = "^0.13.0" }

52
chirp-imgui/src/args.rs Normal file
View File

@ -0,0 +1,52 @@
//! Parses arguments into a struct
use std::path::PathBuf;
use super::Emulator;
use chirp::Mode;
use gumdrop::*;
/// Parses a hexadecimal string into a u16
fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> {
u16::from_str_radix(value, 16)
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)]
pub struct Arguments {
#[options(help = "Load a ROM to run on Chirp.", required, free)]
pub file: PathBuf,
#[options(help = "Print this help message.")]
help: bool,
#[options(help = "Enable debug mode at startup.")]
pub debug: bool,
#[options(help = "Enable pause mode at startup.")]
pub pause: bool,
#[options(help = "Set the instructions-per-frame rate.")]
pub speed: Option<usize>,
// #[options(help = "Enable performance benchmarking on stderr (requires -S)")]
// pub perf: bool,
#[options(help = "Run in (Chip8, SChip, XOChip) mode.")]
pub mode: Option<Mode>,
#[options(help = "Set the target framerate.", default = "60", meta = "FR")]
pub frame_rate: u64,
}
impl Arguments {
pub fn parse() -> Arguments {
Arguments::parse_args_default_or_exit()
}
}
impl From<Arguments> for Emulator {
fn from(value: Arguments) -> Self {
let mut emu = Emulator::new(value.speed.unwrap_or(10), value.file);
if let Some(mode) = value.mode {
emu.set_quirks(mode.into());
}
if value.pause {
emu.pause()
}
emu.set_disasm(value.debug);
emu
}
}

176
chirp-imgui/src/emu.rs Normal file
View File

@ -0,0 +1,176 @@
//! The emulator's state, including the screen data
use super::{error, BACKGROUND, FOREGROUND};
use chirp::{Grab, Screen, CPU};
use pixels::Pixels;
use std::path::{Path, PathBuf};
use winit::event::VirtualKeyCode;
use winit_input_helper::WinitInputHelper;
/// The state of the application
#[derive(Debug)]
pub struct Emulator {
screen: Screen,
cpu: CPU,
pub ipf: usize,
pub rom: PathBuf,
pub colors: [[u8; 4]; 2],
}
impl Emulator {
/// Constructs a new CPU, with the provided ROM loaded at 0x0200
///
/// # Panics
/// Panics if the provided ROM does not exist
pub fn new(ipf: usize, rom: impl AsRef<Path>) -> Self {
let screen = Screen::default();
let mut cpu = CPU::default();
cpu.load_program(&rom).expect("Loaded file MUST exist.");
Self {
cpu,
ipf,
rom: rom.as_ref().into(),
screen,
colors: [*FOREGROUND, *BACKGROUND],
}
}
/// Runs a single epoch
pub fn update(&mut self) -> Result<(), error::Error> {
self.cpu.multistep(&mut self.screen, self.ipf)?;
Ok(())
}
/// Rasterizes the screen into a [Pixels] buffer
pub fn draw(&mut self, pixels: &mut Pixels) -> Result<(), error::Error> {
if let Some(screen) = self.screen.grab(..) {
let len_log2 = screen.len().ilog2() / 2;
#[allow(unused_variables)]
let (width, height) = (2u32.pow(len_log2 + 2), 2u32.pow(len_log2 + 1));
pixels.resize_buffer(width, height)?;
for (idx, pixel) in pixels.frame_mut().iter_mut().enumerate() {
let (byte, bit, component) = (idx >> 5, (idx >> 2) % 8, idx & 0b11);
*pixel = if screen[byte] & (0x80 >> bit) > 0 {
self.colors[0][component]
} else {
self.colors[1][component]
}
}
}
Ok(())
}
/// Processes keyboard input for the Emulator
pub fn input(&mut self, input: &WinitInputHelper) -> Result<(), error::Error> {
const KEYMAP: [VirtualKeyCode; 16] = [
VirtualKeyCode::X,
VirtualKeyCode::Key1,
VirtualKeyCode::Key2,
VirtualKeyCode::Key3,
VirtualKeyCode::Q,
VirtualKeyCode::W,
VirtualKeyCode::E,
VirtualKeyCode::A,
VirtualKeyCode::S,
VirtualKeyCode::D,
VirtualKeyCode::Z,
VirtualKeyCode::C,
VirtualKeyCode::Key4,
VirtualKeyCode::R,
VirtualKeyCode::F,
VirtualKeyCode::V,
];
for (id, &key) in KEYMAP.iter().enumerate() {
if input.key_released(key) {
self.cpu.release(id)?;
}
if input.key_pressed(key) {
self.cpu.press(id)?;
}
}
Ok(())
}
pub fn quirks(&mut self) -> chirp::Quirks {
self.cpu.flags.quirks
}
pub fn set_quirks(&mut self, quirks: chirp::Quirks) {
self.cpu.flags.quirks = quirks
}
/// Prints the CPU registers and cycle count to stderr
pub fn print_registers(&self) {
self.cpu.dump()
}
/// Prints the screen (using the highest resolution available printer) to stdout
pub fn print_screen(&self) -> Result<(), error::Error> {
self.screen.print_screen();
Ok(())
}
/// Dumps the raw screen bytes to a file named `{rom}_{cycle}.bin`,
/// or, failing that, `screen_dump.bin`
pub fn dump_screen(&self) -> Result<(), error::Error> {
let mut path = PathBuf::new().join(format!(
"{}_{}.bin",
self.rom.file_stem().unwrap_or_default().to_string_lossy(),
self.cpu.cycle()
));
path.set_extension("bin");
if std::fs::write(&path, self.screen.as_slice()).is_ok() {
eprintln!("Saved to {}", &path.display());
} else if std::fs::write("screen_dump.bin", self.screen.as_slice()).is_ok() {
eprintln!("Saved to screen_dump.bin");
} else {
eprintln!("Failed to dump screen to file.")
}
Ok(())
}
/// Sets live disassembly
pub fn set_disasm(&mut self, enabled: bool) {
self.cpu.flags.debug = enabled;
}
/// Checks live disassembly
pub fn is_disasm(&mut self) {
eprintln!(
"Live Disassembly {}abled",
if self.cpu.flags.debug { "En" } else { "Dis" }
);
}
/// Toggles emulator pause
pub fn pause(&mut self) {
self.cpu.flags.pause();
eprintln!("{}aused", if self.cpu.flags.pause { "P" } else { "Unp" });
}
/// Single-steps the emulator, pausing afterward
pub fn singlestep(&mut self) -> Result<(), error::Error> {
self.cpu.singlestep(&mut self.screen)?;
Ok(())
}
/// Sets a breakpoint at the current address
pub fn set_break(&mut self) {
self.cpu.set_break(self.cpu.pc());
eprintln!("Set breakpoint at {}", self.cpu.pc());
}
/// Unsets a breakpoint at the current address
pub fn unset_break(&mut self) {
self.cpu.unset_break(self.cpu.pc());
eprintln!("Unset breakpoint at {}", self.cpu.pc());
}
/// Soft-resets the CPU, keeping the program in memory
pub fn soft_reset(&mut self) {
self.cpu.reset();
self.screen.clear();
eprintln!("Soft Reset");
}
/// Creates a new CPU with the current CPU's flags
pub fn hard_reset(&mut self) {
self.cpu.reset();
self.screen.clear();
// keep the flags
let flags = self.cpu.flags.clone();
// instantiate a completely new CPU, and reload the ROM from disk
self.cpu = CPU::default();
self.cpu.flags = flags;
self.cpu
.load_program(&self.rom)
.expect("Previously loaded ROM no longer exists (was it moved?)");
eprintln!("Hard Reset");
}
}

28
chirp-imgui/src/error.rs Normal file
View File

@ -0,0 +1,28 @@
//! Error type for chirp-imgui
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
/// Error originated in [`chirp`]
#[error(transparent)]
Chirp(#[from] chirp::error::Error),
/// Error originated in [`std::io`]
#[error(transparent)]
Io(#[from] std::io::Error),
/// Error originated in [`pixels`]
#[error(transparent)]
Pixels(#[from] pixels::Error),
/// Error originated in [`pixels`]
#[error(transparent)]
PixelsTexture(#[from] pixels::TextureError),
/// Error originated from [`winit::error::OsError`]
#[error(transparent)]
WinitOs(#[from] winit::error::OsError),
/// Error originated from [`winit::error::ExternalError`]
#[error(transparent)]
WinitExternal(#[from] winit::error::ExternalError),
/// Error originated from [`winit::error::NotSupportedError`]
#[error(transparent)]
WinitNotSupported(#[from] winit::error::NotSupportedError),
}

194
chirp-imgui/src/gui.rs Normal file
View File

@ -0,0 +1,194 @@
//! Represents the Dear Imgui
//!
//! Adapted from the [ImGui-winit Example]
//!
//! [ImGui-winit Example]: https://github.com/parasyte/pixels/blob/main/examples/imgui-winit/src/gui.rs
use pixels::{wgpu, PixelsContext};
use std::time::Instant;
mod about;
mod menubar;
use menubar::Menubar;
/// Lays out the imgui widgets for a thing
pub trait Drawable {
// Lay out the ImGui widgets for this thing
fn draw(&mut self, ui: &imgui::Ui);
}
/// Holds state of GUI
pub(crate) struct Gui {
imgui: imgui::Context,
platform: imgui_winit_support::WinitPlatform,
renderer: imgui_wgpu::Renderer,
last_frame: Instant,
last_cursor: Option<imgui::MouseCursor>,
pub menubar: Menubar,
}
/// Queries the state of the [Gui]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Wants {
Quit,
Disasm,
SoftReset,
HardReset,
Reset,
}
impl std::fmt::Debug for Gui {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Gui")
.field("imgui", &self.imgui)
.field("platform", &self.platform)
.field("last_frame", &self.last_frame)
.field("last_cursor", &self.last_cursor)
.field("menubar", &self.menubar)
.finish_non_exhaustive()
}
}
impl Gui {
pub fn new(window: &winit::window::Window, pixels: &pixels::Pixels) -> Self {
// Create Dear Imgui context
let mut imgui = imgui::Context::create();
imgui.set_ini_filename(None);
// winit init
let mut platform = imgui_winit_support::WinitPlatform::init(&mut imgui);
platform.attach_window(
imgui.io_mut(),
window,
imgui_winit_support::HiDpiMode::Locked(2.2),
);
// Configure fonts
let dpi_scale = window.scale_factor();
let font_size = (13.0 * dpi_scale) as f32;
imgui.io_mut().font_global_scale = (1.0 / dpi_scale) as f32;
imgui
.fonts()
.add_font(&[imgui::FontSource::DefaultFontData {
config: Some(imgui::FontConfig {
size_pixels: font_size,
oversample_h: 2,
oversample_v: 2,
pixel_snap_h: true,
..Default::default()
}),
}]);
// Create WGPU renderer
let renderer = imgui_wgpu::Renderer::new(
&mut imgui,
pixels.device(),
pixels.queue(),
imgui_wgpu::RendererConfig {
texture_format: pixels.render_texture_format(),
..Default::default()
},
);
// Return Gui context
Self {
imgui,
platform,
renderer,
last_frame: Instant::now(),
last_cursor: None,
menubar: Default::default(),
}
}
/// Prepare Dear ImGui.
pub(crate) fn prepare(
&mut self,
window: &winit::window::Window,
) -> Result<(), winit::error::ExternalError> {
// Prepare Dear ImGui
let now = Instant::now();
self.imgui.io_mut().update_delta_time(now - self.last_frame);
self.last_frame = now;
self.platform.prepare_frame(self.imgui.io_mut(), window)
}
pub(crate) fn render(
&mut self,
window: &winit::window::Window,
encoder: &mut wgpu::CommandEncoder,
render_target: &wgpu::TextureView,
context: &PixelsContext,
) -> imgui_wgpu::RendererResult<()> {
// Start a new Dear ImGui frame and update the cursor
let ui = self.imgui.new_frame();
let mouse_cursor = ui.mouse_cursor();
if self.last_cursor != mouse_cursor {
self.last_cursor = mouse_cursor;
self.platform.prepare_render(ui, window);
}
self.menubar.draw(ui);
// Draw windows and GUI elements here
if self.menubar.about.about_open {
ui.open_popup("About");
self.menubar.about.about_open = false;
}
// Render Dear ImGui with WGPU
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("imgui"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: render_target,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: true,
},
})],
depth_stencil_attachment: None,
});
self.renderer.render(
self.imgui.render(),
&context.queue,
&context.device,
&mut rpass,
)
}
/// Handle any outstanding events
pub fn handle_event(
&mut self,
window: &winit::window::Window,
event: &winit::event::Event<()>,
) {
self.platform
.handle_event(self.imgui.io_mut(), window, event);
}
/// Shows or hides the [Menubar]. If `visible` is [None], toggles visibility.
pub fn show_menubar(&mut self, visible: Option<bool>) {
match visible {
Some(visible) => self.menubar.active = visible,
None => self.menubar.active ^= true,
}
}
/// Query the state of the Gui through a unified interface
pub fn wants(&mut self, wants: Wants) -> bool {
match wants {
Wants::Quit => self.menubar.file.quit,
Wants::Disasm => self.menubar.debug.dis,
Wants::Reset => {
let reset = self.menubar.debug.reset;
self.menubar.debug.reset = false;
reset
}
_ => unreachable!(),
}
}
}

View File

@ -0,0 +1,28 @@
//! Information about the crate at compile time
use imgui::Ui;
/// Project authors
const AUTHORS: Option<&str> = option_env!("CARGO_PKG_AUTHORS");
/// Compiled binary name
const BIN: Option<&str> = option_env!("CARGO_BIN_NAME");
/// Crate description
const DESCRIPTION: Option<&str> = option_env!("CARGO_PKG_DESCRIPTION");
/// Crate name
const NAME: Option<&str> = option_env!("CARGO_PKG_NAME");
/// Crate repository
const REPO: Option<&str> = option_env!("CARGO_PKG_REPOSITORY");
/// Crate version
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
pub(crate) fn about(ui: &Ui) {
ui.popup("About", || {
ui.text(format!(
"{} v{}",
NAME.unwrap_or("Chirp"),
VERSION.unwrap_or("None"),
));
ui.text(format!("Crafted by: {}", AUTHORS.unwrap_or("some people")));
ui.text(REPO.unwrap_or("Repo unavailable"));
});
}

View File

@ -0,0 +1,164 @@
//! The menubar that shows at the top of the screen
use super::Drawable;
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct Menubar {
pub(super) active: bool,
pub file: File,
pub settings: Settings,
pub debug: Debug,
pub about: Help,
}
impl Drawable for Menubar {
fn draw(&mut self, ui: &imgui::Ui) {
if self.active {
ui.main_menu_bar(|| {
self.file.draw(ui);
self.settings.draw(ui);
self.debug.draw(ui);
self.about.draw(ui);
})
}
}
}
impl Default for Menubar {
fn default() -> Self {
Self {
active: true,
file: Default::default(),
settings: Default::default(),
debug: Default::default(),
about: Default::default(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct File {
pub(super) reset: bool,
pub(super) quit: bool,
}
impl Drawable for File {
fn draw(&mut self, ui: &imgui::Ui) {
ui.menu("File", || {
self.reset = ui.menu_item("Reset");
self.quit = ui.menu_item("Quit");
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Debug {
pub(super) reset: bool,
pub(super) dis: bool,
}
impl Drawable for Debug {
fn draw(&mut self, ui: &imgui::Ui) {
ui.menu("Debug", || {
self.reset = ui.menu_item("Reset");
ui.checkbox("Live Disassembly", &mut self.dis);
})
}
}
impl Debug {
pub fn reset(&self) -> bool {
self.reset
}
pub fn dis(&self) -> bool {
self.dis
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Help {
pub(super) about_open: bool,
}
impl Drawable for Help {
fn draw(&mut self, ui: &imgui::Ui) {
ui.menu("Help", || self.about_open = ui.menu_item("About..."))
}
}
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
pub struct Settings {
pub(super) target_ipf: usize,
pub(super) quirks: chirp::Quirks,
pub(super) mode_index: usize,
pub(super) colors: [[f32; 4]; 2],
pub(super) applied: bool,
}
impl Drawable for Settings {
fn draw(&mut self, ui: &imgui::Ui) {
self.applied = false;
ui.menu("Settings", || {
use chirp::Mode::*;
ui.menu("Foreground Color", || {
self.applied |= ui.color_picker4("", &mut self.colors[0])
});
ui.menu("Background Color", || {
self.applied |= ui.color_picker4("", &mut self.colors[1])
});
const MODES: [chirp::Mode; 3] = [Chip8, SChip, XOChip];
if ui.combo_simple_string("Mode", &mut self.mode_index, &MODES) {
self.quirks = MODES[self.mode_index].into();
self.applied |= true;
}
self.applied |= {
ui.input_scalar("IPF", &mut self.target_ipf)
.chars_decimal(true)
.build()
| ui.checkbox("Bin-ops don't clear vF", &mut self.quirks.bin_ops)
| ui.checkbox("DMA doesn't modify I", &mut self.quirks.dma_inc)
| ui.checkbox("Draw calls are instant", &mut self.quirks.draw_wait)
| ui.checkbox("Screen wraps at edge", &mut self.quirks.screen_wrap)
| ui.checkbox("Shift ops ignore vY", &mut self.quirks.shift)
| ui.checkbox("Jumps behave eratically", &mut self.quirks.stupid_jumps)
};
})
}
}
impl Settings {
pub fn target_ipf(&mut self) -> &mut usize {
&mut self.target_ipf
}
pub fn quirks(&mut self) -> &mut chirp::Quirks {
&mut self.quirks
}
pub fn set_mode(&mut self, mode: chirp::Mode) {
self.mode_index = mode as usize;
}
pub fn set_color(&mut self, fg: &[u8; 4], bg: &[u8; 4]) {
for (idx, component) in fg.iter().enumerate() {
self.colors[0][idx] = *component as f32 / 255.0;
}
for (idx, component) in bg.iter().enumerate() {
self.colors[1][idx] = *component as f32 / 255.0;
}
}
pub fn applied(&mut self) -> Option<(usize, chirp::Quirks, [u8; 4], [u8; 4])> {
let (fg, bg) = (self.colors[0], self.colors[1]);
let fg = [
(fg[0] * 255.0) as u8,
(fg[1] * 255.0) as u8,
(fg[2] * 255.0) as u8,
(fg[3] * 255.0) as u8,
];
let bg = [
(bg[0] * 255.0) as u8,
(bg[1] * 255.0) as u8,
(bg[2] * 255.0) as u8,
(bg[3] * 255.0) as u8,
];
self.applied
.then_some((self.target_ipf, self.quirks, fg, bg))
}
}

205
chirp-imgui/src/main.rs Normal file
View File

@ -0,0 +1,205 @@
#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![allow(dead_code)] // TODO: finish writing the code
mod args;
mod emu;
mod error;
mod gui;
use crate::args::Arguments;
use crate::emu::*;
use crate::gui::*;
use owo_colors::OwoColorize;
use pixels::{Pixels, SurfaceTexture};
use std::result::Result;
use winit::dpi::LogicalSize;
use winit::event::{Event, VirtualKeyCode};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
use winit_input_helper::WinitInputHelper;
// TODO: Make these configurable in the frontend
const FOREGROUND: &[u8; 4] = &0xFFFF00FF_u32.to_be_bytes();
const BACKGROUND: &[u8; 4] = &0x623701FF_u32.to_be_bytes();
const INIT_SPEED: usize = 10;
struct Application {
gui: Gui,
emu: Emulator,
}
fn main() -> Result<(), error::Error> {
let args = Arguments::parse();
// Make sure the ROM file exists
if !args.file.is_file() {
eprintln!(
"{} not found. If the file exists, you might not have permission you access it.",
args.file.display().italic().red()
);
return Ok(());
}
let event_loop = EventLoop::new();
let mut input = WinitInputHelper::new();
let size = LogicalSize::new(128 * 8, 64 * 8);
let window = WindowBuilder::new()
.with_title("Chirp")
.with_inner_size(size)
.with_always_on_top(true)
.build(&event_loop)?;
let mut scale_factor = window.scale_factor();
let mut pixels = {
let window_size = window.inner_size();
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
Pixels::new(128, 64, surface_texture)?
};
let mut gui = Gui::new(&window, &pixels);
// set initial parameters
if let Some(mode) = args.mode {
gui.menubar.settings.set_mode(mode);
}
gui.menubar.settings.set_color(FOREGROUND, BACKGROUND);
*gui.menubar.settings.target_ipf() = args.speed.unwrap_or(INIT_SPEED);
let mut app = Application::new(args.into(), gui);
// Copy quirks from the running Emulator, for consistency
*app.gui.menubar.settings.quirks() = app.emu.quirks();
// Run event loop
event_loop.run(move |event, _, control_flow| {
let toggle_fullscreen = || {
window.set_fullscreen(if window.fullscreen().is_some() {
None
} else {
Some(winit::window::Fullscreen::Borderless(None))
})
};
let redraw = |gui: &mut Gui, pixels: &mut Pixels| -> Result<(), error::Error> {
// Prepare gui for redraw
gui.prepare(&window)?;
// Render everything together
pixels.render_with(|encoder, render_target, context| {
// Render the emulator's screen
context.scaling_renderer.render(encoder, render_target);
// Render Dear ImGui
gui.render(&window, encoder, render_target, context)?;
Ok(())
})?;
Ok(())
};
let mut handle_events = |state: &mut Application,
pixels: &mut Pixels,
control_flow: &mut ControlFlow|
-> Result<(), error::Error> {
state.gui.handle_event(&window, &event);
if input.update(&event) {
use VirtualKeyCode::*;
match_pressed!( match input {
Escape | input.quit() | state.gui.wants(Wants::Quit) => {
*control_flow = ControlFlow::Exit;
return Ok(());
},
F1 => state.emu.print_registers(),
F2 => state.emu.print_screen()?,
F3 => state.emu.dump_screen()?,
F4 => state.emu.is_disasm(),
F5 => state.emu.pause(),
F6 => state.emu.singlestep()?,
F7 => state.emu.set_break(),
F8 => state.emu.unset_break(),
Delete => state.emu.soft_reset(),
Insert => state.emu.hard_reset(),
F11 => toggle_fullscreen(),
LAlt => state.gui.show_menubar(None),
});
state.emu.input(&input)?;
// Apply settings
if let Some((ipf, quirks, fg, bg)) = state.gui.menubar.settings.applied() {
state.emu.ipf = ipf;
state.emu.set_quirks(quirks);
state.emu.colors = [fg, bg];
}
// Update the scale factor
if let Some(factor) = input.scale_factor() {
scale_factor = factor;
}
// Resize the window
if let Some(size) = input.window_resized() {
if size.width > 0 && size.height > 0 {
// Resize the surface texture
pixels.resize_surface(size.width, size.height)?;
// Resize the world
let LogicalSize { width, height } = size.to_logical(scale_factor);
pixels.resize_buffer(width, height)?;
}
}
state.emu.set_disasm(state.gui.wants(Wants::Disasm));
if state.gui.wants(Wants::Reset) {
state.emu.hard_reset();
}
// Run the game loop
state.emu.update()?;
state.emu.draw(pixels)?;
// redraw the window
window.request_redraw();
}
Ok(())
};
if let Event::RedrawRequested(_) = event {
if let Err(e) = redraw(&mut app.gui, &mut pixels) {
eprintln!("{e}");
*control_flow = ControlFlow::Exit;
}
}
if let Err(e) = handle_events(&mut app, &mut pixels, control_flow) {
eprintln!("{e}");
*control_flow = ControlFlow::Exit;
}
});
}
impl Application {
fn new(emu: Emulator, gui: Gui) -> Self {
Self { gui, emu }
}
}
#[macro_export]
macro_rules! match_pressed {
(match $input:ident {$($key:path $( | $cond:expr)? => $action:expr),+ $(,)?}) => {
$(
if $input.key_pressed($key) $( || $cond )? {
$action;
}
)+
};
}
#[macro_export]
macro_rules! match_released {
(match $input:ident {$($key:path $( | $cond:expr)? => $action:expr),+ $(,)?}) => {
$(
if $input.key_released($key) $( || $cond )? {
$action;
}
)+
};
}

15
chirp-minifb/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "chirp-minifb"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chirp = { path = ".." }
minifb = { version = "0.24.0" }
gumdrop = "0.8.1"
owo-colors = "3"
thiserror = "1.0.39"

18
chirp-minifb/src/error.rs Normal file
View File

@ -0,0 +1,18 @@
//! Error type for chirp-minifb
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)]
pub enum Error {
/// Error originated in [`chirp`]
#[error(transparent)]
Chirp(#[from] chirp::error::Error),
/// Error originated in [`std::io`]
#[error(transparent)]
Io(#[from] std::io::Error),
/// Error originated in [`minifb`]
#[error(transparent)]
Minifb(#[from] minifb::Error),
}

View File

@ -1,18 +1,20 @@
// (c) 2023 John A. Breaux // (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE.txt for details) // This code is licensed under MIT license (see LICENSE for details)
//! Chirp: A chip-8 interpreter in Rust //! Chirp: A chip-8 interpreter in Rust
//! Hello, world! //! Hello, world!
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
mod error;
mod ui; mod ui;
use chirp::error::Error::BreakpointHit; use chirp::error::Error::BreakpointHit;
use chirp::{error::Result, *}; use chirp::*;
use error::Result;
use gumdrop::*; use gumdrop::*;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use std::fs::read;
use std::{ use std::{
path::PathBuf, path::PathBuf,
time::{Duration, Instant}, time::{Duration, Instant},
@ -47,7 +49,7 @@ struct Arguments {
#[options(help = "Enable pause mode at startup.")] #[options(help = "Enable pause mode at startup.")]
pub pause: bool, pub pause: bool,
#[options(help = "Set the instructions-per-delay rate, or use realtime.")] #[options(help = "Set the instructions-per-delay rate. If unspecified, use realtime.")]
pub speed: Option<usize>, pub speed: Option<usize>,
#[options(help = "Set the instructions-per-frame rate.")] #[options(help = "Set the instructions-per-frame rate.")]
pub step: Option<usize>, pub step: Option<usize>,
@ -76,16 +78,19 @@ struct Arguments {
help = "Use CHIP-48 style DMA instructions, which don't touch I." help = "Use CHIP-48 style DMA instructions, which don't touch I."
)] )]
pub memory: bool, pub memory: bool,
#[options( #[options(
short = "v", short = "v",
help = "Use CHIP-48 style bit-shifts, which don't touch vY." help = "Use CHIP-48 style bit-shifts, which don't touch vY."
)] )]
pub shift: bool, pub shift: bool,
#[options( #[options(
short = "b", short = "b",
help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]." help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]."
)] )]
pub jumping: bool, pub jumping: bool,
#[options( #[options(
long = "break", long = "break",
help = "Set breakpoints for the emulator to stop at.", help = "Set breakpoints for the emulator to stop at.",
@ -93,16 +98,23 @@ struct Arguments {
meta = "BP" meta = "BP"
)] )]
pub breakpoints: Vec<u16>, pub breakpoints: Vec<u16>,
#[options( #[options(
help = "Load additional word at address 0x1fe", help = "Load additional word at address 0x1fe",
parse(try_from_str = "parse_hex"), parse(try_from_str = "parse_hex"),
meta = "WORD" meta = "WORD"
)] )]
pub data: u16, pub data: u16,
#[options(help = "Set the target framerate.", default = "60", meta = "FR")] #[options(help = "Set the target framerate.", default = "60", meta = "FR")]
pub frame_rate: u64, pub frame_rate: u64,
} }
#[derive(Debug)]
pub struct Chip8 {
pub cpu: CPU,
pub screen: Screen,
}
#[derive(Debug)] #[derive(Debug)]
struct State { struct State {
pub speed: usize, pub speed: usize,
@ -122,31 +134,20 @@ impl State {
rate: options.frame_rate, rate: options.frame_rate,
perf: options.perf, perf: options.perf,
ch8: Chip8 { ch8: Chip8 {
bus: bus! {
// Load the charset into ROM
Charset [0x0050..0x00A0] = include_bytes!("../../mem/charset.bin"),
// Load the ROM file into RAM
Program [0x0200..0x1000] = &read(&options.file)?,
// Create a screen
Screen [0x1000..0x1100],
// Create a stack
Stack [0x0EA0..0x0F00],
},
cpu: CPU::new( cpu: CPU::new(
0x1000, Some(&options.file),
0x50, 0x50,
0x200, 0x200,
0xefe,
Dis::default(), Dis::default(),
options.breakpoints, options.breakpoints,
Flags { Flags {
quirks: options.mode.unwrap_or_default().into(), quirks: options.mode.unwrap_or_default().into(),
debug: options.debug, debug: options.debug,
pause: options.pause, pause: options.pause,
monotonic: options.speed,
..Default::default() ..Default::default()
}, },
), )?,
screen: Screen::default(),
}, },
ui: UIBuilder::new(128, 64, &options.file).build()?, ui: UIBuilder::new(128, 64, &options.file).build()?,
ft: Instant::now(), ft: Instant::now(),
@ -157,14 +158,14 @@ impl State {
state.ch8.cpu.flags.quirks.draw_wait ^= options.drawsync; state.ch8.cpu.flags.quirks.draw_wait ^= options.drawsync;
state.ch8.cpu.flags.quirks.shift ^= options.shift; state.ch8.cpu.flags.quirks.shift ^= options.shift;
state.ch8.cpu.flags.quirks.stupid_jumps ^= options.jumping; state.ch8.cpu.flags.quirks.stupid_jumps ^= options.jumping;
state.ch8.bus.write(0x1feu16, options.data); state.ch8.screen.write(0x1feu16, options.data);
Ok(state) Ok(state)
} }
fn keys(&mut self) -> Result<bool> { fn keys(&mut self) -> Result<bool> {
self.ui.keys(&mut self.ch8) self.ui.keys(&mut self.ch8)
} }
fn frame(&mut self) -> Result<bool> { fn frame(&mut self) -> Result<bool> {
self.ui.frame(&mut self.ch8) self.ui.frame(&self.ch8)
} }
fn tick_cpu(&mut self) -> Result<()> { fn tick_cpu(&mut self) -> Result<()> {
if !self.ch8.cpu.flags.pause { if !self.ch8.cpu.flags.pause {
@ -172,7 +173,7 @@ impl State {
match self.step { match self.step {
Some(ticks) => { Some(ticks) => {
let time = Instant::now(); let time = Instant::now();
self.ch8.cpu.multistep(&mut self.ch8.bus, ticks)?; self.ch8.cpu.multistep(&mut self.ch8.screen, ticks)?;
if self.perf { if self.perf {
let time = time.elapsed(); let time = time.elapsed();
let nspt = time.as_secs_f64() / ticks as f64; let nspt = time.as_secs_f64() / ticks as f64;
@ -185,16 +186,16 @@ impl State {
} }
} }
None => { None => {
self.ch8.cpu.multistep(&mut self.ch8.bus, rate)?; self.ch8.cpu.multistep(&mut self.ch8.screen, rate)?;
} }
} }
} }
Ok(()) Ok(())
} }
fn wait_for_next_frame(&mut self) { fn wait_for_next_frame(&mut self) {
let rate = 1_000_000_000 / self.rate + 1; let rate = Duration::from_nanos(1_000_000_000 / self.rate + 1);
std::thread::sleep(Duration::from_nanos(rate).saturating_sub(self.ft.elapsed())); std::thread::sleep(rate.saturating_sub(self.ft.elapsed()));
self.ft = Instant::now(); self.ft += rate;
} }
} }
@ -211,7 +212,7 @@ impl Iterator for State {
} }
// Allow breakpoint hit messages // Allow breakpoint hit messages
match self.tick_cpu() { match self.tick_cpu() {
Err(BreakpointHit { addr, next }) => { Err(error::Error::Chirp(BreakpointHit { addr, next })) => {
eprintln!("Breakpoint hit: {:3x} ({:4x})", addr, next); eprintln!("Breakpoint hit: {:3x} ({:4x})", addr, next);
} }
Err(e) => return Some(Err(e)), Err(e) => return Some(Err(e)),

View File

@ -1,6 +1,9 @@
//! Tests for chirp-minifb //! Tests for chirp-minifb
#![allow(clippy::redundant_clone)]
use super::ui::*; use super::ui::*;
use super::Chip8;
use crate::error::Result;
use chirp::*; use chirp::*;
use std::{collections::hash_map::DefaultHasher, hash::Hash}; use std::{collections::hash_map::DefaultHasher, hash::Hash};
@ -29,14 +32,14 @@ mod ui {
fn new_chip8() -> Chip8 { fn new_chip8() -> Chip8 {
Chip8 { Chip8 {
cpu: CPU::default(), cpu: CPU::default(),
bus: bus! {}, screen: Screen::default(),
} }
} }
#[test] #[test]
fn frame() -> Result<()> { fn frame() -> Result<()> {
let mut ui = UIBuilder::new(32, 64, "dummy.ch8").build()?; let mut ui = UIBuilder::new(32, 64, "dummy.ch8").build()?;
let mut ch8 = new_chip8(); let ch8 = new_chip8();
ui.frame(&mut ch8).unwrap(); ui.frame(&ch8).unwrap();
Ok(()) Ok(())
} }
#[test] #[test]

View File

@ -1,22 +1,18 @@
// (c) 2023 John A. Breaux // (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE.txt for details) // This code is licensed under MIT license (see LICENSE for details)
#![allow(missing_docs)] #![allow(missing_docs)]
//! Platform-specific IO/UI code, and some debug functionality. //! Platform-specific IO/UI code, and some debug functionality.
//! TODO: Destroy this all. //! TODO: Destroy this all.
use super::Chip8;
use crate::error::Result;
use chirp::screen::Screen;
use minifb::*;
use std::{ use std::{
ffi::OsStr,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Instant, time::Instant,
}; };
use chirp::{
bus::{Bus, Region},
error::Result,
Chip8,
};
use minifb::*;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct UIBuilder { pub struct UIBuilder {
pub width: usize, pub width: usize,
@ -81,8 +77,10 @@ pub struct FrameBufferFormat {
impl Default for FrameBufferFormat { impl Default for FrameBufferFormat {
fn default() -> Self { fn default() -> Self {
FrameBufferFormat { FrameBufferFormat {
fg: 0x0011a434, // fg: 0x0011a434,
bg: 0x001E2431, // bg: 0x001E2431,
fg: 0x00FFFF00,
bg: 0x00623701,
} }
} }
} }
@ -104,8 +102,7 @@ impl FrameBuffer {
format: Default::default(), format: Default::default(),
} }
} }
pub fn render(&mut self, window: &mut Window, bus: &Bus) -> Result<()> { pub fn render(&mut self, window: &mut Window, screen: &Screen) -> Result<()> {
if let Some(screen) = bus.get_region(Region::Screen) {
// Resizing the buffer does not unmap memory. // Resizing the buffer does not unmap memory.
// After the first use of high-res mode, this is pretty cheap // After the first use of high-res mode, this is pretty cheap
(self.width, self.height) = match screen.len() { (self.width, self.height) = match screen.len() {
@ -116,7 +113,7 @@ impl FrameBuffer {
} }
}; };
self.buffer.resize(self.width * self.height, 0); self.buffer.resize(self.width * self.height, 0);
for (idx, byte) in screen.iter().enumerate() { for (idx, byte) in screen.as_slice().iter().enumerate() {
for bit in 0..8 { for bit in 0..8 {
self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 { self.buffer[8 * idx + bit] = if byte & (1 << (7 - bit)) as u8 != 0 {
self.format.fg self.format.fg
@ -127,7 +124,6 @@ impl FrameBuffer {
} }
} }
} }
}
window.update_with_buffer(&self.buffer, self.width, self.height)?; window.update_with_buffer(&self.buffer, self.width, self.height)?;
Ok(()) Ok(())
} }
@ -149,7 +145,7 @@ pub struct UI {
} }
impl UI { impl UI {
pub fn frame(&mut self, ch8: &mut Chip8) -> Result<bool> { pub fn frame(&mut self, ch8: &Chip8) -> Result<bool> {
if ch8.cpu.flags.pause { if ch8.cpu.flags.pause {
self.window.set_title("Chirp ⏸") self.window.set_title("Chirp ⏸")
} else { } else {
@ -163,7 +159,7 @@ impl UI {
} }
self.time = Instant::now(); self.time = Instant::now();
// update framebuffer // update framebuffer
self.fb.render(&mut self.window, &ch8.bus)?; self.fb.render(&mut self.window, &ch8.screen)?;
Ok(true) Ok(true)
} }
@ -181,7 +177,6 @@ impl UI {
.into_iter() .into_iter()
.filter(|key| !self.window.get_keys().contains(key)) .filter(|key| !self.window.get_keys().contains(key))
}; };
use crate::ui::Region::*;
for key in get_keys_released() { for key in get_keys_released() {
if let Some(key) = identify_key(key) { if let Some(key) = identify_key(key) {
ch8.cpu.release(key)?; ch8.cpu.release(key)?;
@ -192,7 +187,7 @@ impl UI {
use Key::*; use Key::*;
match key { match key {
F1 | Comma => ch8.cpu.dump(), F1 | Comma => ch8.cpu.dump(),
F2 | Period => ch8.bus.print_screen()?, F2 | Period => ch8.screen.print_screen(),
F3 => { F3 => {
debug_dump_screen(ch8, &self.rom).expect("Unable to write debug screen dump"); debug_dump_screen(ch8, &self.rom).expect("Unable to write debug screen dump");
} }
@ -216,7 +211,7 @@ impl UI {
}), }),
F6 | Enter => { F6 | Enter => {
eprintln!("Step"); eprintln!("Step");
ch8.cpu.singlestep(&mut ch8.bus)?; ch8.cpu.singlestep(&mut ch8.screen)?;
} }
F7 => { F7 => {
eprintln!("Set breakpoint {:03x}.", ch8.cpu.pc()); eprintln!("Set breakpoint {:03x}.", ch8.cpu.pc());
@ -229,7 +224,7 @@ impl UI {
F9 | Delete => { F9 | Delete => {
eprintln!("Soft reset state.cpu {:03x}", ch8.cpu.pc()); eprintln!("Soft reset state.cpu {:03x}", ch8.cpu.pc());
ch8.cpu.soft_reset(); ch8.cpu.soft_reset();
ch8.bus.clear_region(Screen); ch8.screen.clear();
} }
Escape => return Ok(false), Escape => return Ok(false),
key => { key => {
@ -267,29 +262,18 @@ pub fn identify_key(key: Key) -> Option<usize> {
} }
pub fn debug_dump_screen(ch8: &Chip8, rom: &Path) -> Result<()> { pub fn debug_dump_screen(ch8: &Chip8, rom: &Path) -> Result<()> {
let path = PathBuf::new() let mut path = PathBuf::new().join(format!(
.join("src/cpu/tests/screens/") "{}_{}.bin",
.join(if rom.is_absolute() { rom.file_stem().unwrap_or_default().to_string_lossy(),
Path::new("unknown/") ch8.cpu.cycle()
} else { ));
rom.file_name().unwrap_or(OsStr::new("unknown")).as_ref() path.set_extension("bin");
}) if std::fs::write(&path, ch8.screen.as_slice()).is_ok() {
.join(format!("{}.bin", ch8.cpu.cycle()));
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()); 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.")
}
Ok(()) Ok(())
} }

View File

@ -1,3 +1,6 @@
# (c) 2023 John A. Breaux
# This code is licensed under MIT license (see LICENSE for details)
# Some common commands for working on this stuff # Some common commands for working on this stuff
# Run All Tests # Run All Tests
@ -14,7 +17,7 @@ debug rom:
cargo run -- -d '{{rom}}' cargo run -- -d '{{rom}}'
# Run at 2100000 instructions per frame, and output per-frame runtime statistics # Run at 2100000 instructions per frame, and output per-frame runtime statistics
bench: bench:
cargo run --release -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip cargo run --release --bin chirp --features="minifb" -- chip8Archive/roms/1dcell.ch8 -Ps10 -S2100000 -m xochip
flame rom: flame rom:
CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10 CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph -F 15300 --open --bin chirp-minifb -- '{{rom}}' -s10

View File

@ -3,9 +3,34 @@ use gumdrop::*;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use std::{fs::read, path::PathBuf}; use std::{fs::read, path::PathBuf};
mod tree;
fn main() -> Result<()> { fn main() -> Result<()> {
let options = Arguments::parse_args_default_or_exit(); let mut options = Arguments::parse_args_default_or_exit();
let contents = &read(&options.file)?; while let Some(file) = options.file.pop() {
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(); let disassembler = Dis::default();
for (addr, insn) in contents[options.offset..].chunks_exact(2).enumerate() { for (addr, insn) in contents[options.offset..].chunks_exact(2).enumerate() {
let insn = u16::from_be_bytes( let insn = u16::from_be_bytes(
@ -13,15 +38,14 @@ fn main() -> Result<()> {
.expect("Iterated over 2-byte chunks, got <2 bytes"), .expect("Iterated over 2-byte chunks, got <2 bytes"),
); );
println!( println!(
"{}", "{:03x}: {:04x} {}",
format_args!(
"{:03x}: {} {:04x}",
2 * addr + 0x200 + options.offset, 2 * addr + 0x200 + options.offset,
disassembler.once(insn),
insn.bright_black(), insn.bright_black(),
) disassembler.once(insn),
); );
} }
}
}
Ok(()) Ok(())
} }
@ -30,11 +54,19 @@ struct Arguments {
#[options(help = "Show help text")] #[options(help = "Show help text")]
help: bool, help: bool,
#[options(help = "Load a ROM to run on Chirp", free, required)] #[options(help = "Load a ROM to run on Chirp", free, required)]
pub file: PathBuf, pub file: Vec<PathBuf>,
#[options(help = "Load address (usually 200)", parse(try_from_str = "parse_hex"))] #[options(
help = "Load address (usually 200)",
parse(try_from_str = "parse_hex"),
default = "200"
)]
pub loadaddr: u16, pub loadaddr: u16,
#[options(help = "Start disassembling at offset...")] #[options(help = "Start disassembling at offset...")]
pub offset: usize, pub offset: usize,
#[options(help = "Print the disassembly as a tree")]
pub tree: bool,
#[options(help = "Prune unreachable instructions ")]
pub traverse: bool,
} }
fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> { fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> {

View File

@ -0,0 +1,183 @@
#![allow(dead_code)]
#![allow(unused_variables)]
use std::{
collections::{BTreeMap, HashSet},
fmt::{Display, LowerHex},
rc::{Rc, Weak},
};
use chirp::cpu::instruction::Insn;
use imperative_rs::InstructionSet;
use owo_colors::OwoColorize;
type Adr = usize;
/// Represents the kinds of control flow an instruction can take
#[derive(Clone, Debug, Default)]
pub enum DisNodeContents {
Subroutine {
insn: Insn,
jump: Rc<DisNode>,
ret: Rc<DisNode>,
},
Branch {
insn: Insn,
next: Rc<DisNode>,
jump: Rc<DisNode>,
},
Continue {
insn: Insn,
next: Rc<DisNode>,
},
End {
insn: Insn,
},
RelBranch {
insn: Insn,
},
Merge {
insn: Insn,
back: Option<Weak<DisNode>>,
},
PendingMerge {
insn: Insn,
},
#[default]
Invalid,
}
/// Represents the kinds of control flow an instruction can take
#[derive(Clone, Debug)]
pub struct DisNode {
pub contents: DisNodeContents,
pub addr: Adr,
pub depth: usize,
}
impl DisNode {
pub fn traverse(
mem: &[u8],
nodes: &mut BTreeMap<Adr, Weak<DisNode>>,
addr: Adr,
) -> Rc<DisNode> {
Self::tree_recurse(mem, &mut Default::default(), nodes, addr, 0)
}
pub fn tree_recurse(
mem: &[u8],
visited: &mut HashSet<Adr>,
nodes: &mut BTreeMap<Adr, Weak<DisNode>>,
addr: Adr,
depth: usize,
) -> Rc<DisNode> {
use DisNodeContents::*;
// Try to decode an instruction. If the instruction is invalid, fail early.
let Ok((len, insn)) = Insn::decode(&mem[addr..]) else {
return Rc::new(DisNode {
contents: Invalid,
addr,
depth,
});
};
let mut next = DisNode {
contents: {
match insn {
// instruction is already visited, but the branch isn't guaranteed to be in the tree yet
_ if !visited.insert(addr) => PendingMerge { insn },
Insn::ret | Insn::halt => End { insn },
// A branch to the current address will halt the machine
Insn::jmp { A } | Insn::call { A } if A as usize == addr => End { insn },
Insn::jmp { A } => Continue {
insn,
next: DisNode::tree_recurse(mem, visited, nodes, A as usize, depth),
},
Insn::call { A } => Branch {
insn,
jump: DisNode::tree_recurse(mem, visited, nodes, A as usize, depth + 1),
next: DisNode::tree_recurse(mem, visited, nodes, addr + len, depth),
},
// If the instruction is a skip instruction, first visit the next instruction,
// then visit the skip instruction. This preserves visitor order.
Insn::seb { .. }
| Insn::sneb { .. }
| Insn::se { .. }
| Insn::sne { .. }
| Insn::sek { .. } => Branch {
insn,
// FIXME: If the next instruction is Long I, this will just break
next: DisNode::tree_recurse(mem, visited, nodes, addr + len, depth),
jump: DisNode::tree_recurse(mem, visited, nodes, addr + len + 2, depth + 1),
},
// Relative branch prediction is out of scope right now
Insn::jmpr { .. } => RelBranch { insn },
// If the instruction is any other instruction, emit a Continue token
_ => Continue {
insn,
next: DisNode::tree_recurse(mem, visited, nodes, addr + len, depth),
},
}
},
addr,
depth,
};
// Resolve pending merges
if let PendingMerge { insn } = next.contents {
next.contents = Merge {
insn,
back: nodes.get(&addr).cloned(),
}
}
let next = Rc::new(next);
nodes.insert(addr, Rc::downgrade(&next));
next
}
}
impl Display for DisNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use DisNodeContents::*;
write!(f, "\n{:04x}: ", self.addr,)?;
for indent in 0..self.depth {
Display::fmt(&"".bright_magenta(), f)?;
}
match &self.contents {
Subroutine { insn, ret, jump } => write!(f, "{insn}{jump}{ret}"),
Branch { insn, next, jump } => write!(f, "{insn}{jump}{next}"),
Continue { insn, next } => write!(f, "{insn}{next}"),
RelBranch { insn } => Display::fmt(&insn.underline(), f),
PendingMerge { insn } | Merge { insn, .. } => write!(
f,
"{}",
format_args!("{}; ...", insn).italic().bright_black()
),
End { insn } => Display::fmt(insn, f),
Invalid => Display::fmt(&"Invalid".bold().red(), f),
}
}
}
impl LowerHex for DisNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use DisNodeContents::*;
match self.contents {
Subroutine { insn, .. }
| Branch { insn, .. }
| Continue { insn, .. }
| Merge { insn, .. }
| End { insn }
| RelBranch { insn }
| PendingMerge { insn } => {
LowerHex::fmt(&u32::from(&insn).bright_black(), f)?;
f.write_str(" ")?;
Display::fmt(&insn.cyan(), f)
}
Invalid => Display::fmt("Invalid", f),
}
}
}

View File

@ -1,26 +0,0 @@
#![allow(unused_imports)]
use chirp::*;
#[cfg(features = "iced")]
use iced::{
executor, time, window, Alignment, Application, Command, Element, Length, Settings,
Subscription,
};
#[cfg(features = "iced")]
fn main() -> iced::Result {
Ok(())
}
#[cfg(not(features = "iced"))]
fn main() -> Result<()> {
Ok(())
}
/// TODO: `impl Application for Emulator {}`
#[derive(Clone, Debug, Default, PartialEq)]
struct Emulator {
mem: Bus,
cpu: CPU,
fps: f64,
ipf: usize,
}

View File

@ -4,7 +4,7 @@ use std::{env::args, fs::read};
fn main() -> Result<()> { fn main() -> Result<()> {
for screen in args().skip(1).inspect(|screen| println!("{screen}")) { for screen in args().skip(1).inspect(|screen| println!("{screen}")) {
let screen = read(screen)?; let screen = read(screen)?;
bus! {Screen [0..screen.len()] = &screen}.print_screen()?; Screen::from(screen).print_screen();
} }
Ok(()) Ok(())
} }

View File

@ -1,497 +0,0 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE.txt for details)
//! The Bus connects the CPU to Memory
//!
//! This is more of a memory management unit + some utils for reading/writing
use crate::error::{Error::MissingRegion, Result};
use std::{
fmt::{Debug, Display, Formatter},
ops::Range,
slice::SliceIndex,
};
/// Creates a new bus, growing the backing memory as needed
/// # Examples
/// ```rust
/// # use chirp::*;
/// let mut bus = bus! {
/// Stack [0x0000..0x0800] = b"ABCDEF",
/// Program [0x0800..0x1000] = include_bytes!("bus.rs"),
/// };
/// ```
#[macro_export]
macro_rules! bus {
($($name:path $(:)? [$range:expr] $(= $data:expr)?) ,* $(,)?) => {
$crate::bus::Bus::default()
$(
.add_region($name, $range)
$(
.load_region($name, $data)
)?
)*
};
}
// Traits Read and Write are here purely to make implementing other things more bearable
/// Read a T from address `addr`
pub trait Read<T> {
/// Read a T from address `addr`
fn read(&self, addr: impl Into<usize>) -> T;
}
/// Write "some data" to the Bus
pub trait Write<T> {
/// Write a T to address `addr`
fn write(&mut self, addr: impl Into<usize>, data: T);
}
/// Represents a named region in memory
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Region {
/// Character ROM (but writable!)
Charset,
/// Program memory
Program,
/// Screen buffer
Screen,
/// Stack space
Stack,
#[doc(hidden)]
/// Total number of named regions
Count,
}
impl Display for Region {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Region::Charset => "charset",
Region::Program => "program",
Region::Screen => "screen",
Region::Stack => "stack",
_ => "",
}
)
}
}
/// Stores memory in a series of named regions with ranges
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Bus {
memory: Vec<u8>,
region: [Option<Range<usize>>; Region::Count as usize],
}
impl Bus {
// TODO: make bus::new() give a properly set up bus with a default memory map
/// Constructs a new bus
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new();
/// assert!(bus.is_empty());
///# Ok(())
///# }
/// ```
pub fn new() -> Self {
Bus::default()
}
/// Gets the length of the bus' backing memory
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new()
/// .add_region(Program, 0..1234);
/// assert_eq!(1234, bus.len());
///# Ok(())
///# }
/// ```
pub fn len(&self) -> usize {
self.memory.len()
}
/// Returns true if the backing memory contains no elements
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new();
/// assert!(bus.is_empty());
///# Ok(())
///# }
/// ```
pub fn is_empty(&self) -> bool {
self.memory.is_empty()
}
/// Grows the Bus backing memory to at least size bytes, but does not truncate
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let mut bus = Bus::new();
/// bus.with_size(1234);
/// assert_eq!(1234, bus.len());
/// bus.with_size(0);
/// assert_eq!(1234, bus.len());
///# Ok(())
///# }
/// ```
pub fn with_size(&mut self, size: usize) {
if self.len() < size {
self.memory.resize(size, 0);
}
}
/// Adds a new named range (Region) to the bus
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new().add_region(Program, 0..1234);
/// assert_eq!(1234, bus.len());
///# Ok(())
///# }
/// ```
pub fn add_region(mut self, name: Region, range: Range<usize>) -> Self {
self.with_size(range.end);
if let Some(region) = self.region.get_mut(name as usize) {
*region = Some(range);
}
self
}
/// Updates an existing named range (Region)
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new().add_region(Program, 0..1234);
/// assert_eq!(1234, bus.len());
///# Ok(())
///# }
/// ```
pub fn set_region(&mut self, name: Region, range: Range<usize>) -> &mut Self {
self.with_size(range.end);
if let Some(region) = self.region.get_mut(name as usize) {
*region = Some(range);
}
self
}
/// Loads data into a named region
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new()
/// .add_region(Program, 0..1234)
/// .load_region(Program, b"Hello, world!");
///# // TODO: Test if region actually contains "Hello, world!"
///# Ok(())
///# }
/// ```
pub fn load_region(mut self, name: Region, data: &[u8]) -> Self {
use std::io::Write;
if let Some(mut region) = self.get_region_mut(name) {
region.write(data).ok(); // TODO: THIS SUCKS
}
self
}
/// Fills a named region with zeroes
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new()
/// .add_region(Program, 0..1234)
/// .clear_region(Program);
///# // TODO: test if region actually clear
///# Ok(())
///# }
/// ```
/// If the region doesn't exist, that's okay.
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new()
/// .add_region(Program, 0..1234)
/// .clear_region(Screen);
///# // TODO: test if region actually clear
///# Ok(())
///# }
/// ```
pub fn clear_region(&mut self, name: Region) -> &mut Self {
if let Some(region) = self.get_region_mut(name) {
region.fill(0)
}
self
}
/// Gets a slice of bus memory
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new()
/// .add_region(Program, 0..10);
/// assert!([0;10].as_slice() == bus.get(0..10).unwrap());
///# Ok(())
///# }
/// ```
pub fn get<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
where
I: SliceIndex<[u8]>,
{
self.memory.get(index)
}
/// Gets a mutable slice of bus memory
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let mut bus = Bus::new()
/// .add_region(Program, 0..10);
/// assert!([0;10].as_slice() == bus.get_mut(0..10).unwrap());
///# Ok(())
///# }
/// ```
pub fn get_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[u8]>>::Output>
where
I: SliceIndex<[u8]>,
{
self.memory.get_mut(index)
}
/// Gets a slice of a named region of memory
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new()
/// .add_region(Program, 0..10);
/// assert!([0;10].as_slice() == bus.get_region(Program).unwrap());
///# Ok(())
///# }
/// ```
pub fn get_region(&self, name: Region) -> Option<&[u8]> {
self.get(self.region.get(name as usize)?.clone()?)
}
/// Gets a mutable slice of a named region of memory
/// # Examples
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let mut bus = Bus::new()
/// .add_region(Program, 0..10);
/// assert!([0;10].as_slice() == bus.get_region_mut(Program).unwrap());
///# Ok(())
///# }
/// ```
pub fn get_region_mut(&mut self, name: Region) -> Option<&mut [u8]> {
self.get_mut(self.region.get(name as usize)?.clone()?)
}
/// Prints the region of memory called `Screen` at 1bpp using box characters
/// # Examples
///
/// [Bus::print_screen] will print the screen
/// ```rust
///# use chirp::*;
///# fn main() -> Result<()> {
/// let bus = Bus::new()
/// .add_region(Screen, 0x000..0x100);
/// bus.print_screen()?;
///# Ok(())
///# }
/// ```
/// If there is no Screen region, it will return Err([MissingRegion])
/// ```rust,should_panic
///# use chirp::*;
///# fn main() -> Result<()> {
/// let mut bus = Bus::new()
/// .add_region(Program, 0..10);
/// bus.print_screen()?;
///# Ok(())
///# }
/// ```
pub fn print_screen(&self) -> Result<()> {
const REGION: Region = Region::Screen;
if let Some(screen) = self.get_region(REGION) {
let len_log2 = screen.len().ilog2() / 2;
#[allow(unused_variables)]
let (width, height) = (2u32.pow(len_log2 - 1), 2u32.pow(len_log2));
// draw with the drawille library, if available
#[cfg(feature = "drawille")]
{
use drawille::Canvas;
let mut canvas = Canvas::new(width * 8, height);
let width = width * 8;
screen
.iter()
.enumerate()
.flat_map(|(bytei, byte)| {
(0..8)
.into_iter()
.enumerate()
.filter_map(move |(biti, bit)| {
if (byte << bit) & 0x80 != 0 {
Some(bytei * 8 + biti)
} else {
None
}
})
})
.for_each(|index| canvas.set(index as u32 % (width), index as u32 / (width)));
println!("{}", canvas.frame());
}
#[cfg(not(feature = "drawille"))]
for (index, byte) in screen.iter().enumerate() {
if index % width as usize == 0 {
print!("{index:03x}|");
}
print!(
"{}",
format!("{byte:08b}").replace('0', " ").replace('1', "")
);
if index % width as usize == width as usize - 1 {
println!("|");
}
}
} else {
return Err(MissingRegion { region: REGION });
}
Ok(())
}
}
impl Read<u8> for Bus {
/// Read a u8 from address `addr`
fn read(&self, addr: impl Into<usize>) -> u8 {
let addr: usize = addr.into();
*self.memory.get(addr).unwrap_or(&0xc5)
}
}
impl Read<u16> for Bus {
/// Read a u16 from address `addr`
fn read(&self, addr: impl Into<usize>) -> u16 {
let addr: usize = addr.into();
if let Some(bytes) = self.memory.get(addr..addr + 2) {
u16::from_be_bytes(bytes.try_into().expect("Should get 2 bytes"))
} else {
0xc5c5
}
}
}
impl Read<u32> for Bus {
/// Read a u16 from address `addr`
fn read(&self, addr: impl Into<usize>) -> u32 {
let addr: usize = addr.into();
if let Some(bytes) = self.memory.get(addr..addr + 4) {
u32::from_be_bytes(bytes.try_into().expect("Should get 4 bytes"))
} else {
0xc5c5
}
}
}
impl Read<u64> for Bus {
/// Read a u16 from address `addr`
fn read(&self, addr: impl Into<usize>) -> u64 {
let addr: usize = addr.into();
if let Some(bytes) = self.memory.get(addr..addr + 8) {
u64::from_be_bytes(bytes.try_into().expect("Should get 8 bytes"))
} else {
0xc5c5
}
}
}
impl Read<u128> for Bus {
/// Read a u16 from address `addr`
fn read(&self, addr: impl Into<usize>) -> u128 {
let addr: usize = addr.into();
if let Some(bytes) = self.memory.get(addr..addr + 16) {
u128::from_be_bytes(bytes.try_into().expect("Should get 16 bytes"))
} else {
0xc5c5
}
}
}
impl Write<u8> for Bus {
/// Write a u8 to address `addr`
fn write(&mut self, addr: impl Into<usize>, data: u8) {
let addr: usize = addr.into();
if let Some(byte) = self.get_mut(addr) {
*byte = data;
}
}
}
impl Write<u16> for Bus {
/// Write a u16 to address `addr`
fn write(&mut self, addr: impl Into<usize>, data: u16) {
let addr: usize = addr.into();
if let Some(slice) = self.get_mut(addr..addr + 2) {
data.to_be_bytes().as_mut().swap_with_slice(slice);
}
}
}
impl Write<u32> for Bus {
/// Write a u16 to address `addr`
fn write(&mut self, addr: impl Into<usize>, data: u32) {
let addr: usize = addr.into();
if let Some(slice) = self.get_mut(addr..addr + 4) {
data.to_be_bytes().as_mut().swap_with_slice(slice);
}
}
}
impl Write<u64> for Bus {
/// Write a u16 to address `addr`
fn write(&mut self, addr: impl Into<usize>, data: u64) {
let addr: usize = addr.into();
if let Some(slice) = self.get_mut(addr..addr + 8) {
data.to_be_bytes().as_mut().swap_with_slice(slice);
}
}
}
impl Write<u128> for Bus {
/// Write a u16 to address `addr`
fn write(&mut self, addr: impl Into<usize>, data: u128) {
let addr: usize = addr.into();
if let Some(slice) = self.get_mut(addr..addr + 16) {
data.to_be_bytes().as_mut().swap_with_slice(slice);
}
}
}
#[cfg(target_feature = "rhexdump")]
impl Display for Bus {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
use rhexdump::Rhexdump;
let mut rhx = Rhexdump::default();
rhx.set_bytes_per_group(2)
.expect("2 <= MAX_BYTES_PER_GROUP (8)");
rhx.display_duplicate_lines(false);
for (&name, range) in &self.region {
writeln!(
f,
"[{name}]\n{}\n",
rhx.hexdump(&self.memory[range.clone()])
)?
}
write!(f, "")
}
}

View File

@ -1,114 +1,141 @@
// (c) 2023 John A. Breaux // (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE.txt for details) // This code is licensed under MIT license (see LICENSE for details)
//! Decodes and runs instructions //! Decodes and runs instructions
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
pub mod disassembler; mod behavior;
pub mod flags; pub mod flags;
pub mod instruction; pub mod instruction;
#[macro_use]
pub mod mem;
pub mod mode; pub mod mode;
pub mod quirks; pub mod quirks;
use self::{ use self::{
disassembler::{Dis, Disassembler, Insn},
flags::Flags, flags::Flags,
instruction::{
disassembler::{Dis, Disassembler},
Insn,
},
mem::{Mem, Region::*},
mode::Mode, mode::Mode,
quirks::Quirks, quirks::Quirks,
}; };
use crate::{ use crate::{
bus::{Bus, Read, Region, Write},
error::{Error, Result}, error::{Error, Result},
screen::Screen,
traits::{AutoCast, Grab},
}; };
use imperative_rs::InstructionSet; use imperative_rs::InstructionSet;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use rand::random; use std::fmt::Debug;
use std::time::Instant;
type Reg = usize; type Reg = usize;
type Adr = u16; type Adr = u16;
type Nib = u8; type Nib = u8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Timers {
frame: Instant,
insn: Instant,
}
impl Default for Timers {
fn default() -> Self {
let now = Instant::now();
Self {
frame: now,
insn: now,
}
}
}
/// Represents the internal state of the CPU interpreter /// Represents the internal state of the CPU interpreter
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CPU { pub struct CPU {
/// Flags that control how the CPU behaves, but which aren't inherent to the /// Flags that control how the CPU behaves, but which aren't inherent to the
/// chip-8. Includes [Quirks], target IPF, etc. /// chip-8. Includes [Quirks], target IPF, etc.
pub flags: Flags, pub flags: Flags,
// memory map info // memory map info
screen: Adr, mem: Mem,
font: Adr, font: Adr,
// memory
stack: Vec<Adr>,
// registers // registers
pc: Adr, pc: Adr,
sp: Adr,
i: Adr, i: Adr,
v: [u8; 16], v: [u8; 16],
delay: f64, delay: u8,
sound: f64, sound: u8,
// I/O // I/O
keys: [bool; 16], keys: [bool; 16],
/// Set to the last key that's been *released* after a keypause
pub lastkey: Option<usize>,
// Execution data // Execution data
timers: Timers,
cycle: usize, cycle: usize,
breakpoints: Vec<Adr>, breakpoints: Vec<Adr>,
#[cfg_attr(feature = "serde", serde(skip))]
disassembler: Dis, disassembler: Dis,
} }
// public interface // public interface
impl CPU { impl CPU {
// TODO: implement From<&bus> for CPU
/// Constructs a new CPU, taking all configurable parameters /// Constructs a new CPU, taking all configurable parameters
/// # Examples /// # Examples
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let cpu = CPU::new( /// let cpu = CPU::new(
/// 0xf00, // screen location /// None::<&str>, // ROM to load
/// 0x50, // font location /// 0x50, // font location
/// 0x200, // start of program /// 0x200, // start of program
/// 0xefe, // top of stack
/// Dis::default(), /// Dis::default(),
/// vec![], // Breakpoints /// vec![], // Breakpoints
/// ControlFlags::default() /// Flags::default()
/// ); /// );
/// dbg!(cpu); /// dbg!(cpu);
/// ``` /// ```
pub fn new( pub fn new(
screen: Adr, rom: Option<impl AsRef<std::path::Path>>,
font: Adr, font: Adr,
pc: Adr, pc: Adr,
sp: Adr,
disassembler: Dis, disassembler: Dis,
breakpoints: Vec<Adr>, breakpoints: Vec<Adr>,
flags: Flags, flags: Flags,
) -> Self { ) -> Result<Self> {
CPU { const CHARSET: &[u8] = include_bytes!("mem/charset.bin");
let mem = mem! {
Charset [font as usize..font as usize + CHARSET.len()] = CHARSET,
Program [pc as usize..0x1000],
};
let mut cpu = CPU {
disassembler, disassembler,
screen,
font, font,
pc, pc,
sp,
breakpoints, breakpoints,
flags, flags,
mem,
..Default::default() ..Default::default()
};
// load the provided rom
if let Some(rom) = rom {
cpu.load_program(rom)?;
} }
Ok(cpu)
}
/// Loads a program into the CPU's program space
pub fn load_program(&mut self, rom: impl AsRef<std::path::Path>) -> Result<&mut Self> {
self.load_program_bytes(&std::fs::read(rom)?)
}
/// Loads bytes into the CPU's program space
pub fn load_program_bytes(&mut self, rom: &[u8]) -> Result<&mut Self> {
self.mem.clear_region(Program);
self.mem.load_region(Program, rom)?;
Ok(self)
}
/// Pokes a value into memory
pub fn poke(&mut self, addr: Adr, data: u8) {
self.mem.write(addr, data)
}
/// Peeks a value from memory
pub fn peek(&mut self, addr: Adr) -> u8 {
self.mem.read(addr)
}
/// Grabs a reference to the [CPU]'s memory
pub fn introspect(&mut self) -> &Mem {
&self.mem
} }
/// Presses a key, and reports whether the key's state changed. /// Presses a key, and reports whether the key's state changed.
@ -143,8 +170,8 @@ impl CPU {
/// Releases a key, and reports whether the key's state changed. /// Releases a key, and reports whether the key's state changed.
/// If key is outside range `0..=0xF`, returns [Error::InvalidKey]. /// If key is outside range `0..=0xF`, returns [Error::InvalidKey].
/// ///
/// If [ControlFlags::keypause] was enabled, it is disabled, /// If [Flags::keypause] was enabled, it is disabled,
/// and the [ControlFlags::lastkey] is recorded. /// and the lastkey is recorded.
/// # Examples /// # Examples
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
@ -163,7 +190,7 @@ impl CPU {
if *keyref { if *keyref {
*keyref = false; *keyref = false;
if self.flags.keypause { if self.flags.keypause {
self.flags.lastkey = Some(key); self.lastkey = Some(key);
self.flags.keypause = false; self.flags.keypause = false;
} }
return Ok(true); return Ok(true);
@ -240,7 +267,7 @@ impl CPU {
/// assert_eq!(0, cpu.sound()); /// assert_eq!(0, cpu.sound());
/// ``` /// ```
pub fn sound(&self) -> u8 { pub fn sound(&self) -> u8 {
self.sound as u8 self.sound
} }
/// Gets the value in the Delay Timer register /// Gets the value in the Delay Timer register
@ -251,7 +278,7 @@ impl CPU {
/// assert_eq!(0, cpu.delay()); /// assert_eq!(0, cpu.delay());
/// ``` /// ```
pub fn delay(&self) -> u8 { pub fn delay(&self) -> u8 {
self.delay as u8 self.delay
} }
/// Gets the number of cycles the CPU has executed /// Gets the number of cycles the CPU has executed
@ -274,14 +301,13 @@ impl CPU {
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let mut cpu = CPU::new( /// let mut cpu = CPU::new(
/// 0xf00, /// None::<&str>,
/// 0x50, /// 0x50,
/// 0x340, /// 0x340,
/// 0xefe,
/// Dis::default(), /// Dis::default(),
/// vec![], /// vec![],
/// ControlFlags::default() /// Flags::default()
/// ); /// ).unwrap();
/// cpu.flags.keypause = true; /// cpu.flags.keypause = true;
/// cpu.flags.draw_wait = true; /// cpu.flags.draw_wait = true;
/// assert_eq!(0x340, cpu.pc()); /// assert_eq!(0x340, cpu.pc());
@ -296,6 +322,35 @@ impl CPU {
self.flags.draw_wait = false; self.flags.draw_wait = false;
} }
/// Resets the emulator.
///
/// Touches the [Flags] (keypause, draw_wait, draw_mode, and lastkey),
/// stack, pc, registers, keys, and cycle count.
///
/// Does not touch [Quirks], [Mode], [Dis], breakpoints, or memory map.
pub fn reset(&mut self) {
self.flags = Flags {
keypause: false,
draw_wait: false,
draw_mode: false,
..self.flags
};
// clear the stack
self.stack.truncate(0);
// Reset the program counter
self.pc = 0x200;
// Zero the registers
self.i = 0;
self.v = [0; 16];
self.delay = 0;
self.sound = 0;
// I/O
self.keys = [false; 16];
self.lastkey = None;
// Execution data
self.cycle = 0;
}
/// Set a breakpoint /// Set a breakpoint
// TODO: Unit test this // TODO: Unit test this
pub fn set_break(&mut self, point: Adr) -> &mut Self { pub fn set_break(&mut self, point: Adr) -> &mut Self {
@ -344,18 +399,13 @@ impl CPU {
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let mut cpu = CPU::default(); /// let mut cpu = CPU::default();
/// let mut bus = bus!{ /// let mut screen = Screen::default();
/// Program [0x0200..0x0f00] = &[ /// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
/// 0x00, 0xe0, // cls /// cpu.singlestep(&mut screen).unwrap();
/// 0x22, 0x02, // jump 0x202 (pc)
/// ],
/// Screen [0x0f00..0x1000],
/// };
/// cpu.singlestep(&mut bus).unwrap();
/// assert_eq!(0x202, cpu.pc()); /// assert_eq!(0x202, cpu.pc());
/// assert_eq!(1, cpu.cycle()); /// assert_eq!(1, cpu.cycle());
/// ``` /// ```
pub fn singlestep(&mut self, bus: &mut Bus) -> Result<&mut Self> { pub fn singlestep(&mut self, bus: &mut Screen) -> Result<&mut Self> {
self.flags.pause = false; self.flags.pause = false;
self.tick(bus)?; self.tick(bus)?;
self.flags.draw_wait = false; self.flags.draw_wait = false;
@ -370,65 +420,25 @@ impl CPU {
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let mut cpu = CPU::default(); /// let mut cpu = CPU::default();
/// let mut bus = bus!{ /// let mut screen = Screen::default();
/// Program [0x0200..0x0f00] = &[ /// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
/// 0x00, 0xe0, // cls /// cpu.multistep(&mut screen, 0x20)
/// 0x22, 0x02, // jump 0x202 (pc)
/// ],
/// Screen [0x0f00..0x1000],
/// };
/// cpu.multistep(&mut bus, 0x20)
/// .expect("The program should only have valid opcodes."); /// .expect("The program should only have valid opcodes.");
/// assert_eq!(0x202, cpu.pc()); /// assert_eq!(0x202, cpu.pc());
/// assert_eq!(0x20, cpu.cycle()); /// assert_eq!(0x20, cpu.cycle());
/// ``` /// ```
pub fn multistep(&mut self, bus: &mut Bus, steps: usize) -> Result<&mut Self> { pub fn multistep(&mut self, screen: &mut Screen, steps: usize) -> Result<&mut Self> {
//let speed = 1.0 / steps as f64;
for _ in 0..steps { for _ in 0..steps {
self.tick(bus)?; self.tick(screen)?;
self.vertical_blank(); if self.flags.is_paused() {
break;
} }
Ok(self)
} }
self.delay = self.delay.saturating_sub(1);
/// Simulates vertical blanking self.sound = self.sound.saturating_sub(1);
///
/// 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; self.flags.draw_wait = false;
} Ok(self)
if self.delay > 0.0 {
self.delay -= time;
}
if self.sound > 0.0 {
self.sound -= time;
}
self
} }
/// Executes a single instruction /// Executes a single instruction
@ -437,80 +447,69 @@ impl CPU {
/// This result contains information about the breakpoint, but can be safely ignored. /// This result contains information about the breakpoint, but can be safely ignored.
/// ///
/// Returns [Error::UnimplementedInstruction] if the instruction at `pc` is unimplemented. /// Returns [Error::UnimplementedInstruction] if the instruction at `pc` is unimplemented.
///
/// # Examples /// # Examples
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// let mut cpu = CPU::default(); /// let mut cpu = CPU::default();
/// let mut bus = bus!{ /// let mut screen = Screen::default();
/// Program [0x0200..0x0f00] = &[ /// cpu.load_program_bytes(&[0x00, 0xe0, 0x22, 0x02]);
/// 0x00, 0xe0, // cls /// cpu.tick(&mut screen)
/// 0x22, 0x02, // jump 0x202 (pc)
/// ],
/// Screen [0x0f00..0x1000],
/// };
/// cpu.tick(&mut bus)
/// .expect("0x00e0 (cls) should be a valid opcode."); /// .expect("0x00e0 (cls) should be a valid opcode.");
/// assert_eq!(0x202, cpu.pc()); /// assert_eq!(0x202, cpu.pc());
/// assert_eq!(1, cpu.cycle()); /// assert_eq!(1, cpu.cycle());
/// ``` /// ```
/// Returns [Error::UnimplementedInstruction] if the instruction is not implemented. /// Returns [Error::UnimplementedInstruction] if the instruction is not implemented.
/// ```rust /// ```rust
/// # use chirp::*; /// # use chirp::*;
/// # use chirp::error::Error; /// # use chirp::error::Error;
/// let mut cpu = CPU::default(); /// let mut cpu = CPU::default();
/// # cpu.flags.debug = true; // enable live disassembly /// # cpu.flags.debug = true; // enable live disassembly
/// # cpu.flags.monotonic = Some(8); // enable monotonic/test timing /// # cpu.flags.monotonic = true; // enable monotonic/test timing
/// let mut bus = bus!{ /// let mut bus = Screen::default();
/// Program [0x0200..0x0f00] = &[ /// cpu.load_program_bytes(&[
/// 0xff, 0xff, // invalid! /// 0xff, 0xff, // invalid!
/// 0x22, 0x02, // jump 0x202 (pc) /// 0x22, 0x02, // jump 0x202
/// ], /// ]);
/// Screen [0x0f00..0x1000],
/// };
/// dbg!(cpu.tick(&mut bus)) /// dbg!(cpu.tick(&mut bus))
/// .expect_err("Should return Error::InvalidInstruction { 0xffff }"); /// .expect_err("Should return Error::InvalidInstruction { 0xffff }");
/// ``` /// ```
pub fn tick(&mut self, bus: &mut Bus) -> Result<&mut Self> { pub fn tick(&mut self, screen: &mut Screen) -> Result<&mut Self> {
// Do nothing if paused // Do nothing if paused
if self.flags.is_paused() { if self.flags.is_paused() {
// always tick in test mode // always tick in test mode
if self.flags.monotonic.is_some() { if self.flags.monotonic {
self.cycle += 1; self.cycle += 1;
} }
return Ok(self); return Ok(self);
} }
self.cycle += 1; self.cycle += 1;
// fetch opcode // Fetch slice of memory starting at pc, for var-width opcode 0xf000_iiii
let opcode: &[u8; 2] = if let Some(slice) = bus.get(self.pc as usize..self.pc as usize + 2) let opchunk = self
{ .mem
slice .grab(self.pc as usize..)
.try_into() .ok_or(Error::InvalidAddressRange {
.expect("`slice` should be exactly 2 bytes.") range: (self.pc as usize..).into(),
} else { })?;
return Err(Error::InvalidBusRange {
range: self.pc as usize..self.pc as usize + 2,
});
};
// Print opcode disassembly: // Print opcode disassembly:
if self.flags.debug { if self.flags.debug {
println!("{:?}", self.timers.insn.elapsed().bright_black()); std::println!(
self.timers.insn = Instant::now();
std::print!(
"{:3} {:03x}: {:<36}", "{:3} {:03x}: {:<36}",
self.cycle.bright_black(), self.cycle.bright_black(),
self.pc, self.pc,
self.disassembler.once(u16::from_be_bytes(*opcode)) self.disassembler.once(self.mem.read(self.pc))
); );
} }
// decode opcode // decode opcode
if let Ok((inc, insn)) = Insn::decode(opcode) { if let Ok((inc, insn)) = Insn::decode(opchunk) {
self.pc = self.pc.wrapping_add(inc as u16); self.pc = self.pc.wrapping_add(inc as u16);
self.execute(bus, insn); self.execute(screen, insn);
} else { } else {
return Err(Error::UnimplementedInstruction { return Err(Error::UnimplementedInstruction {
word: u16::from_be_bytes(*opcode), word: self.mem.read(self.pc),
}); });
} }
@ -519,7 +518,7 @@ impl CPU {
self.flags.pause = true; self.flags.pause = true;
return Err(Error::BreakpointHit { return Err(Error::BreakpointHit {
addr: self.pc, addr: self.pc,
next: bus.read(self.pc), next: self.mem.read(self.pc),
}); });
} }
Ok(self) Ok(self)
@ -543,31 +542,52 @@ impl CPU {
/// ``` /// ```
pub fn dump(&self) { pub fn dump(&self) {
//let dumpstyle = owo_colors::Style::new().bright_black(); //let dumpstyle = owo_colors::Style::new().bright_black();
use std::fmt::Write;
std::println!( std::println!(
"PC: {:04x}, SP: {:04x}, I: {:04x}\n{}DLY: {}, SND: {}, CYC: {:6}", "PC: {:04x}, SP: {:04x}, I: {:04x}\n{}DLY: {}, SND: {}, CYC: {:6}",
self.pc, self.pc,
self.sp, self.stack.len(),
self.i, self.i,
self.v self.v
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(i, gpr)| { .fold(String::new(), |mut s, (i, gpr)| {
format!( let _ = write!(
"v{i:X}: {gpr:02x} {}", s,
"{}v{i:X}: {gpr:02x}",
match i % 4 { match i % 4 {
3 => "\n", 0 => "\n",
_ => "", _ => "",
} }
) );
}) s
.collect::<String>(), }),
self.delay as u8, self.delay,
self.sound as u8, self.sound,
self.cycle, self.cycle,
); );
} }
} }
impl Debug for CPU {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CPU")
.field("flags", &self.flags)
.field("font", &self.font)
.field("stack", &self.stack)
.field("pc", &self.pc)
.field("i", &self.i)
.field("v", &self.v)
.field("delay", &self.delay)
.field("sound", &self.sound)
.field("keys", &self.keys)
.field("cycle", &self.cycle)
.field("breakpoints", &self.breakpoints)
.field("disassembler", &self.disassembler)
.finish_non_exhaustive()
}
}
impl Default for CPU { impl Default for CPU {
/// Constructs a new CPU with sane defaults and debug mode ON /// Constructs a new CPU with sane defaults and debug mode ON
/// ///
@ -576,8 +596,6 @@ impl Default for CPU {
/// | screen |`0x0f00` | Location of screen memory. /// | screen |`0x0f00` | Location of screen memory.
/// | font |`0x0050` | Location of font memory. /// | font |`0x0050` | Location of font memory.
/// | pc |`0x0200` | Start location. Generally 0x200 or 0x600. /// | pc |`0x0200` | Start location. Generally 0x200 or 0x600.
/// | sp |`0x0efe` | Initial top of stack.
///
/// ///
/// # Examples /// # Examples
/// ```rust /// ```rust
@ -586,21 +604,25 @@ impl Default for CPU {
/// ``` /// ```
fn default() -> Self { fn default() -> Self {
CPU { CPU {
screen: 0xf00, stack: vec![],
mem: mem! {
Charset [0x0050..0x00a0] = include_bytes!("mem/charset.bin"),
HiresCharset [0x00a0..0x0140] = include_bytes!("mem/hires.bin"),
Program [0x0200..0x1000],
},
font: 0x050, font: 0x050,
pc: 0x200, pc: 0x200,
sp: 0xefe,
i: 0, i: 0,
v: [0; 16], v: [0; 16],
delay: 0.0, delay: 0,
sound: 0.0, sound: 0,
cycle: 0, cycle: 0,
keys: [false; 16], keys: [false; 16],
lastkey: None,
flags: Flags { flags: Flags {
debug: true, debug: true,
..Default::default() ..Default::default()
}, },
timers: Default::default(),
breakpoints: vec![], breakpoints: vec![],
disassembler: Dis::default(), disassembler: Dis::default(),
} }

750
src/cpu/behavior.rs Normal file
View File

@ -0,0 +1,750 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! Contains implementations for each Chip-8 [Insn]
use super::*;
use crate::traits::Grab;
use rand::random;
impl CPU {
/// Executes a single [Insn]
#[rustfmt::skip]
#[inline(always)]
pub(super) fn execute(&mut self, screen: &mut Screen, instruction: Insn) {
match instruction {
// Core Chip-8 instructions
Insn::cls => self.clear_screen(screen),
Insn::ret => self.ret(),
Insn::jmp { A } => self.jump(A),
Insn::call { A } => self.call(A),
Insn::seb { x, B } => self.skip_equals_immediate(x, B),
Insn::sneb { x, B } => self.skip_not_equals_immediate(x, B),
Insn::se { y, x } => self.skip_equals(x, y),
Insn::movb { x, B } => self.load_immediate(x, B),
Insn::addb { x, B } => self.add_immediate(x, B),
Insn::mov { y, x } => self.load(x, y),
Insn::or { y, x } => self.or(x, y),
Insn::and { y, x } => self.and(x, y),
Insn::xor { y, x } => self.xor(x, y),
Insn::add { y, x } => self.add(x, y),
Insn::sub { y, x } => self.sub(x, y),
Insn::shr { y, x } => self.shift_right(x, y),
Insn::bsub { y, x } => self.backwards_sub(x, y),
Insn::shl { y, x } => self.shift_left(x, y),
Insn::sne { y, x } => self.skip_not_equals(x, y),
Insn::movI { A } => self.load_i_immediate(A),
Insn::jmpr { A } => self.jump_indexed(A),
Insn::rand { x, B } => self.rand(x, B),
Insn::draw { y, x, n } => self.draw(x, y, n, screen),
Insn::sek { x } => self.skip_key_equals(x),
Insn::snek { x } => self.skip_key_not_equals(x),
Insn::getdt { x } => self.load_delay_timer(x),
Insn::waitk { x } => self.wait_for_key(x),
Insn::setdt { x } => self.store_delay_timer(x),
Insn::movst { x } => self.store_sound_timer(x),
Insn::addI { x } => self.add_i(x),
Insn::font { x } => self.load_sprite(x),
Insn::bcd { x } => self.bcd_convert(x),
Insn::dmao { x } => self.store_dma(x),
Insn::dmai { x } => self.load_dma(x),
// Super-Chip extensions
Insn::scd { n } => self.scroll_down(n, screen),
Insn::scr => self.scroll_right(screen),
Insn::scl => self.scroll_left(screen),
Insn::halt => self.flags.pause(),
Insn::lores => self.init_lores(screen),
Insn::hires => self.init_hires(screen),
Insn::hfont { x } => self.load_big_sprite(x),
Insn::flgo { x } => self.store_flags(x),
Insn::flgi { x } => self.load_flags(x),
// XO-Chip extensions
_ => unimplemented!(),
}
}
}
/// |`0aaa`| Issues a "System call" (ML routine)
///
/// |opcode| effect |
/// |------|------------------------------------|
/// |`00e0`| Clear screen memory to all 0 |
/// |`00ee`| Return from subroutine |
impl CPU {
/// |`00e0`| Clears the screen memory to 0
/// Corresponds to [Insn::cls]
#[inline(always)]
pub(super) fn clear_screen(&mut self, screen: &mut Screen) {
screen.clear()
}
/// |`00ee`| Returns from subroutine
/// Corresponds to [Insn::ret]
#[inline(always)]
pub(super) fn ret(&mut self) {
self.pc = self.stack.pop().unwrap_or(0x200);
}
}
/// Super Chip screen-control routines
///
/// |opcode| effect |
/// |------|------------------------------------|
/// |`00cN`| Scroll the screen down N lines |
/// |`00fb`| Scroll the screen right |
/// |`00fc`| Scroll the screen left |
/// |`00fe`| Initialize lores mode |
/// |`00ff`| Initialize hires mode |
impl CPU {
/// # |`00cN`|
/// Scroll the screen down N lines
///
/// Corresponds to [Insn::scd]
#[inline(always)]
pub(super) fn scroll_down(&mut self, n: Nib, screen: &mut Screen) {
match self.flags.draw_mode {
true => {
// Get a line from the bus
for i in (0..16 * (64 - n as usize)).step_by(16).rev() {
let line: u128 = screen.read(i);
screen.write(i - (n as usize * 16), 0u128);
screen.write(i, line);
}
}
false => {
// Get a line from the bus
for i in (0..8 * (32 - n as usize)).step_by(8).rev() {
let line: u64 = screen.read(i);
screen.write(i, 0u64);
screen.write(i + (n as usize * 8), line);
}
}
}
}
/// # |`00fb`|
/// Scroll the screen right
///
/// Corresponds to [Insn::scr]
#[inline(always)]
pub(super) fn scroll_right(&mut self, screen: &mut impl AutoCast<u128>) {
// Get a line from the bus
for i in (0..16 * 64_usize).step_by(16) {
//let line: u128 = bus.read(self.screen + i) >> 4;
screen.write(i, screen.read(i) >> 4);
}
}
/// # |`00fc`|
/// Scroll the screen left
///
/// Corresponds to [Insn::scl]
#[inline(always)]
pub(super) fn scroll_left(&mut self, screen: &mut impl AutoCast<u128>) {
// Get a line from the bus
for i in (0..16 * 64_usize).step_by(16) {
let line: u128 = u128::wrapping_shl(screen.read(i), 4);
screen.write(i, line);
}
}
/// # |`00fe`|
/// Initialize lores mode
///
/// Corresponds to [Insn::lores]
pub(super) fn init_lores(&mut self, screen: &mut Screen) {
self.flags.draw_mode = false;
screen.with_size(256);
self.clear_screen(screen);
}
/// # |`00ff`|
/// Initialize hires mode
///
/// Corresponds to [Insn::hires]
pub(super) fn init_hires(&mut self, screen: &mut Screen) {
self.flags.draw_mode = true;
screen.with_size(1024);
self.clear_screen(screen);
}
}
/// |`1aaa`| Sets pc to an absolute address
impl CPU {
/// |`1aaa`| Sets the program counter to an absolute address
///
/// Corresponds to [Insn::jmp]
#[inline(always)]
pub(super) fn jump(&mut self, a: Adr) {
// jump to self == halt
if a.wrapping_add(2) == self.pc {
self.flags.pause = true;
}
self.pc = a;
}
}
/// |`2aaa`| Pushes pc onto the stack, then jumps to a
impl CPU {
/// |`2aaa`| Pushes pc onto the stack, then jumps to a
///
/// Corresponds to [Insn::call]
#[inline(always)]
pub(super) fn call(&mut self, a: Adr) {
self.stack.push(self.pc);
self.pc = a;
}
}
/// |`3xbb`| Skips next instruction if register X == b
impl CPU {
/// |`3xbb`| Skips the next instruction if register X == b
///
/// Corresponds to [Insn::seb]
#[inline(always)]
pub(super) fn skip_equals_immediate(&mut self, x: Reg, b: u8) {
if self.v[x] == b {
self.pc = self.pc.wrapping_add(2);
}
}
}
/// |`4xbb`| Skips next instruction if register X != b
impl CPU {
/// |`4xbb`| Skips the next instruction if register X != b
///
/// Corresponds to [Insn::sneb]
#[inline(always)]
pub(super) fn skip_not_equals_immediate(&mut self, x: Reg, b: u8) {
if self.v[x] != b {
self.pc = self.pc.wrapping_add(2);
}
}
}
/// |`5xyn`| Performs a register-register comparison
///
/// |opcode| effect |
/// |------|------------------------------------|
/// |`5XY0`| Skip next instruction if vX == vY |
impl CPU {
/// |`5xy0`| Skips the next instruction if register X != register Y
///
/// Corresponds to [Insn::sne]
#[inline(always)]
pub(super) fn skip_equals(&mut self, x: Reg, y: Reg) {
if self.v[x] == self.v[y] {
self.pc = self.pc.wrapping_add(2);
}
}
}
/// |`6xbb`| Loads immediate byte b into register vX
impl CPU {
/// |`6xbb`| Loads immediate byte b into register vX
///
/// Corresponds to [Insn::movb]
#[inline(always)]
pub(super) fn load_immediate(&mut self, x: Reg, b: u8) {
self.v[x] = b;
}
}
/// |`7xbb`| Adds immediate byte b to register vX
impl CPU {
/// |`7xbb`| Adds immediate byte b to register vX
///
/// Corresponds to [Insn::addb]
#[inline(always)]
pub(super) fn add_immediate(&mut self, x: Reg, b: u8) {
self.v[x] = self.v[x].wrapping_add(b);
}
}
/// |`8xyn`| Performs ALU operation
///
/// |opcode| effect |
/// |------|------------------------------------|
/// |`8xy0`| Y = X |
/// |`8xy1`| X = X | Y |
/// |`8xy2`| X = X & Y |
/// |`8xy3`| X = X ^ Y |
/// |`8xy4`| X = X + Y; Set vF=carry |
/// |`8xy5`| X = X - Y; Set vF=carry |
/// |`8xy6`| X = X >> 1 |
/// |`8xy7`| X = Y - X; Set vF=carry |
/// |`8xyE`| X = X << 1 |
impl CPU {
/// |`8xy0`| Loads the value of y into x
///
/// Corresponds to [Insn::mov]
#[inline(always)]
pub(super) fn load(&mut self, x: Reg, y: Reg) {
self.v[x] = self.v[y];
}
/// |`8xy1`| Performs bitwise or of vX and vY, and stores the result in vX
///
/// Corresponds to [Insn::or]
///
/// # Quirk
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
#[inline(always)]
pub(super) fn or(&mut self, x: Reg, y: Reg) {
self.v[x] |= self.v[y];
if !self.flags.quirks.bin_ops {
self.v[0xf] = 0;
}
}
/// |`8xy2`| Performs bitwise and of vX and vY, and stores the result in vX
///
/// Corresponds to [Insn::and]
///
/// # Quirk
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
#[inline(always)]
pub(super) fn and(&mut self, x: Reg, y: Reg) {
self.v[x] &= self.v[y];
if !self.flags.quirks.bin_ops {
self.v[0xf] = 0;
}
}
/// |`8xy3`| Performs bitwise xor of vX and vY, and stores the result in vX
///
/// Corresponds to [Insn::xor]
///
/// # Quirk
/// The original chip-8 interpreter will clobber vF for any 8-series instruction
#[inline(always)]
pub(super) fn xor(&mut self, x: Reg, y: Reg) {
self.v[x] ^= self.v[y];
if !self.flags.quirks.bin_ops {
self.v[0xf] = 0;
}
}
/// |`8xy4`| Performs addition of vX and vY, and stores the result in vX
///
/// Corresponds to [Insn::add]
#[inline(always)]
pub(super) fn add(&mut self, x: Reg, y: Reg) {
let carry;
(self.v[x], carry) = self.v[x].overflowing_add(self.v[y]);
self.v[0xf] = carry.into();
}
/// |`8xy5`| Performs subtraction of vX and vY, and stores the result in vX
///
/// Corresponds to [Insn::sub]
#[inline(always)]
pub(super) fn sub(&mut self, x: Reg, y: Reg) {
let carry;
(self.v[x], carry) = self.v[x].overflowing_sub(self.v[y]);
self.v[0xf] = (!carry).into();
}
/// |`8xy6`| Performs bitwise right shift of vX
///
/// Corresponds to [Insn::shr]
///
/// # Quirk
/// On the original chip-8 interpreter, this shifts vY and stores the result in vX
#[inline(always)]
pub(super) fn shift_right(&mut self, x: Reg, y: Reg) {
let src: Reg = if self.flags.quirks.shift { x } else { y };
let shift_out = self.v[src] & 1;
self.v[x] = self.v[src] >> 1;
self.v[0xf] = shift_out;
}
/// |`8xy7`| Performs subtraction of vY and vX, and stores the result in vX
///
/// Corresponds to [Insn::bsub]
#[inline(always)]
pub(super) fn backwards_sub(&mut self, x: Reg, y: Reg) {
let carry;
(self.v[x], carry) = self.v[y].overflowing_sub(self.v[x]);
self.v[0xf] = (!carry).into();
}
/// 8X_E: Performs bitwise left shift of vX
///
/// Corresponds to [Insn::shl]
///
/// # Quirk
/// On the original chip-8 interpreter, this would perform the operation on vY
/// and store the result in vX. This behavior was left out, for now.
#[inline(always)]
pub(super) fn shift_left(&mut self, x: Reg, y: Reg) {
let src: Reg = if self.flags.quirks.shift { x } else { y };
let shift_out: u8 = self.v[src] >> 7;
self.v[x] = self.v[src] << 1;
self.v[0xf] = shift_out;
}
}
/// |`9xyn`| Performs a register-register comparison
///
/// |opcode| effect |
/// |------|------------------------------------|
/// |`9XY0`| Skip next instruction if vX != vY |
impl CPU {
/// |`9xy0`| Skip next instruction if X != y
///
/// Corresponds to [Insn::sne]
#[inline(always)]
pub(super) fn skip_not_equals(&mut self, x: Reg, y: Reg) {
if self.v[x] != self.v[y] {
self.pc = self.pc.wrapping_add(2);
}
}
}
/// |`Aaaa`| Load address #a into register I
impl CPU {
/// |`Aadr`| Load address #adr into register I
///
/// Corresponds to [Insn::movI]
#[inline(always)]
pub(super) fn load_i_immediate(&mut self, a: Adr) {
self.i = a;
}
}
/// |`Baaa`| Jump to &adr + v0
impl CPU {
/// |`Badr`| Jump to &adr + v0
///
/// Corresponds to [Insn::jmpr]
///
/// Quirk:
/// On the Super-Chip, this does stupid shit
#[inline(always)]
pub(super) fn jump_indexed(&mut self, a: Adr) {
let reg = if self.flags.quirks.stupid_jumps {
a as usize >> 8
} else {
0
};
self.pc = a.wrapping_add(self.v[reg] as Adr);
}
}
/// |`Cxbb`| Stores a random number & the provided byte into vX
impl CPU {
/// |`Cxbb`| Stores a random number & the provided byte into vX
///
/// Corresponds to [Insn::rand]
#[inline(always)]
pub(super) fn rand(&mut self, x: Reg, b: u8) {
self.v[x] = random::<u8>() & b;
}
}
/// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
impl CPU {
/// |`Dxyn`| Draws n-byte sprite to the screen at coordinates (vX, vY)
///
/// Corresponds to [Insn::draw]
///
/// # Quirk
/// On the original chip-8 interpreter, this will wait for a VBI
#[inline(always)]
pub(super) fn draw(&mut self, x: Reg, y: Reg, n: Nib, screen: &mut Screen) {
if !self.flags.quirks.draw_wait {
self.flags.draw_wait = true;
}
// self.draw_hires handles both hi-res mode and drawing 16x16 sprites
if self.flags.draw_mode || n == 0 {
self.draw_hires(x, y, n, screen);
} else {
self.draw_lores(x, y, n, screen);
}
}
/// |`Dxyn`| Chip-8: Draws n-byte sprite to the screen at coordinates (vX, vY)
#[inline(always)]
pub(super) fn draw_lores(&mut self, x: Reg, y: Reg, n: Nib, scr: &mut Screen) {
self.draw_sprite(self.v[x] as u16 % 64, self.v[y] as u16 % 32, n, 64, 32, scr);
}
#[inline(always)]
pub(super) fn draw_sprite(
&mut self,
x: u16,
y: u16,
n: Nib,
w: u16,
h: u16,
screen: &mut Screen,
) {
let w_bytes = w / 8;
self.v[0xf] = 0;
if let Some(sprite) = self.mem.grab(self.i as usize..(self.i + n as u16) as usize) {
for (line, &sprite) in sprite.iter().enumerate() {
let line = line as u16;
let sprite = ((sprite as u16) << (8 - (x % 8))).to_be_bytes();
for (addr, &byte) in sprite.iter().enumerate().filter_map(|(idx, byte)| {
let x = (x / 8) + idx as u16;
Some((
if self.flags.quirks.screen_wrap {
((y + line) % h * w_bytes + (x % w_bytes)) % (w_bytes * h)
} else if x < w_bytes {
(y + line) * w_bytes + x
} else {
return None;
},
byte,
))
}) {
let display: u8 = screen.read(addr);
screen.write(addr, byte ^ display);
if byte & display != 0 {
self.v[0xf] = 1;
}
}
}
}
}
}
/// |`Dxyn`| Super-Chip extension high-resolution graphics mode
impl CPU {
/// |`Dxyn`| Super-Chip extension high-resolution graphics mode
#[inline(always)]
pub(super) fn draw_hires(&mut self, x: Reg, y: Reg, n: Nib, screen: &mut Screen) {
if !self.flags.quirks.draw_wait {
self.flags.draw_wait = true;
}
let (w, h) = match self.flags.draw_mode {
true => (128, 64),
false => (64, 32),
};
let (x, y) = (self.v[x] as u16 % w, self.v[y] as u16 % h);
match n {
0 => self.draw_schip_sprite(x, y, w, screen),
_ => self.draw_sprite(x, y, n, w, h, screen),
}
}
/// Draws a 16x16 Super Chip sprite
#[inline(always)]
pub(super) fn draw_schip_sprite(&mut self, x: u16, y: u16, w: u16, screen: &mut Screen) {
self.v[0xf] = 0;
let w_bytes = w / 8;
if let Some(sprite) = self.mem.grab(self.i as usize..(self.i + 32) as usize) {
let sprite = sprite.to_owned();
for (line, sprite) in sprite.chunks_exact(2).enumerate() {
let sprite = u16::from_be_bytes(
sprite
.try_into()
.expect("Chunks should only return 2 bytes"),
);
let addr = (y + line as u16) * w_bytes + x / 8;
let sprite = (sprite as u32) << (16 - (x % 8));
let display: u32 = screen.read(addr);
screen.write(addr, display ^ sprite);
if display & sprite != 0 {
self.v[0xf] += 1;
}
}
}
}
}
/// |`Exbb`| Skips instruction on value of keypress
///
/// |opcode| effect |
/// |------|------------------------------------|
/// |`eX9e`| Skip next instruction if key == vX |
/// |`eXa1`| Skip next instruction if key != vX |
impl CPU {
/// |`Ex9E`| Skip next instruction if key == vX
///
/// Corresponds to [Insn::sek]
#[inline(always)]
pub(super) fn skip_key_equals(&mut self, x: Reg) {
if self.keys[self.v[x] as usize & 0xf] {
self.pc += 2;
}
}
/// |`ExaE`| Skip next instruction if key != vX
///
/// Corresponds to [Insn::snek]
#[inline(always)]
pub(super) fn skip_key_not_equals(&mut self, x: Reg) {
if !self.keys[self.v[x] as usize & 0xf] {
self.pc += 2;
}
}
}
/// |`Fxbb`| Performs IO
///
/// |opcode| effect |
/// |------|------------------------------------|
/// |`fX07`| Set vX to value in delay timer |
/// |`fX0a`| Wait for input, store key in vX |
/// |`fX15`| Set sound timer to the value in vX |
/// |`fX18`| set delay timer to the value in vX |
/// |`fX1e`| Add vX to I |
/// |`fX29`| Load sprite for character x into I |
/// |`fX33`| BCD convert X into I[0..3] |
/// |`fX55`| DMA Stor from I to registers 0..=X |
/// |`fX65`| DMA Load from I to registers 0..=X |
impl CPU {
/// |`Fx07`| Get the current DT, and put it in vX
///
/// Corresponds to [Insn::getdt]
///
/// ```py
/// vX = DT
/// ```
#[inline(always)]
pub(super) fn load_delay_timer(&mut self, x: Reg) {
self.v[x] = self.delay;
}
/// |`Fx0A`| Wait for key, then vX = K
#[inline(always)]
pub(super) fn wait_for_key(&mut self, x: Reg) {
if let Some(key) = self.lastkey {
self.v[x] = key as u8;
self.lastkey = None;
} else {
self.pc = self.pc.wrapping_sub(2);
self.flags.keypause = true;
}
}
/// |`Fx15`| Load vX into DT
///
/// Corresponds to [Insn::setdt]
/// ```py
/// DT = vX
/// ```
#[inline(always)]
pub(super) fn store_delay_timer(&mut self, x: Reg) {
self.delay = self.v[x];
}
/// |`Fx18`| Load vX into ST
///
/// Corresponds to [Insn::movst]
/// ```py
/// ST = vX;
/// ```
#[inline(always)]
pub(super) fn store_sound_timer(&mut self, x: Reg) {
self.sound = self.v[x];
}
/// |`Fx1e`| Add vX to I,
///
/// Corresponds to [Insn::addI]
/// ```py
/// I += vX;
/// ```
#[inline(always)]
pub(super) fn add_i(&mut self, x: Reg) {
self.i += self.v[x] as u16;
}
/// |`Fx29`| Load sprite for character x into I
///
/// Corresponds to [Insn::font]
/// ```py
/// I = sprite(X);
/// ```
#[inline(always)]
pub(super) fn load_sprite(&mut self, x: Reg) {
self.i = self.font + (5 * (self.v[x] as Adr % 0x10));
}
/// |`Fx33`| BCD convert X into I`[0..3]`
///
/// Corresponds to [Insn::bcd]
#[inline(always)]
pub(super) fn bcd_convert(&mut self, x: Reg) {
let x = self.v[x];
self.mem.write(self.i.wrapping_add(2), x % 10);
self.mem.write(self.i.wrapping_add(1), x / 10 % 10);
self.mem.write(self.i, x / 100 % 10);
}
/// |`Fx55`| DMA Stor from I to registers 0..=X
///
/// Corresponds to [Insn::dmao]
///
/// # Quirk
/// The original chip-8 interpreter uses I to directly index memory,
/// with the side effect of leaving I as I+X+1 after the transfer is done.
#[inline(always)]
pub(super) fn store_dma(&mut self, x: Reg) {
let i = self.i as usize;
for (reg, value) in self
.mem
.grab_mut(i..=i + x)
.unwrap_or_default()
.iter_mut()
.enumerate()
{
*value = self.v[reg]
}
if !self.flags.quirks.dma_inc {
self.i += x as Adr + 1;
}
}
/// |`Fx65`| DMA Load from I to registers 0..=X
///
/// Corresponds to [Insn::dmai]
///
/// # Quirk
/// The original chip-8 interpreter uses I to directly index memory,
/// with the side effect of leaving I as I+X+1 after the transfer is done.
#[inline(always)]
pub(super) fn load_dma(&mut self, x: Reg) {
let i = self.i as usize;
for (reg, value) in self
.mem
.grab(i..=i + x)
.unwrap_or_default()
.iter()
.enumerate()
{
self.v[reg] = *value;
}
if !self.flags.quirks.dma_inc {
self.i += x as Adr + 1;
}
}
}
/// |`Fxbb`| Super Chip: Performs IO
///
/// |opcode| effect |
/// |------|------------------------------------|
/// |`Fx30`| 16x16 equivalent of load_sprite |
/// |`Fx75`| Save to "flag registers" |
/// |`Fx85`| Load from "flag registers" |
impl CPU {
/// |`Fx30`| (Super-Chip) 16x16 equivalent of [CPU::load_sprite]
///
/// Corresponds to [Insn::hfont]
///
/// TODO: Actually make and import the 16x font
#[inline(always)]
pub(super) fn load_big_sprite(&mut self, x: Reg) {
self.i = self.font + (5 * 8) + (16 * (self.v[x] as Adr % 0x10));
}
/// |`Fx75`| (Super-Chip) Save to "flag registers"
/// I just chuck it in 0x0..0xf. Screw it.
///
/// Corresponds to [Insn::flgo]
#[inline(always)]
pub(super) fn store_flags(&mut self, x: Reg) {
// TODO: Save these, maybe
for (reg, value) in self
.mem
.grab_mut(0..=x)
.unwrap_or_default()
.iter_mut()
.enumerate()
{
*value = self.v[reg]
}
}
/// |`Fx85`| (Super-Chip) Load from "flag registers"
/// I just chuck it in 0x0..0xf. Screw it.
///
/// Corresponds to [Insn::flgi]
#[inline(always)]
pub(super) fn load_flags(&mut self, x: Reg) {
for (reg, value) in self.mem.grab(0..=x).unwrap_or_default().iter().enumerate() {
self.v[reg] = *value;
}
}
}

View File

@ -1,230 +0,0 @@
//! A disassembler for Chip-8 opcodes
#![allow(clippy::bad_bit_mask)]
use imperative_rs::InstructionSet;
use owo_colors::{OwoColorize, Style};
use std::fmt::Display;
/// Disassembles Chip-8 instructions
pub trait Disassembler {
/// Disassemble a single instruction
fn once(&self, insn: u16) -> String;
}
#[allow(non_camel_case_types, non_snake_case, missing_docs)]
#[derive(Clone, Copy, Debug, InstructionSet, PartialEq, Eq)]
/// Implements a Disassembler using imperative_rs
pub enum Insn {
// Base instruction set
/// | 00e0 | Clear screen memory to 0s
#[opcode = "0x00e0"]
cls,
/// | 00ee | Return from subroutine
#[opcode = "0x00ee"]
ret,
/// | 1aaa | Jumps to an absolute address
#[opcode = "0x1AAA"]
jmp { A: u16 },
/// | 2aaa | Pushes pc onto the stack, then jumps to a
#[opcode = "0x2AAA"]
call { A: u16 },
/// | 3xbb | Skips next instruction if register X == b
#[opcode = "0x3xBB"]
seb { B: u8, x: usize },
/// | 4xbb | Skips next instruction if register X != b
#[opcode = "0x4xBB"]
sneb { B: u8, x: usize },
/// | 9XY0 | Skip next instruction if vX == vY |
#[opcode = "0x5xy0"]
se { y: usize, x: usize },
/// | 6xbb | Loads immediate byte b into register vX
#[opcode = "0x6xBB"]
movb { B: u8, x: usize },
/// | 7xbb | Adds immediate byte b to register vX
#[opcode = "0x7xBB"]
addb { B: u8, x: usize },
/// | 8xy0 | Loads the value of y into x
#[opcode = "0x8xy0"]
mov { x: usize, y: usize },
/// | 8xy1 | Performs bitwise or of vX and vY, and stores the result in vX
#[opcode = "0x8xy1"]
or { y: usize, x: usize },
/// | 8xy2 | Performs bitwise and of vX and vY, and stores the result in vX
#[opcode = "0x8xy2"]
and { y: usize, x: usize },
/// | 8xy3 | Performs bitwise xor of vX and vY, and stores the result in vX
#[opcode = "0x8xy3"]
xor { y: usize, x: usize },
/// | 8xy4 | Performs addition of vX and vY, and stores the result in vX
#[opcode = "0x8xy4"]
add { y: usize, x: usize },
/// | 8xy5 | Performs subtraction of vX and vY, and stores the result in vX
#[opcode = "0x8xy5"]
sub { y: usize, x: usize },
/// | 8xy6 | Performs bitwise right shift of vX (or vY)
#[opcode = "0x8xy6"]
shr { y: usize, x: usize },
/// | 8xy7 | Performs subtraction of vY and vX, and stores the result in vX
#[opcode = "0x8xy7"]
bsub { y: usize, x: usize },
/// | 8xyE | Performs bitwise left shift of vX
#[opcode = "0x8xye"]
shl { y: usize, x: usize },
/// | 9XY0 | Skip next instruction if vX != vY
#[opcode = "0x9xy0"]
sne { y: usize, x: usize },
/// | Aaaa | Load address #a into register I
#[opcode = "0xaAAA"]
movI { A: u16 },
/// | Baaa | Jump to &adr + v0
#[opcode = "0xbAAA"]
jmpr { A: u16 },
/// | Cxbb | Stores a random number & the provided byte into vX
#[opcode = "0xcxBB"]
rand { B: u8, x: usize },
/// | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY)
#[opcode = "0xdxyn"]
draw { y: usize, x: usize, n: u8 },
/// | eX9e | Skip next instruction if key == vX
#[opcode = "0xex9e"]
sek { x: usize },
/// | eXa1 | Skip next instruction if key != vX
#[opcode = "0xexa1"]
snek { x: usize },
// | fX07 | Set vX to value in delay timer
#[opcode = "0xfx07"]
getdt { x: usize },
// | fX0a | Wait for input, store key in vX
#[opcode = "0xfx0a"]
waitk { x: usize },
// | fX15 | Set sound timer to the value in vX
#[opcode = "0xfx15"]
setdt { x: usize },
// | fX18 | set delay timer to the value in vX
#[opcode = "0xfx18"]
movst { x: usize },
// | fX1e | Add vX to I
#[opcode = "0xfx1e"]
addI { x: usize },
// | fX29 | Load sprite for character x into I
#[opcode = "0xfx29"]
font { x: usize },
// | fX33 | BCD convert X into I[0..3]
#[opcode = "0xfx33"]
bcd { x: usize },
// | fX55 | DMA Stor from I to registers 0..X
#[opcode = "0xfx55"]
dmao { x: usize },
// | fX65 | DMA Load from I to registers 0..X
#[opcode = "0xfx65"]
dmai { x: usize },
// Super Chip extensions
/// | 00cN | Scroll the screen down
#[opcode = "0x00cn"]
scd { n: u8 },
/// | 00fb | Scroll the screen right
#[opcode = "0x00fb"]
scr,
/// | 00fc | Scroll the screen left
#[opcode = "0x00fc"]
scl,
/// | 00fd | Exit (halt and catch fire)
#[opcode = "0x00fd"]
halt,
/// | 00fe | Return to low-resolution mode
#[opcode = "0x00fe"]
lores,
/// | 00ff | Enter high-resolution mode
#[opcode = "0x00ff"]
hires,
/// | fx30 | Enter high-resolution mode
#[opcode = "0xfx30"]
hfont { x: usize },
/// | fx75 | Save to "flag registers"
#[opcode = "0xfx75"]
flgo { x: usize },
/// | fx85 | Load from "flag registers"
#[opcode = "0xfx85"]
flgi { x: usize },
}
impl Display for Insn {
#[rustfmt::skip]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// Base instruction set
Insn::cls => write!(f, "cls "),
Insn::ret => write!(f, "ret "),
Insn::jmp { A } => write!(f, "jmp {A:03x}"),
Insn::call { A } => write!(f, "call {A:03x}"),
Insn::seb { B, x } => write!(f, "se #{B:02x}, v{x:X}"),
Insn::sneb { B, x } => write!(f, "sne #{B:02x}, v{x:X}"),
Insn::se { y, x } => write!(f, "se v{y:X}, v{x:X}"),
Insn::movb { B, x } => write!(f, "mov #{B:02x}, v{x:X}"),
Insn::addb { B, x } => write!(f, "add #{B:02x}, v{x:X}"),
Insn::mov { x, y } => write!(f, "mov v{y:X}, v{x:X}"),
Insn::or { y, x } => write!(f, "or v{y:X}, v{x:X}"),
Insn::and { y, x } => write!(f, "and v{y:X}, v{x:X}"),
Insn::xor { y, x } => write!(f, "xor v{y:X}, v{x:X}"),
Insn::add { y, x } => write!(f, "add v{y:X}, v{x:X}"),
Insn::sub { y, x } => write!(f, "sub v{y:X}, v{x:X}"),
Insn::shr { y, x } => write!(f, "shr v{y:X}, v{x:X}"),
Insn::bsub { y, x } => write!(f, "bsub v{y:X}, v{x:X}"),
Insn::shl { y, x } => write!(f, "shl v{y:X}, v{x:X}"),
Insn::sne { y, x } => write!(f, "sne v{y:X}, v{x:X}"),
Insn::movI { A } => write!(f, "mov ${A:03x}, I"),
Insn::jmpr { A } => write!(f, "jmp ${A:03x}+v0"),
Insn::rand { B, x } => write!(f, "rand #{B:02x}, v{x:X}"),
Insn::draw { y, x, n } => write!(f, "draw #{n:x}, v{x:X}, v{y:X}"),
Insn::sek { x } => write!(f, "sek v{x:X}"),
Insn::snek { x } => write!(f, "snek v{x:X}"),
Insn::getdt { x } => write!(f, "mov DT, v{x:X}"),
Insn::waitk { x } => write!(f, "waitk v{x:X}"),
Insn::setdt { x } => write!(f, "mov v{x:X}, DT"),
Insn::movst { x } => write!(f, "mov v{x:X}, ST"),
Insn::addI { x } => write!(f, "add v{x:X}, I"),
Insn::font { x } => write!(f, "font v{x:X}, I"),
Insn::bcd { x } => write!(f, "bcd v{x:X}, &I"),
Insn::dmao { x } => write!(f, "dmao v{x:X}"),
Insn::dmai { x } => write!(f, "dmai v{x:X}"),
// Super Chip extensions
Insn::scd { n } => write!(f, "scd #{n:x}"),
Insn::scr => write!(f, "scr "),
Insn::scl => write!(f, "scl "),
Insn::halt => write!(f, "halt "),
Insn::lores => write!(f, "lores "),
Insn::hires => write!(f, "hires "),
Insn::hfont { x } => write!(f, "hfont v{x:X}"),
Insn::flgo { x } => write!(f, "flgo v{x:X}"),
Insn::flgi { x } => write!(f, "flgi v{x:X}"),
}
}
}
/// Disassembles Chip-8 instructions, printing them in the provided [owo_colors::Style]s
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Dis {
/// Styles invalid instructions
pub invalid: Style,
/// Styles valid instruction
pub normal: Style,
}
impl Default for Dis {
fn default() -> Self {
Self {
invalid: Style::new().bold().red(),
normal: Style::new().green(),
}
}
}
impl Disassembler for Dis {
fn once(&self, insn: u16) -> String {
if let Ok((_, insn)) = Insn::decode(&insn.to_be_bytes()) {
format!("{}", insn.style(self.normal))
} else {
format!("{}", format_args!("inval {insn:04x}").style(self.invalid))
}
}
}

View File

@ -1,9 +1,13 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! Represents [Flags] that aid in implementation but aren't a part of the Chip-8 spec //! Represents [Flags] that aid in implementation but aren't a part of the Chip-8 spec
use super::{Mode, Quirks}; use super::{Mode, Quirks};
/// Represents flags that aid in implementation but aren't a part of the Chip-8 spec /// Represents flags that aid in implementation but aren't a part of the Chip-8 spec
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Flags { pub struct Flags {
/// Set when debug (live disassembly) mode enabled /// Set when debug (live disassembly) mode enabled
pub debug: bool, pub debug: bool,
@ -15,14 +19,12 @@ pub struct Flags {
pub draw_wait: bool, pub draw_wait: bool,
/// Set when the emulator is in high-res mode /// Set when the emulator is in high-res mode
pub draw_mode: bool, pub draw_mode: bool,
/// Set to the last key that's been *released* after a keypause
pub lastkey: Option<usize>,
/// Represents the current emulator [Mode] /// Represents the current emulator [Mode]
pub mode: Mode, pub mode: Mode,
/// Represents the set of emulator [Quirks] to enable, independent of the [Mode] /// Represents the set of emulator [Quirks] to enable, independent of the [Mode]
pub quirks: Quirks, pub quirks: Quirks,
/// Represents the number of instructions to run per tick of the internal timer /// Set when the interpreter should increase the cycle count during a pause
pub monotonic: Option<usize>, pub monotonic: bool,
} }
impl Flags { impl Flags {

View File

@ -1,625 +1,231 @@
// (c) 2023 John A. Breaux // (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE.txt for details) // This code is licensed under MIT license (see LICENSE for details)
//! Contains implementations for each [Insn] as private member functions of [CPU] //! Contains the definition of a Chip-8 [Insn]
#![allow(clippy::bad_bit_mask)]
use super::*; pub mod disassembler;
impl CPU { use imperative_rs::InstructionSet;
/// Executes a single [Insn] use std::fmt::Display;
#[inline(always)]
#[allow(non_camel_case_types, non_snake_case, missing_docs)]
#[derive(Clone, Copy, Debug, InstructionSet, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// Represents a Chip-8 Instruction
pub enum Insn {
// Base instruction set
/// | 00e0 | Clear screen memory to 0s
#[opcode = "0x00e0"]
cls,
/// | 00ee | Return from subroutine
#[opcode = "0x00ee"]
ret,
/// | 1aaa | Jumps to an absolute address
#[opcode = "0x1AAA"]
jmp { A: u16 },
/// | 2aaa | Pushes pc onto the stack, then jumps to a
#[opcode = "0x2AAA"]
call { A: u16 },
/// | 3xbb | Skips next instruction if register X == b
#[opcode = "0x3xBB"]
seb { B: u8, x: usize },
/// | 4xbb | Skips next instruction if register X != b
#[opcode = "0x4xBB"]
sneb { B: u8, x: usize },
/// | 9XY0 | Skip next instruction if vX == vY |
#[opcode = "0x5xy0"]
se { y: usize, x: usize },
/// | 6xbb | Loads immediate byte b into register vX
#[opcode = "0x6xBB"]
movb { B: u8, x: usize },
/// | 7xbb | Adds immediate byte b to register vX
#[opcode = "0x7xBB"]
addb { B: u8, x: usize },
/// | 8xy0 | Loads the value of y into x
#[opcode = "0x8xy0"]
mov { x: usize, y: usize },
/// | 8xy1 | Performs bitwise or of vX and vY, and stores the result in vX
#[opcode = "0x8xy1"]
or { y: usize, x: usize },
/// | 8xy2 | Performs bitwise and of vX and vY, and stores the result in vX
#[opcode = "0x8xy2"]
and { y: usize, x: usize },
/// | 8xy3 | Performs bitwise xor of vX and vY, and stores the result in vX
#[opcode = "0x8xy3"]
xor { y: usize, x: usize },
/// | 8xy4 | Performs addition of vX and vY, and stores the result in vX
#[opcode = "0x8xy4"]
add { y: usize, x: usize },
/// | 8xy5 | Performs subtraction of vX and vY, and stores the result in vX
#[opcode = "0x8xy5"]
sub { y: usize, x: usize },
/// | 8xy6 | Performs bitwise right shift of vX (or vY)
#[opcode = "0x8xy6"]
shr { y: usize, x: usize },
/// | 8xy7 | Performs subtraction of vY and vX, and stores the result in vX
#[opcode = "0x8xy7"]
bsub { y: usize, x: usize },
/// | 8xyE | Performs bitwise left shift of vX
#[opcode = "0x8xye"]
shl { y: usize, x: usize },
/// | 9XY0 | Skip next instruction if vX != vY
#[opcode = "0x9xy0"]
sne { y: usize, x: usize },
/// | Aaaa | Load address #a into register I
#[opcode = "0xaAAA"]
movI { A: u16 },
/// | Baaa | Jump to &adr + v0
#[opcode = "0xbAAA"]
jmpr { A: u16 },
/// | Cxbb | Stores a random number & the provided byte into vX
#[opcode = "0xcxBB"]
rand { B: u8, x: usize },
/// | Dxyn | Draws n-byte sprite to the screen at coordinates (vX, vY)
#[opcode = "0xdxyn"]
draw { y: usize, x: usize, n: u8 },
/// | eX9e | Skip next instruction if key == vX
#[opcode = "0xex9e"]
sek { x: usize },
/// | eXa1 | Skip next instruction if key != vX
#[opcode = "0xexa1"]
snek { x: usize },
/// | fX07 | Set vX to value in delay timer
#[opcode = "0xfx07"]
getdt { x: usize },
/// | fX0a | Wait for input, store key in vX
#[opcode = "0xfx0a"]
waitk { x: usize },
/// | fX15 | Set sound timer to the value in vX
#[opcode = "0xfx15"]
setdt { x: usize },
/// | fX18 | set delay timer to the value in vX
#[opcode = "0xfx18"]
movst { x: usize },
/// | fX1e | Add vX to I
#[opcode = "0xfx1e"]
addI { x: usize },
/// | fX29 | Load sprite for character x into I
#[opcode = "0xfx29"]
font { x: usize },
/// | fX33 | BCD convert X into I[0..3]
#[opcode = "0xfx33"]
bcd { x: usize },
/// | fX55 | DMA Stor from I to registers 0..X
#[opcode = "0xfx55"]
dmao { x: usize },
/// | fX65 | DMA Load from I to registers 0..X
#[opcode = "0xfx65"]
dmai { x: usize },
// Super Chip extensions
/// | 00cN | Scroll the screen down
#[opcode = "0x00cn"]
scd { n: u8 },
/// | 00fb | Scroll the screen right
#[opcode = "0x00fb"]
scr,
/// | 00fc | Scroll the screen left
#[opcode = "0x00fc"]
scl,
/// | 00fd | Exit (halt and catch fire)
#[opcode = "0x0000"]
#[opcode = "0x00fd"]
halt,
/// | 00fe | Return to low-resolution mode
#[opcode = "0x00fe"]
lores,
/// | 00ff | Enter high-resolution mode
#[opcode = "0x00ff"]
hires,
/// | fx30 | Load hires font address into vX
#[opcode = "0xfx30"]
hfont { x: usize },
/// | fx75 | Save to "flag registers"
#[opcode = "0xfx75"]
flgo { x: usize },
/// | fx85 | Load from "flag registers"
#[opcode = "0xfx85"]
flgi { x: usize },
// XO-Chip instructions
/// | 00dN | Scroll the screen up
#[opcode = "0x00dn"]
scu { n: u8 },
/// | 5XY2 | DMA Load from I to vX..vY
#[opcode = "0x5xy2"]
dmaro { y: usize, x: usize },
/// | 5XY3 | DMA Load from I to vX..vY
#[opcode = "0x5xy3"]
dmari { y: usize, x: usize },
/// | F000 | Load long address into character I
#[opcode = "0xf000_iiii"]
long { i: usize },
}
impl From<&Insn> for u32 {
fn from(value: &Insn) -> Self {
let mut buf = [0u8;4];
let len = Insn::encode(value, &mut buf).unwrap_or_default();
u32::from_be_bytes(buf) >> ((4-len)*8)
}
}
impl Display for Insn {
#[rustfmt::skip] #[rustfmt::skip]
pub(super) fn execute(&mut self, bus: &mut Bus, instruction: Insn) { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match instruction { match self {
// Core Chip-8 instructions // Base instruction set
Insn::cls => self.clear_screen(bus), Insn::cls => write!(f, "cls "),
Insn::ret => self.ret(bus), Insn::ret => write!(f, "ret "),
Insn::jmp { A } => self.jump(A), Insn::jmp { A } => write!(f, "jmp {A:03x}"),
Insn::call { A } => self.call(A, bus), Insn::call { A } => write!(f, "call {A:03x}"),
Insn::seb { x, B } => self.skip_equals_immediate(x, B), Insn::seb { B, x } => write!(f, "se #{B:02x}, v{x:X}"),
Insn::sneb { x, B } => self.skip_not_equals_immediate(x, B), Insn::sneb { B, x } => write!(f, "sne #{B:02x}, v{x:X}"),
Insn::se { y, x } => self.skip_equals(x, y), Insn::se { y, x } => write!(f, "se v{y:X}, v{x:X}"),
Insn::movb { x, B } => self.load_immediate(x, B), Insn::movb { B, x } => write!(f, "mov #{B:02x}, v{x:X}"),
Insn::addb { x, B } => self.add_immediate(x, B), Insn::addb { B, x } => write!(f, "add #{B:02x}, v{x:X}"),
Insn::mov { y, x } => self.load(x, y), Insn::mov { x, y } => write!(f, "mov v{y:X}, v{x:X}"),
Insn::or { y, x } => self.or(x, y), Insn::or { y, x } => write!(f, "or v{y:X}, v{x:X}"),
Insn::and { y, x } => self.and(x, y), Insn::and { y, x } => write!(f, "and v{y:X}, v{x:X}"),
Insn::xor { y, x } => self.xor(x, y), Insn::xor { y, x } => write!(f, "xor v{y:X}, v{x:X}"),
Insn::add { y, x } => self.add(x, y), Insn::add { y, x } => write!(f, "add v{y:X}, v{x:X}"),
Insn::sub { y, x } => self.sub(x, y), Insn::sub { y, x } => write!(f, "sub v{y:X}, v{x:X}"),
Insn::shr { y, x } => self.shift_right(x, y), Insn::shr { y, x } => write!(f, "shr v{y:X}, v{x:X}"),
Insn::bsub { y, x } => self.backwards_sub(x, y), Insn::bsub { y, x } => write!(f, "bsub v{y:X}, v{x:X}"),
Insn::shl { y, x } => self.shift_left(x, y), Insn::shl { y, x } => write!(f, "shl v{y:X}, v{x:X}"),
Insn::sne { y, x } => self.skip_not_equals(x, y), Insn::sne { y, x } => write!(f, "sne v{y:X}, v{x:X}"),
Insn::movI { A } => self.load_i_immediate(A), Insn::movI { A } => write!(f, "mov ${A:03x}, I"),
Insn::jmpr { A } => self.jump_indexed(A), Insn::jmpr { A } => write!(f, "jmp ${A:03x}+v0"),
Insn::rand { x, B } => self.rand(x, B), Insn::rand { B, x } => write!(f, "rand #{B:02x}, v{x:X}"),
Insn::draw { y, x, n } => self.draw(x, y, n, bus), Insn::draw { y, x, n } => write!(f, "draw #{n:x}, v{x:X}, v{y:X}"),
Insn::sek { x } => self.skip_key_equals(x), Insn::sek { x } => write!(f, "sek v{x:X}"),
Insn::snek { x } => self.skip_key_not_equals(x), Insn::snek { x } => write!(f, "snek v{x:X}"),
Insn::getdt { x } => self.load_delay_timer(x), Insn::getdt { x } => write!(f, "mov DT, v{x:X}"),
Insn::waitk { x } => self.wait_for_key(x), Insn::waitk { x } => write!(f, "waitk v{x:X}"),
Insn::setdt { x } => self.store_delay_timer(x), Insn::setdt { x } => write!(f, "mov v{x:X}, DT"),
Insn::movst { x } => self.store_sound_timer(x), Insn::movst { x } => write!(f, "mov v{x:X}, ST"),
Insn::addI { x } => self.add_i(x), Insn::addI { x } => write!(f, "add v{x:X}, I"),
Insn::font { x } => self.load_sprite(x), Insn::font { x } => write!(f, "font v{x:X}, I"),
Insn::bcd { x } => self.bcd_convert(x, bus), Insn::bcd { x } => write!(f, "bcd v{x:X}, &I"),
Insn::dmao { x } => self.store_dma(x, bus), Insn::dmao { x } => write!(f, "dmao v{x:X}"),
Insn::dmai { x } => self.load_dma(x, bus), Insn::dmai { x } => write!(f, "dmai v{x:X}"),
// Super-Chip extensions // Super Chip extensions
Insn::scd { n } => self.scroll_down(n, bus), Insn::scd { n } => write!(f, "scd #{n:x}"),
Insn::scr => self.scroll_right(bus), Insn::scr => write!(f, "scr "),
Insn::scl => self.scroll_left(bus), Insn::scl => write!(f, "scl "),
Insn::halt => self.flags.pause(), Insn::halt => write!(f, "halt "),
Insn::lores => self.init_lores(bus), Insn::lores => write!(f, "lores "),
Insn::hires => self.init_hires(bus), Insn::hires => write!(f, "hires "),
Insn::hfont { x } => self.load_big_sprite(x), Insn::hfont { x } => write!(f, "hfont v{x:X}"),
Insn::flgo { x } => self.store_flags(x, bus), Insn::flgo { x } => write!(f, "flgo v{x:X}"),
Insn::flgi { x } => self.load_flags(x, bus), Insn::flgi { x } => write!(f, "flgi v{x:X}"),
// XO-Chip extensions
Insn::scu { n } => write!(f, "scu #{n:x}"),
Insn::dmaro { y, x } => write!(f, "dmaro v{x:X}..v{y:X}"),
Insn::dmari { y, x } => write!(f, "dmari v{x:X}..v{y:X}"),
Insn::long { i } => write!(f, "long ${i:04x}"),
} }
} }
} }
// |`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);
}
}

View File

@ -0,0 +1,41 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! A disassembler for Chip-8 opcodes
use super::Insn;
use imperative_rs::InstructionSet;
use owo_colors::{OwoColorize, Style};
/// Disassembles Chip-8 instructions
pub trait Disassembler {
/// Disassemble a single instruction
fn once(&self, insn: u16) -> String;
}
/// Disassembles Chip-8 instructions, printing them in the provided [owo_colors::Style]s
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Dis {
/// Styles invalid instructions
pub invalid: Style,
/// Styles valid instruction
pub normal: Style,
}
impl Default for Dis {
fn default() -> Self {
Self {
invalid: Style::new().bold().red(),
normal: Style::new().green(),
}
}
}
impl Disassembler for Dis {
fn once(&self, insn: u16) -> String {
if let Ok((_, insn)) = Insn::decode(&insn.to_be_bytes()) {
format!("{}", insn.style(self.normal))
} else {
format!("{}", format_args!("inval {insn:04x}").style(self.invalid))
}
}
}

327
src/cpu/mem.rs Normal file
View File

@ -0,0 +1,327 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! The Mem represents the CPU's memory
//!
//! Contains some handy utils for reading and writing
use crate::{error::Result, traits::Grab};
use std::{
fmt::{Debug, Display, Formatter},
ops::Range,
slice::SliceIndex,
};
/// Creates a new [Mem], growing as needed
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::Region::*, *};
/// let mut mem = mem! {
/// Charset [0x0000..0x0800] = b"ABCDEF",
/// Program [0x0800..0xf000] = include_bytes!("mem.rs"),
/// };
/// ```
#[macro_export]
macro_rules! mem {
($($name:path $(:)? [$range:expr] $(= $data:expr)?) ,* $(,)?) => {
$crate::cpu::mem::Mem::default()$(.add_region_owned($name, $range)$(.load_region_owned($name, $data))?)*
};
}
// Traits Read and Write are here purely to make implementing other things more bearable
impl Grab for Mem {
/// Gets a slice of [Mem] memory
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mem = Mem::new()
/// .add_region_owned(Program, 0..10);
/// assert!([0;10].as_slice() == mem.grab(0..10).unwrap());
///# Ok(())
///# }
/// ```
#[inline(always)]
fn grab<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
where
I: SliceIndex<[u8]>,
{
self.memory.get(index)
}
/// Gets a mutable slice of [Mem] memory
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mut mem = Mem::new()
/// .add_region_owned(Program, 0..10);
/// assert!([0;10].as_slice() == mem.grab_mut(0..10).unwrap());
///# Ok(())
///# }
/// ```
#[inline(always)]
fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[u8]>>::Output>
where
I: SliceIndex<[u8]>,
{
self.memory.get_mut(index)
}
}
/// Represents a named region in memory
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Region {
/// Character ROM (but writable!)
Charset,
/// Hires character ROM
HiresCharset,
/// Program memory
Program,
#[doc(hidden)]
/// Total number of named regions
Count,
}
impl Display for Region {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Region::HiresCharset => "HiresCharset",
Region::Charset => "Charset",
Region::Program => "Program",
_ => "",
}
)
}
}
/// Stores memory in a series of named regions with ranges
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Mem {
memory: Vec<u8>,
region: [Option<Range<usize>>; Region::Count as usize],
}
impl Mem {
// TODO: make mem::new() give a properly set up mem with a default memory map
/// Constructs a new mem
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mem = Mem::new();
/// assert!(mem.is_empty());
///# Ok(())
///# }
/// ```
pub fn new() -> Self {
Mem::default()
}
/// Gets the length of the mem' backing memory
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mem = Mem::new()
/// .add_region_owned(Program, 0..1234);
/// assert_eq!(1234, mem.len());
///# Ok(())
///# }
/// ```
pub fn len(&self) -> usize {
self.memory.len()
}
/// Returns true if the backing memory contains no elements
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mem = Mem::new();
/// assert!(mem.is_empty());
///# Ok(())
///# }
/// ```
pub fn is_empty(&self) -> bool {
self.memory.is_empty()
}
/// Grows the Mem backing memory to at least size bytes, but does not truncate
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mut mem = Mem::new();
/// mem.with_size(1234);
/// assert_eq!(1234, mem.len());
/// mem.with_size(0);
/// assert_eq!(1234, mem.len());
///# Ok(())
///# }
/// ```
pub fn with_size(&mut self, size: usize) {
if self.len() < size {
self.memory.resize(size, 0);
}
}
/// Adds a new names range ([Region]) to an owned [Mem]
pub fn add_region_owned(mut self, name: Region, range: Range<usize>) -> Self {
self.add_region(name, range);
self
}
/// Adds a new named range ([Region]) to a [Mem]
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mut mem = Mem::new();
/// mem.add_region(Program, 0..1234);
/// assert_eq!(1234, mem.len());
///# Ok(())
///# }
/// ```
pub fn add_region(&mut self, name: Region, range: Range<usize>) -> &mut Self {
self.with_size(range.end);
if let Some(region) = self.region.get_mut(name as usize) {
*region = Some(range);
}
self
}
/// Updates an existing [Region]
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mut mem = Mem::new().add_region_owned(Program, 0..1234);
/// mem.set_region(Program, 1234..2345);
/// assert_eq!(2345, mem.len());
///# Ok(())
///# }
/// ```
pub fn set_region(&mut self, name: Region, range: Range<usize>) -> &mut Self {
self.with_size(range.end);
if let Some(region) = self.region.get_mut(name as usize) {
*region = Some(range);
}
self
}
/// Loads data into a [Region] on an *owned* [Mem], for use during initialization
pub fn load_region_owned(mut self, name: Region, data: &[u8]) -> Self {
self.load_region(name, data).ok();
self
}
/// Loads data into a named [Region]
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mem = Mem::new()
/// .add_region_owned(Program, 0..1234)
/// .load_region(Program, b"Hello, world!")?;
///# // TODO: Test if region actually contains "Hello, world!"
///# Ok(())
///# }
/// ```
pub fn load_region(&mut self, name: Region, data: &[u8]) -> Result<&mut Self> {
use std::io::Write;
if let Some(mut region) = self.get_region_mut(name) {
assert_eq!(region.write(data)?, data.len());
}
Ok(self)
}
/// Fills a [Region] with zeroes
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mem = Mem::new()
/// .add_region_owned(Program, 0..1234)
/// .clear_region(Program);
///# // TODO: test if region actually clear
///# Ok(())
///# }
/// ```
/// If the region doesn't exist, that's okay.
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mem = Mem::new()
/// .add_region_owned(Program, 0..1234)
/// .clear_region(Program);
///# // TODO: test if region actually clear
///# Ok(())
///# }
/// ```
pub fn clear_region(&mut self, name: Region) -> &mut Self {
if let Some(region) = self.get_region_mut(name) {
region.fill(0)
}
self
}
/// Gets a slice of a named [Region] of memory
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mem = Mem::new()
/// .add_region_owned(Program, 0..10);
/// assert!([0;10].as_slice() == mem.get_region(Program).unwrap());
///# Ok(())
///# }
/// ```
#[inline(always)]
pub fn get_region(&self, name: Region) -> Option<&[u8]> {
debug_assert!(self.region.get(name as usize).is_some());
self.grab(self.region.get(name as usize)?.clone()?)
}
/// Gets a mutable slice of a named region of memory
/// # Examples
/// ```rust
/// use chirp::{cpu::mem::{Region::*, Mem}, *};
///# fn main() -> Result<()> {
/// let mut mem = Mem::new()
/// .add_region_owned(Program, 0..10);
/// assert!([0;10].as_slice() == mem.get_region_mut(Program).unwrap());
///# Ok(())
///# }
/// ```
#[inline(always)]
pub fn get_region_mut(&mut self, name: Region) -> Option<&mut [u8]> {
debug_assert!(self.region.get(name as usize).is_some());
self.grab_mut(self.region.get(name as usize)?.clone()?)
}
}
#[cfg(target_feature = "rhexdump")]
impl Display for Mem {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
use rhexdump::Rhexdump;
let mut rhx = Rhexdump::default();
rhx.set_bytes_per_group(2)
.expect("2 <= MAX_BYTES_PER_GROUP (8)");
rhx.display_duplicate_lines(false);
for (&name, range) in &self.region {
writeln!(
f,
"[{name}]\n{}\n",
rhx.hexdump(&self.memory[range.clone()])
)?
}
write!(f, "")
}
}

View File

@ -1,18 +1,23 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! Selects the memory behavior of the [super::CPU] //! Selects the memory behavior of the [super::CPU]
//! //!
//! Since [super::Quirks] implements [From<Mode>], //! Since [Quirks] implements [`From<Mode>`],
//! this can be used to select the appropriate quirk-set //! this can be used to select the appropriate quirk-set
use super::Quirks;
use crate::error::Error; use crate::error::Error;
use std::str::FromStr; use std::str::FromStr;
/// Selects the memory behavior of the interpreter /// Selects the memory behavior of the interpreter
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Mode { pub enum Mode {
/// VIP emulation mode /// VIP emulation mode
#[default] #[default]
Chip8, Chip8,
/// Chip-48 emulation mode /// Super Chip emulation mode
SChip, SChip,
/// XO-Chip emulation mode /// XO-Chip emulation mode
XOChip, XOChip,
@ -23,12 +28,54 @@ impl FromStr for Mode {
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() { match s.to_lowercase().as_str() {
"chip8" | "chip-8" => Ok(Mode::Chip8), "chip" | "chip8" | "chip-8" => Ok(Mode::Chip8),
"schip" | "superchip" => Ok(Mode::SChip), "s" | "schip" | "superchip" | "super chip" => Ok(Mode::SChip),
"xo-chip" | "xochip" => Ok(Mode::XOChip), "xo" | "xochip" | "xo-chip" => Ok(Mode::XOChip),
_ => Err(Error::InvalidMode { _ => Err(Error::InvalidMode { mode: s.into() }),
mode: s.to_string(), }
}), }
}
impl AsRef<str> for Mode {
fn as_ref(&self) -> &str {
match self {
Mode::Chip8 => "Chip-8",
Mode::SChip => "Super Chip",
Mode::XOChip => "XO-Chip",
}
}
}
impl ToString for Mode {
fn to_string(&self) -> String {
self.as_ref().into()
}
}
impl From<usize> for Mode {
fn from(value: usize) -> Self {
match value {
0 => Self::Chip8,
1 => Self::SChip,
2 => Self::XOChip,
_ => Self::Chip8,
}
}
}
impl From<Mode> for Quirks {
fn from(value: Mode) -> Self {
match value {
Mode::Chip8 => false.into(),
Mode::SChip => true.into(),
Mode::XOChip => Self {
bin_ops: true,
shift: false,
draw_wait: true,
screen_wrap: true,
dma_inc: false,
stupid_jumps: false,
},
} }
} }
} }

View File

@ -1,17 +1,25 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! Controls the [Quirks] behavior of the CPU on a granular level. //! Controls the [Quirks] behavior of the CPU on a granular level.
use super::Mode;
/// Controls the authenticity behavior of the CPU on a granular level. /// Controls the quirk behavior of the CPU on a granular level.
///
/// `false` is Cosmac-VIP-like behavior
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Quirks { pub struct Quirks {
/// Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0 /// Super Chip: Binary ops in `8xy`(`1`, `2`, `3`) shouldn't set vF to 0
pub bin_ops: bool, pub bin_ops: bool,
/// Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX /// Super Chip: Shift ops in `8xy`(`6`, `E`) shouldn't source from vY instead of vX
pub shift: bool, pub shift: bool,
/// Draw operations shouldn't pause execution until the next timer tick /// Super Chip: Draw operations shouldn't pause execution until the next timer tick
pub draw_wait: bool, pub draw_wait: bool,
/// DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1 /// XO-Chip: Draw operations should wrap from bottom to top and side to side
pub screen_wrap: bool,
/// Super Chip: DMA instructions `Fx55`/`Fx65` shouldn't change I to I + x + 1
pub dma_inc: bool, pub dma_inc: bool,
/// Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`. /// Super Chip: Indexed jump instructions should go to `adr` + v`a` where `a` is high nibble of `adr`.
pub stupid_jumps: bool, pub stupid_jumps: bool,
} }
@ -22,6 +30,7 @@ impl From<bool> for Quirks {
bin_ops: true, bin_ops: true,
shift: true, shift: true,
draw_wait: true, draw_wait: true,
screen_wrap: false,
dma_inc: true, dma_inc: true,
stupid_jumps: true, stupid_jumps: true,
} }
@ -30,6 +39,7 @@ impl From<bool> for Quirks {
bin_ops: false, bin_ops: false,
shift: false, shift: false,
draw_wait: false, draw_wait: false,
screen_wrap: false,
dma_inc: false, dma_inc: false,
stupid_jumps: false, stupid_jumps: false,
} }
@ -37,22 +47,6 @@ impl From<bool> for Quirks {
} }
} }
impl From<Mode> for Quirks {
fn from(value: Mode) -> Self {
match value {
Mode::Chip8 => false.into(),
Mode::SChip => true.into(),
Mode::XOChip => Self {
bin_ops: true,
shift: false,
draw_wait: true,
dma_inc: false,
stupid_jumps: false,
},
}
}
}
impl Default for Quirks { impl Default for Quirks {
fn default() -> Self { fn default() -> Self {
Self::from(false) Self::from(false)

View File

@ -1,5 +1,5 @@
// (c) 2023 John A. Breaux // (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE.txt for details) // This code is licensed under MIT license (see LICENSE for details)
//! Unit tests for [super::CPU] //! Unit tests for [super::CPU]
//! //!
@ -13,79 +13,72 @@
//! Some of these tests run >16M times, which is very silly //! Some of these tests run >16M times, which is very silly
use super::*; use super::*;
pub(self) use crate::{ use crate::Screen;
bus, use rand::random;
bus::{Bus, Region::*},
};
mod decode; mod decode;
fn setup_environment() -> (CPU, Bus) { fn setup_environment() -> (CPU, Screen) {
( let mut ch8 = (
CPU { CPU {
flags: Flags { flags: Flags {
debug: true, debug: true,
pause: false, pause: false,
monotonic: Some(8),
..Default::default() ..Default::default()
}, },
..CPU::default() ..CPU::default()
}, },
bus! { Screen::default(),
// Load the charset into ROM );
Charset [0x0050..0x00A0] = include_bytes!("../mem/charset.bin"), ch8.0
// Load the ROM file into RAM (dummy binary which contains nothing but `jmp pc+2`) .load_program_bytes(include_bytes!("tests/roms/jumptest.ch8"))
Program [0x0200..0x1000] = include_bytes!("tests/roms/jumptest.ch8"), .unwrap();
// Create a screen ch8
Screen [0x0F00..0x1000] = include_bytes!("../../chip8Archive/roms/1dcell.ch8"),
},
)
} }
fn print_screen(bytes: &[u8]) { fn print_screen(bytes: &[u8]) {
bus! {Screen [0..0x100] = bytes} Screen::from(bytes).print_screen()
.print_screen()
.expect("Printing screen should not fail if Screen exists.")
} }
/// Unused instructions /// Unused instructions
mod unimplemented { mod unimplemented {
use super::*; use super::*;
#[test] macro_rules! invalid {
fn ins_5xyn() { ($(
let (mut cpu, mut bus) = setup_environment(); $(#[$attr:meta])* // Preserve doc comments, etc.
bus.write(0x200u16, 0x500fu16); $pub:vis test $name:ident { $($insn:literal),+$(,)? }
cpu.tick(&mut bus) );+ $(;)?) => {
.expect_err("0x500f is not an instruction"); $( $(#[$attr])* #[test] $pub fn $name () {$(
let (mut cpu, mut screen) = setup_environment();
cpu.mem.write(0x200u16, $insn as u16);
cpu.tick(&mut screen)
.expect_err(stringify!($insn is not an instruction));
)*} )+
};
} }
#[test]
fn ins_8xyn() { invalid! {
let (mut cpu, mut bus) = setup_environment(); /// Invalid opcodes 5001, 0x5004-500f
bus.write(0x200u16, 0x800fu16); test ins_5xyn {
cpu.tick(&mut bus) 0x5001,
.expect_err("0x800f is not an instruction"); 0x5004, 0x5005, 0x5006, 0x5007, 0x5008, 0x5009,
} 0x500a, 0x500b, 0x500c, 0x500d, 0x500e, 0x500f,
#[test] };
fn ins_9xyn() { /// Invalid opcodes 8008-800d and 800f
let (mut cpu, mut bus) = setup_environment(); test ins_8xyn {
bus.write(0x200u16, 0x900fu16); 0x8008, 0x8009, 0x800a, 0x800b, 0x800c, 0x800d,
cpu.tick(&mut bus) 0x800f,
.expect_err("0x900f is not an instruction"); };
} /// Invalid opcodes 9001-900f
#[test] test ins_9xyn {
fn ins_exbb() { 0x9001, 0x9002, 0x9003, 0x9004, 0x9005,
let (mut cpu, mut bus) = setup_environment(); 0x9006, 0x9007, 0x9008, 0x9009, 0x900a,
bus.write(0x200u16, 0xe00fu16); 0x900b, 0x900c, 0x900d, 0x900e, 0x900f,
cpu.tick(&mut bus) };
.expect_err("0xe00f is not an instruction"); /// Invalid opcodes in e000 range
} test ins_exbb { 0xe00f };
// Fxbb /// Invalid opcodes in the 0xf000 range
#[test] test ins_fxbb { 0xf001, 0xf00f };
fn ins_fxbb() {
let (mut cpu, mut bus) = setup_environment();
bus.write(0x200u16, 0xf00fu16);
cpu.tick(&mut bus)
.expect_err("0xf00f is not an instruction");
} }
} }
@ -94,10 +87,11 @@ mod sys {
/// 00e0: Clears the screen memory to 0 /// 00e0: Clears the screen memory to 0
#[test] #[test]
fn clear_screen() { fn clear_screen() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.clear_screen(&mut bus); cpu.clear_screen(&mut screen);
bus.get_region(Screen) screen
.expect("Expected screen, got None") .grab(..)
.unwrap()
.iter() .iter()
.for_each(|byte| assert_eq!(*byte, 0)); .for_each(|byte| assert_eq!(*byte, 0));
} }
@ -106,17 +100,14 @@ mod sys {
#[test] #[test]
fn ret() { fn ret() {
let test_addr = random::<u16>() & 0x7ff; let test_addr = random::<u16>() & 0x7ff;
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, _) = setup_environment();
let sp_orig = cpu.sp;
// Place the address on the stack // Place the address on the stack
bus.write(cpu.sp.wrapping_add(2), test_addr); cpu.stack.push(test_addr);
cpu.ret(&bus); cpu.ret();
// Verify the current address is the address from the stack // Verify the current address is the address from the stack
assert_eq!(test_addr, cpu.pc); assert_eq!(test_addr, cpu.pc);
// Verify the stack pointer has moved
assert!(dbg!(cpu.sp.wrapping_sub(sp_orig)) == 0x2);
} }
} }
@ -132,9 +123,9 @@ mod cf {
let (mut cpu, _) = setup_environment(); let (mut cpu, _) = setup_environment();
// Test all valid addresses // Test all valid addresses
for addr in 0x000..0xffe { for addr in 0x000..0xffe {
// Call an address // Jump to an address
cpu.jump(addr); cpu.jump(addr);
// Verify the current address is the called address // Verify the current address is the jump target address
assert_eq!(addr, cpu.pc); assert_eq!(addr, cpu.pc);
} }
} }
@ -143,15 +134,15 @@ mod cf {
#[test] #[test]
fn call() { fn call() {
let test_addr = random::<u16>(); let test_addr = random::<u16>();
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, _) = setup_environment();
// Save the current address // Save the current address
let curr_addr = cpu.pc; let curr_addr = cpu.pc;
// Call an address // Call an address
cpu.call(test_addr, &mut bus); cpu.call(test_addr);
// Verify the current address is the called address // Verify the current address is the called address
assert_eq!(test_addr, cpu.pc); assert_eq!(test_addr, cpu.pc);
// Verify the previous address was stored on the stack (sp+2) // Verify the previous address was stored on the stack (sp+2)
let stack_addr: u16 = bus.read(cpu.sp.wrapping_add(2)); let stack_addr: u16 = cpu.stack.pop().expect("This should return test_addr");
assert_eq!(stack_addr, curr_addr); assert_eq!(stack_addr, curr_addr);
} }
@ -595,7 +586,7 @@ mod math {
for reg in 0..=0xff { for reg in 0..=0xff {
let (x, y) = (reg & 0xf, reg >> 4); let (x, y) = (reg & 0xf, reg >> 4);
// set the register under test to `word` // set the register under test to `word`
(cpu.v[x], cpu.v[y]) = dbg!(0, word); (cpu.v[x], cpu.v[y]) = (0, word);
cpu.shift_left(x, y); cpu.shift_left(x, y);
@ -674,9 +665,9 @@ mod i {
/// - Random number generation /// - Random number generation
/// - Drawing to the display /// - Drawing to the display
mod io { mod io {
use super::*;
use std::io::Write; use std::io::Write;
use super::*;
/// Cxbb: Stores a random number & the provided byte into vX /// Cxbb: Stores a random number & the provided byte into vX
#[test] #[test]
fn rand() { fn rand() {
@ -707,11 +698,12 @@ mod io {
ScreenTest { ScreenTest {
program: include_bytes!("../../chip-8/IBM Logo.ch8"), program: include_bytes!("../../chip-8/IBM Logo.ch8"),
screen: include_bytes!("tests/screens/IBM Logo.ch8/20.bin"), screen: include_bytes!("tests/screens/IBM Logo.ch8/20.bin"),
steps: 56, steps: 21,
quirks: Quirks { quirks: Quirks {
bin_ops: false, bin_ops: false,
shift: false, shift: false,
draw_wait: false, draw_wait: false,
screen_wrap: true,
dma_inc: false, dma_inc: false,
stupid_jumps: false, stupid_jumps: false,
}, },
@ -727,6 +719,7 @@ mod io {
bin_ops: false, bin_ops: false,
shift: false, shift: false,
draw_wait: true, draw_wait: true,
screen_wrap: true,
dma_inc: false, dma_inc: false,
stupid_jumps: false, stupid_jumps: false,
}, },
@ -740,6 +733,7 @@ mod io {
bin_ops: false, bin_ops: false,
shift: false, shift: false,
draw_wait: true, draw_wait: true,
screen_wrap: true,
dma_inc: false, dma_inc: false,
stupid_jumps: false, stupid_jumps: false,
}, },
@ -750,26 +744,21 @@ mod io {
#[test] #[test]
fn draw() { fn draw() {
for test in SCREEN_TESTS { for test in SCREEN_TESTS {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.flags.quirks = test.quirks; cpu.flags.quirks = test.quirks;
// Debug mode is 5x slower // Debug mode is 5x slower
cpu.flags.debug = false; cpu.flags.debug = false;
// Load the test program // Load the test program
bus = bus.load_region(Program, test.program); cpu.mem.load_region(Program, test.program).unwrap();
// Run the test program for the specified number of steps // Run the test program for the specified number of steps
while cpu.cycle() < test.steps { while cpu.cycle() < test.steps {
cpu.multistep(&mut bus, test.steps - cpu.cycle()) cpu.multistep(&mut screen, 10.min(test.steps - cpu.cycle()))
.expect("Draw tests should not contain undefined instructions"); .expect("Draw tests should not contain undefined instructions");
} }
// Compare the screen to the reference screen buffer // Compare the screen to the reference screen buffer
bus.print_screen() screen.print_screen();
.expect("Printing screen should not fail if screen exists");
print_screen(test.screen); print_screen(test.screen);
assert_eq!( assert_eq!(screen.grab(..).unwrap(), test.screen);
bus.get_region(Screen)
.expect("Getting screen should not fail if screen exists"),
test.screen
);
} }
} }
} }
@ -860,7 +849,7 @@ mod io {
assert!(cpu.release(key).expect("Key should be released")); assert!(cpu.release(key).expect("Key should be released"));
assert!(!cpu.release(key).expect("Key shouldn't be released again")); assert!(!cpu.release(key).expect("Key shouldn't be released again"));
assert!(!cpu.flags.keypause); assert!(!cpu.flags.keypause);
assert_eq!(Some(key), cpu.flags.lastkey); assert_eq!(Some(key), cpu.lastkey);
cpu.wait_for_key(x); cpu.wait_for_key(x);
assert_eq!(key as u8, cpu.v[x]); assert_eq!(key as u8, cpu.v[x]);
} }
@ -875,8 +864,7 @@ mod io {
for word in 0..=0xff { for word in 0..=0xff {
for x in 0..=0xf { for x in 0..=0xf {
// set the register under test to `word` // set the register under test to `word`
cpu.delay = word as f64; cpu.delay = word;
cpu.load_delay_timer(x); cpu.load_delay_timer(x);
assert_eq!(cpu.v[x], word); assert_eq!(cpu.v[x], word);
@ -895,7 +883,7 @@ mod io {
cpu.store_delay_timer(x); cpu.store_delay_timer(x);
assert_eq!(cpu.delay, word as f64); assert_eq!(cpu.delay, word);
} }
} }
} }
@ -911,7 +899,7 @@ mod io {
cpu.store_sound_timer(x); cpu.store_sound_timer(x);
assert_eq!(cpu.sound, word as f64); assert_eq!(cpu.sound, word);
} }
} }
} }
@ -948,7 +936,7 @@ mod io {
/// Fx29: Load sprite for character vX into I /// Fx29: Load sprite for character vX into I
#[test] #[test]
fn load_sprite() { fn load_sprite() {
let (mut cpu, bus) = setup_environment(); let (mut cpu, _) = setup_environment();
for test in TESTS { for test in TESTS {
let reg = 0xf & random::<usize>(); let reg = 0xf & random::<usize>();
// load number into CPU register // load number into CPU register
@ -958,7 +946,8 @@ mod io {
let addr = cpu.i as usize; let addr = cpu.i as usize;
assert_eq!( assert_eq!(
bus.get(addr..addr.wrapping_add(5)) cpu.mem
.grab(addr..addr.wrapping_add(5))
.expect("Region at addr should exist!"), .expect("Region at addr should exist!"),
test.output, test.output,
); );
@ -967,7 +956,7 @@ mod io {
} }
mod bcdtest { mod bcdtest {
pub(self) use super::*; use super::*;
struct BCDTest { struct BCDTest {
// value to test // value to test
@ -995,15 +984,18 @@ mod io {
#[test] #[test]
fn bcd_convert() { fn bcd_convert() {
for test in BCD_TESTS { for test in BCD_TESTS {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, _) = setup_environment();
let addr = 0xff0 & random::<u16>() as usize; let addr = 0xff0 & random::<u16>() as usize;
// load CPU registers // load CPU registers
cpu.i = addr as u16; cpu.i = addr as u16;
cpu.v[5] = test.input; cpu.v[5] = test.input;
cpu.bcd_convert(5, &mut bus); cpu.bcd_convert(5);
assert_eq!(bus.get(addr..addr.saturating_add(3)), Some(test.output)) assert_eq!(
cpu.mem.grab(addr..addr.saturating_add(3)),
Some(test.output)
)
} }
} }
} }
@ -1012,7 +1004,7 @@ mod io {
// TODO: Test with dma_inc quirk set // TODO: Test with dma_inc quirk set
#[test] #[test]
fn dma_store() { fn dma_store() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, _) = setup_environment();
const DATA: &[u8] = b"ABCDEFGHIJKLMNOP"; const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
// Load some test data into memory // Load some test data into memory
let addr = 0x456; let addr = 0x456;
@ -1023,15 +1015,16 @@ mod io {
for len in 0..16 { for len in 0..16 {
// Perform DMA store // Perform DMA store
cpu.i = addr as u16; cpu.i = addr as u16;
cpu.store_dma(len, &mut bus); cpu.store_dma(len);
// Check that bus grabbed the correct data // Check that screen grabbed the correct data
let bus = bus let screen = cpu
.get_mut(addr..addr + DATA.len()) .mem
.grab_mut(addr..addr + DATA.len())
.expect("Getting a mutable slice at addr 0x0456 should not fail"); .expect("Getting a mutable slice at addr 0x0456 should not fail");
assert_eq!(bus[0..=len], DATA[0..=len]); assert_eq!(screen[0..=len], DATA[0..=len]);
assert_eq!(bus[len + 1..], [0; 16][len + 1..]); assert_eq!(screen[len + 1..], [0; 16][len + 1..]);
// clear // clear
bus.fill(0); screen.fill(0);
} }
} }
@ -1039,18 +1032,19 @@ mod io {
// TODO: Test with dma_inc quirk set // TODO: Test with dma_inc quirk set
#[test] #[test]
fn dma_load() { fn dma_load() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, _) = setup_environment();
const DATA: &[u8] = b"ABCDEFGHIJKLMNOP"; const DATA: &[u8] = b"ABCDEFGHIJKLMNOP";
// Load some test data into memory // Load some test data into memory
let addr = 0x456; let addr = 0x456;
bus.get_mut(addr..addr + DATA.len()) cpu.mem
.grab_mut(addr..addr + DATA.len())
.expect("Getting a mutable slice at addr 0x0456..0x0466 should not fail") .expect("Getting a mutable slice at addr 0x0456..0x0466 should not fail")
.write_all(DATA) .write_all(DATA)
.unwrap(); .unwrap();
for len in 0..16 { for len in 0..16 {
// Perform DMA load // Perform DMA load
cpu.i = addr as u16; cpu.i = addr as u16;
cpu.load_dma(len, &mut bus); cpu.load_dma(len);
// Check that registers grabbed the correct data // Check that registers grabbed the correct data
assert_eq!(cpu.v[0..=len], DATA[0..=len]); assert_eq!(cpu.v[0..=len], DATA[0..=len]);
assert_eq!(cpu.v[len + 1..], [0; 16][len + 1..]); assert_eq!(cpu.v[len + 1..], [0; 16][len + 1..]);
@ -1068,37 +1062,37 @@ mod behavior {
use std::time::Duration; use std::time::Duration;
#[test] #[test]
fn delay() { fn delay() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.flags.monotonic = None; cpu.flags.monotonic = false;
cpu.delay = 10.0; cpu.delay = 10;
for _ in 0..2 { for _ in 0..2 {
cpu.multistep(&mut bus, 8) cpu.multistep(&mut screen, 8)
.expect("Running valid instructions should always succeed"); .expect("Running valid instructions should always succeed");
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0)); std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
} }
// time is within 1 frame deviance over a theoretical 2 frame pause // time is within 1 frame deviance over a theoretical 2 frame pause
assert!(7.0 <= cpu.delay && cpu.delay <= 9.0); assert_eq!(cpu.delay, 8);
} }
#[test] #[test]
fn sound() { fn sound() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.flags.monotonic = None; // disable monotonic timing cpu.flags.monotonic = false; // disable monotonic timing
cpu.sound = 10.0; cpu.sound = 10;
for _ in 0..2 { for _ in 0..2 {
cpu.multistep(&mut bus, 8) cpu.multistep(&mut screen, 8)
.expect("Running valid instructions should always succeed"); .expect("Running valid instructions should always succeed");
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0)); std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
} }
// time is within 1 frame deviance over a theoretical 2 frame pause // time is within 1 frame deviance over a theoretical 2 frame pause
assert!(7.0 <= cpu.sound && cpu.sound <= 9.0); assert_eq!(cpu.sound, 8);
} }
#[test] #[test]
fn vbi_wait() { fn vbi_wait() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.flags.monotonic = None; // disable monotonic timing cpu.flags.monotonic = false; // disable monotonic timing
cpu.flags.draw_wait = true; cpu.flags.draw_wait = true;
for _ in 0..2 { for _ in 0..2 {
cpu.multistep(&mut bus, 8) cpu.multistep(&mut screen, 8)
.expect("Running valid instructions should always succeed"); .expect("Running valid instructions should always succeed");
std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0)); std::thread::sleep(Duration::from_secs_f64(1.0 / 60.0));
} }
@ -1112,9 +1106,9 @@ mod behavior {
#[test] #[test]
#[cfg_attr(feature = "unstable", no_coverage)] #[cfg_attr(feature = "unstable", no_coverage)]
fn hit_break() { fn hit_break() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.set_break(0x202); cpu.set_break(0x202);
match cpu.multistep(&mut bus, 10) { match cpu.multistep(&mut screen, 10) {
Err(crate::error::Error::BreakpointHit { addr, next }) => { Err(crate::error::Error::BreakpointHit { addr, next }) => {
assert_eq!(0x202, addr); // current address is 202 assert_eq!(0x202, addr); // current address is 202
assert_eq!(0x1204, next); // next insn is `jmp 204` assert_eq!(0x1204, next); // next insn is `jmp 204`
@ -1127,9 +1121,9 @@ mod behavior {
#[test] #[test]
#[cfg_attr(feature = "unstable", no_coverage)] #[cfg_attr(feature = "unstable", no_coverage)]
fn hit_break_singlestep() { fn hit_break_singlestep() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
cpu.set_break(0x202); cpu.set_break(0x202);
match cpu.singlestep(&mut bus) { match cpu.singlestep(&mut screen) {
Err(crate::error::Error::BreakpointHit { addr, next }) => { Err(crate::error::Error::BreakpointHit { addr, next }) => {
assert_eq!(0x202, addr); // current address is 202 assert_eq!(0x202, addr); // current address is 202
assert_eq!(0x1204, next); // next insn is `jmp 204` assert_eq!(0x1204, next); // next insn is `jmp 204`
@ -1144,12 +1138,12 @@ mod behavior {
#[test] #[test]
#[cfg_attr(feature = "unstable", no_coverage)] #[cfg_attr(feature = "unstable", no_coverage)]
fn invalid_pc() { fn invalid_pc() {
let (mut cpu, mut bus) = setup_environment(); let (mut cpu, mut screen) = setup_environment();
// The bus extends from 0x0..0x1000 // The screen extends from 0x0..0x1000
cpu.pc = 0xfff; cpu.pc = 0x1001;
match cpu.tick(&mut bus) { match cpu.tick(&mut screen) {
Err(Error::InvalidBusRange { range }) => { Err(Error::InvalidAddressRange { range }) => {
eprintln!("InvalidBusRange {{ {range:04x?} }}") eprintln!("InvalidAddressRange {{ {range:04x?} }}")
} }
other => unreachable!("{other:04x?}"), other => unreachable!("{other:04x?}"),
} }

View File

@ -1,3 +1,6 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! Exercises the instruction decode logic. //! Exercises the instruction decode logic.
use super::*; use super::*;
@ -6,15 +9,15 @@ const INDX: &[u8; 16] = b"\0\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d
/// runs one arbitrary operation on a brand new CPU /// runs one arbitrary operation on a brand new CPU
/// returns the CPU for inspection /// returns the CPU for inspection
fn run_single_op(op: &[u8]) -> CPU { fn run_single_op(op: &[u8]) -> CPU {
let (mut cpu, mut bus) = ( let (mut cpu, mut screen) = (
CPU::default(), CPU::default(),
bus! { Screen::default(),
Program[0x200..0x240] = op,
},
); );
cpu.mem
.load_region(Program, op).unwrap();
cpu.v = *INDX; cpu.v = *INDX;
cpu.flags.quirks = Quirks::from(false); cpu.flags.quirks = Quirks::from(false);
cpu.tick(&mut bus).unwrap(); // will panic if unimplemented cpu.tick(&mut screen).unwrap(); // will panic if unimplemented
cpu cpu
} }

View File

@ -1,11 +1,12 @@
// (c) 2023 John A. Breaux // (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE.txt for details) // This code is licensed under MIT license (see LICENSE for details)
//! Error type for Chirp //! Error type for Chirp
use std::ops::Range; pub mod any_range;
use any_range::AnyRange;
use crate::bus::Region; use crate::cpu::mem::Region;
use thiserror::Error; use thiserror::Error;
/// Result type, equivalent to [std::result::Result]<T, [enum@Error]> /// Result type, equivalent to [std::result::Result]<T, [enum@Error]>
@ -15,7 +16,7 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
/// Represents a breakpoint being hit /// Represents a breakpoint being hit
#[error("Breakpoint hit: {addr:03x} ({next:04x})")] #[error("breakpoint hit: {addr:03x} ({next:04x})")]
BreakpointHit { BreakpointHit {
/// The address of the breakpoint /// The address of the breakpoint
addr: u16, addr: u16,
@ -23,37 +24,37 @@ pub enum Error {
next: u16, next: u16,
}, },
/// Represents an unimplemented operation /// Represents an unimplemented operation
#[error("Unrecognized opcode: {word:04x}")] #[error("opcode {word:04x} not recognized")]
UnimplementedInstruction { UnimplementedInstruction {
/// The offending word /// The offending word
word: u16, word: u16,
}, },
/// The region you asked for was not defined /// The region you asked for was not defined
#[error("No {region} found on bus")] #[error("region {region} is not present on bus")]
MissingRegion { MissingRegion {
/// The offending [Region] /// The offending [Region]
region: Region, region: Region,
}, },
/// Tried to fetch [Range] from bus, received nothing /// Tried to fetch data at [AnyRange] from bus, received nothing
#[error("Invalid range {range:04x?} for bus")] #[error("range {range:04x?} is not present on bus")]
InvalidBusRange { InvalidAddressRange {
/// The offending [Range] /// The offending [AnyRange]
range: Range<usize>, range: AnyRange<usize>,
}, },
/// Tried to press a key that doesn't exist /// Tried to press a key that doesn't exist
#[error("Invalid key: {key:X}")] #[error("tried to press key {key:X} which does not exist")]
InvalidKey { InvalidKey {
/// The offending key /// The offending key
key: usize, key: usize,
}, },
/// Tried to get/set an out-of-bounds register /// Tried to get/set an out-of-bounds register
#[error("Invalid register: v{reg:X}")] #[error("tried to access register v{reg:X} which does not exist")]
InvalidRegister { InvalidRegister {
/// The offending register /// The offending register
reg: usize, reg: usize,
}, },
/// Tried to convert string into mode, but it did not match. /// Tried to convert string into mode, but it did not match.
#[error("Invalid mode: {mode}")] #[error("no suitable conversion of \"{mode}\" into Mode")]
InvalidMode { InvalidMode {
/// The string which failed to become a mode /// The string which failed to become a mode
mode: String, mode: String,

69
src/error/any_range.rs Normal file
View File

@ -0,0 +1,69 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! Holder for any [Range]
// This is super over-engineered, considering it was originally meant to help with only one LOC
use std::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive};
use thiserror::Error;
#[macro_use]
mod macros;
#[derive(Clone, Debug, Error, PartialEq, Eq, Hash)]
#[error("Failed to convert variant {0} back into range.")]
/// Emitted when conversion back into a [std::ops]::Range\* fails.
pub struct AnyRangeError<Idx>(AnyRange<Idx>);
/// Holder for any [Range]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum AnyRange<Idx> {
/// Bounded exclusive [Range] i.e. `0..10`
Range(Range<Idx>),
/// Unbounded [RangeFrom], i.e. `0..`
RangeFrom(RangeFrom<Idx>),
/// Unbounded [RangeFull], i.e. `..`
RangeFull(RangeFull),
/// Bounded inclusive [RangeInclusive], i.e. `0..=10`
RangeInclusive(RangeInclusive<Idx>),
/// Unbounded [RangeTo], i.e. `..10`
RangeTo(RangeTo<Idx>),
/// Unbounded inclusive [RangeToInclusive], i.e. `..=10`
RangeToInclusive(RangeToInclusive<Idx>),
}
variant_from! {
match impl<Idx> (From, TryInto) for AnyRange<Idx> {
type Error = AnyRangeError<Idx>;
Range<Idx> => AnyRange::Range,
RangeFrom<Idx> => AnyRange::RangeFrom,
RangeFull => AnyRange::RangeFull,
RangeInclusive<Idx> => AnyRange::RangeInclusive,
RangeTo<Idx> => AnyRange::RangeTo,
RangeToInclusive<Idx> => AnyRange::RangeToInclusive,
}
}
/// Convenient conversion functions from [AnyRange] to the inner type
impl<Idx> AnyRange<Idx> {
try_into_fn! {
/// Converts from [AnyRange::Range] into a [Range], else [None]
pub fn range(self) -> Option<Range<Idx>>;
/// Converts from [AnyRange::RangeFrom] into a [RangeFrom], else [None]
pub fn range_from(self) -> Option<RangeFrom<Idx>>;
/// Converts from [AnyRange::RangeFull] into a [RangeFull], else [None]
pub fn range_full(self) -> Option<RangeFull>;
/// Converts from [AnyRange::RangeInclusive] into a [RangeInclusive], else [None]
pub fn range_inclusive(self) -> Option<RangeInclusive<Idx>>;
/// Converts from [AnyRange::RangeTo] into a [RangeTo], else [None]
pub fn range_to(self) -> Option<RangeTo<Idx>>;
/// Converts from [AnyRange::RangeToInclusive] into a [RangeToInclusive], else [None]
pub fn range_to_inclusive(self) -> Option<RangeToInclusive<Idx>>;
}
}
impl<Idx> From<AnyRange<Idx>> for AnyRangeError<Idx> {
fn from(value: AnyRange<Idx>) -> Self {
AnyRangeError(value)
}
}

View File

@ -0,0 +1,33 @@
//! Macros for AnyRange
/// Generate From and TryFrom impls for each variant
macro_rules! variant_from {(
match impl<$T:ident> $_:tt for $enum:path {
type Error = $error:path;
$($src:path => $variant:path),+$(,)?
}
) => {
// The forward direction is infallible
$(impl<$T> From<$src> for $enum {
fn from(value: $src) -> Self { $variant(value) }
})+
// The reverse direction could fail if the $variant doesn't hold $src
$(impl<$T> TryFrom<$enum> for $src {
type Error = $error;
fn try_from(value: $enum) -> Result<Self, Self::Error> {
if let $variant(r) = value { Ok(r) } else { Err(value.into()) }
}
})+
}}
/// Turns a list of function prototypes into functions which use [TryInto], returning [Option]
///
/// # Examples
/// ```rust,ignore
/// try_into_fn! { fn range(self) -> Option<Other>; }
/// ```
macro_rules! try_into_fn {
($($(#[$doc:meta])? $pub:vis fn $name:ident $args:tt -> $ret:ty);+ $(;)?) => {
$($(#[$doc])? $pub fn $name $args -> $ret { $args.try_into().ok() })+
}
}

View File

@ -1,6 +1,6 @@
// (c) 2023 John A. Breaux // (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE.txt for details) // This code is licensed under MIT license (see LICENSE for details)
#![cfg_attr(feature = "unstable", feature(no_coverage))] #![cfg_attr(feature = "nightly", feature(no_coverage))]
#![deny(missing_docs, clippy::all)] #![deny(missing_docs, clippy::all)]
//! This crate implements a Chip-8 interpreter as if it were a real CPU architecture, //! This crate implements a Chip-8 interpreter as if it were a real CPU architecture,
//! to the best of my current knowledge. As it's the first emulator project I've //! to the best of my current knowledge. As it's the first emulator project I've
@ -8,26 +8,19 @@
//! //!
//! Hopefully, though, you'll find some use in it. //! Hopefully, though, you'll find some use in it.
pub mod bus;
pub mod cpu; pub mod cpu;
pub mod error; pub mod error;
pub mod screen;
pub mod traits;
// Common imports for Chirp // Common imports for Chirp
pub use bus::{Bus, Read, Region::*, Write};
pub use cpu::{ pub use cpu::{
disassembler::{Dis, Disassembler},
flags::Flags, flags::Flags,
instruction::disassembler::{Dis, Disassembler},
mode::Mode, mode::Mode,
quirks::Quirks, quirks::Quirks,
CPU, CPU,
}; };
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use screen::Screen;
/// Holds the state of a Chip-8 pub use traits::{AutoCast, FallibleAutoCast, Grab};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Chip8 {
/// Contains the registers, flags, and operating state for a single Chip-8
pub cpu: cpu::CPU,
/// Contains the memory of a chip-8
pub bus: bus::Bus,
}

BIN
src/mem/hires.bin Normal file

Binary file not shown.

132
src/screen.rs Normal file
View File

@ -0,0 +1,132 @@
//! Contains the raw screen bytes, and an iterator over individual bits of those bytes.
use crate::traits::Grab;
/// Stores the screen bytes
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Screen {
/// The screen bytes
bytes: Vec<u8>,
}
impl<T: AsRef<[u8]>> From<T> for Screen {
fn from(value: T) -> Self {
Screen {
bytes: value.as_ref().into(),
}
}
}
impl Default for Screen {
fn default() -> Self {
Self::new(256)
}
}
impl Grab for Screen {
fn grab<I>(&self, index: I) -> Option<&<I as std::slice::SliceIndex<[u8]>>::Output>
where
I: std::slice::SliceIndex<[u8]>,
{
self.bytes.get(index)
}
fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as std::slice::SliceIndex<[u8]>>::Output>
where
I: std::slice::SliceIndex<[u8]>,
{
self.bytes.get_mut(index)
}
}
impl Screen {
/// Creates a new [Screen]
pub fn new(size: usize) -> Self {
Self {
bytes: vec![0; size],
}
}
/// Returns true if the screen has 0 elements
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
/// Gets the length of the [Screen]
pub fn len(&self) -> usize {
self.bytes.len()
}
/// Gets a slice of the whole [Screen]
pub fn as_slice(&self) -> &[u8] {
self.bytes.as_slice()
}
/// Clears the [Screen] to 0
pub fn clear(&mut self) {
self.bytes.fill(0);
}
/// Grows the [Screen] memory to at least size bytes, but does not truncate
/// # Examples
/// ```rust
///# use chirp::screen::Screen;
/// let mut screen = Screen::new(256);
/// assert_eq!(256, screen.len());
/// screen.with_size(0);
/// assert_eq!(256, screen.len());
/// ```
pub fn with_size(&mut self, size: usize) {
if self.len() < size {
self.bytes.resize(size, 0);
}
}
/// Prints the [Screen] using either [drawille] or Box Drawing Characters
/// # Examples
/// [Screen::print_screen] will print the screen
/// ```rust
///# use chirp::screen::Screen;
/// let screen = Screen::default();
/// screen.print_screen();
/// ```
pub fn print_screen(&self) {
let len_log2 = self.bytes.len().ilog2() / 2;
#[allow(unused_variables)]
let (width, height) = (2u32.pow(len_log2 - 1), 2u32.pow(len_log2 + 1) - 1);
// draw with the drawille library, if available
#[cfg(feature = "drawille")]
{
use drawille::Canvas;
let mut canvas = Canvas::new(width * 8, height);
let width = width * 8;
self.bytes
.iter()
.enumerate()
.flat_map(|(bytei, byte)| {
(0..8).enumerate().filter_map(move |(biti, bit)| {
if (byte << bit) & 0x80 != 0 {
Some(bytei * 8 + biti)
} else {
None
}
})
})
.for_each(|index| canvas.set(index as u32 % (width), index as u32 / (width)));
println!("{}", canvas.frame());
}
#[cfg(not(feature = "drawille"))]
for (index, byte) in self.bytes.iter().enumerate() {
if index % width as usize == 0 {
print!("{index:03x}|");
}
print!(
"{}",
format!("{byte:08b}").replace('0', " ").replace('1', "")
);
if index % width as usize == width as usize - 1 {
println!("|");
}
}
}
}

7
src/traits.rs Normal file
View File

@ -0,0 +1,7 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! Traits useful for Chirp
mod auto_cast;
pub use auto_cast::{AutoCast, FallibleAutoCast, Grab};

106
src/traits/auto_cast.rs Normal file
View File

@ -0,0 +1,106 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
//! Traits for automatically serializing and deserializing Rust primitive types.
//!
//! Users of this module should impl [Grab]`<u8>` for their type, which notably returns `&[u8]` and `&mut [u8]`
#[allow(unused_imports)]
use core::mem::size_of;
use std::{fmt::Debug, slice::SliceIndex};
/// Get Raw Bytes at [SliceIndex] `I`.
///
/// This is similar to the [SliceIndex] method `.get(...)`, however implementing this
/// trait will auto-impl [AutoCast]<([i8], [u8], [i16], [u16] ... [i128], [u128])>
pub trait Grab {
/// Gets the slice of Self at [SliceIndex] I
fn grab<I>(&self, index: I) -> Option<&<I as SliceIndex<[u8]>>::Output>
where
I: SliceIndex<[u8]>;
/// Gets a mutable slice of Self at [SliceIndex] I
fn grab_mut<I>(&mut self, index: I) -> Option<&mut <I as SliceIndex<[u8]>>::Output>
where
I: SliceIndex<[u8]>;
}
/// Read or Write a T at address `addr`
pub trait AutoCast<T>: FallibleAutoCast<T> {
/// Reads a T from address `addr`
///
/// # May Panic
///
/// This will panic on error. For a non-panicking implementation, do it yourself.
fn read(&self, addr: impl Into<usize>) -> T {
self.read_fallible(addr).unwrap_or_else(|e| panic!("{e:?}"))
}
/// Writes a T to address `addr`
///
/// # Will Panic
///
/// This will panic on error. For a non-panicking implementation, do it yourself.
fn write(&mut self, addr: impl Into<usize>, data: T) {
self.write_fallible(addr, data)
.unwrap_or_else(|e| panic!("{e:?}"));
}
}
/// Read a T from address `addr`, and return the value as a [Result]
pub trait FallibleAutoCast<T>: Grab {
/// The [Err] type
type Error: Debug;
/// Read a T from address `addr`, returning the value as a [Result]
fn read_fallible(&self, addr: impl Into<usize>) -> Result<T, Self::Error>;
/// Write a T to address `addr`, returning the value as a [Result]
fn write_fallible(&mut self, addr: impl Into<usize>, data: T) -> Result<(), Self::Error>;
}
/// Implements Read and Write for the provided types
///
/// Relies on inherent methods of Rust numeric types:
/// - `Self::from_be_bytes`
/// - `Self::to_be_bytes`
macro_rules! impl_rw {($($t:ty) ,* $(,)?) =>{
$(
#[doc = concat!("Read or Write [`", stringify!($t), "`] at address `addr`, *discarding errors*.\n\nThis will never panic.")]
impl<T: Grab + FallibleAutoCast<$t>> AutoCast<$t> for T {
#[inline(always)]
fn read(&self, addr: impl Into<usize>) -> $t {
self.read_fallible(addr).ok().unwrap_or_default()
}
#[inline(always)]
fn write(&mut self, addr: impl Into<usize>, data: $t) {
self.write_fallible(addr, data).ok();
}
}
impl<T: Grab> FallibleAutoCast<$t> for T {
type Error = $crate::error::Error;
#[inline(always)]
fn read_fallible(&self, addr: impl Into<usize>) -> $crate::error::Result<$t> {
let addr: usize = addr.into();
let top = addr + core::mem::size_of::<$t>();
if let Some(bytes) = self.grab(addr..top) {
// Chip-8 is a big-endian system
Ok(<$t>::from_be_bytes(bytes.try_into()?))
} else {
Err($crate::error::Error::InvalidAddressRange{range: (addr..top).into()})
}
}
#[inline(always)]
fn write_fallible(&mut self, addr: impl Into<usize>, data: $t) -> std::result::Result<(), Self::Error> {
let addr: usize = addr.into();
if let Some(slice) = self.grab_mut(addr..addr + core::mem::size_of::<$t>()) {
// Chip-8 is a big-endian system
data.to_be_bytes().as_mut().swap_with_slice(slice);
}
Ok(())
}
}
)*
}}
// Using macro to be "generic" over types without traits in common
impl_rw!(i8, i16, i32, i64, i128);
impl_rw!(u8, u16, u32, u64, u128);
impl_rw!(f32, f64);

View File

@ -1,46 +1,42 @@
// (c) 2023 John A. Breaux
// This code is licensed under MIT license (see LICENSE for details)
// When compiled, the resulting binary is licensed under version 3 of the GNU General Public License (see chip8-test-suite/LICENSE for details)
//! These are a series of interpreter tests using Timendus's incredible test suite //! These are a series of interpreter tests using Timendus's incredible test suite
pub use chirp::*; pub use chirp::*;
fn setup_environment() -> (CPU, Bus) { fn setup_environment() -> (CPU, Screen) {
let mut cpu = CPU::default(); let mut cpu = CPU::default();
cpu.flags = Flags { cpu.flags = Flags {
debug: true, debug: true,
pause: false, pause: false,
monotonic: Some(8),
..Default::default() ..Default::default()
}; };
( (cpu, Screen::default())
cpu,
bus! {
// Load the charset into ROM
Charset [0x0050..0x00A0] = include_bytes!("../src/mem/charset.bin"),
// Load the ROM file into RAM
Program [0x0200..0x1000] = include_bytes!("../chip8-test-suite/bin/chip8-test-suite.ch8"),
// Create a screen, and fill it with
Screen [0x0F00..0x1000] = include_bytes!("chip8_test_suite.rs"),
},
)
} }
struct SuiteTest { struct SuiteTest {
test: u16, test: u8,
data: &'static [u8],
screen: &'static [u8], screen: &'static [u8],
} }
fn run_screentest(test: SuiteTest, mut cpu: CPU, mut bus: Bus) { fn run_screentest(test: SuiteTest, mut cpu: CPU, mut screen: Screen) {
// Set the test to run // Set the test to run
bus.write(0x1feu16, test.test); cpu.poke(0x1ffu16, test.test);
cpu.load_program_bytes(test.data).unwrap();
// The test suite always initiates a keypause on test completion // The test suite always initiates a keypause on test completion
while !cpu.flags.keypause { while !(cpu.flags.is_paused()) {
cpu.multistep(&mut bus, 8).unwrap(); cpu.multistep(&mut screen, 10).unwrap();
if cpu.cycle() > 1000000 {
panic!("test {} took too long", test.test)
}
} }
// Compare the screen to the reference screen buffer // Compare the screen to the reference screen buffer
bus.print_screen().unwrap(); screen.print_screen();
bus! {crate::bus::Region::Screen [0..256] = test.screen} Screen::from(test.screen).print_screen();
.print_screen() assert_eq!(screen.grab(..).unwrap(), test.screen);
.unwrap();
assert_eq!(bus.get_region(Screen).unwrap(), test.screen);
} }
#[test] #[test]
@ -49,6 +45,7 @@ fn splash_screen() {
run_screentest( run_screentest(
SuiteTest { SuiteTest {
test: 0, test: 0,
data: include_bytes!("../chip8-test-suite/bin/1-chip8-logo.ch8"),
screen: include_bytes!("screens/chip8-test-suite/splash.bin"), screen: include_bytes!("screens/chip8-test-suite/splash.bin"),
}, },
cpu, cpu,
@ -61,7 +58,8 @@ fn ibm_logo() {
let (cpu, bus) = setup_environment(); let (cpu, bus) = setup_environment();
run_screentest( run_screentest(
SuiteTest { SuiteTest {
test: 0x01, test: 0x00,
data: include_bytes!("../chip8-test-suite/bin/2-ibm-logo.ch8"),
screen: include_bytes!("screens/chip8-test-suite/IBM.bin"), screen: include_bytes!("screens/chip8-test-suite/IBM.bin"),
}, },
cpu, cpu,
@ -69,12 +67,27 @@ fn ibm_logo() {
) )
} }
#[test]
fn corax_test() {
let (cpu, bus) = setup_environment();
run_screentest(
SuiteTest {
test: 0x00,
data: include_bytes!("../chip8-test-suite/bin/3-corax+.ch8"),
screen: include_bytes!("screens/chip8-test-suite/corax+.bin"),
},
cpu,
bus,
)
}
#[test] #[test]
fn flags_test() { fn flags_test() {
let (cpu, bus) = setup_environment(); let (cpu, bus) = setup_environment();
run_screentest( run_screentest(
SuiteTest { SuiteTest {
test: 0x03, test: 0x00,
data: include_bytes!("../chip8-test-suite/bin/4-flags.ch8"),
screen: include_bytes!("screens/chip8-test-suite/flags.bin"), screen: include_bytes!("screens/chip8-test-suite/flags.bin"),
}, },
cpu, cpu,
@ -87,7 +100,8 @@ fn quirks_test() {
let (cpu, bus) = setup_environment(); let (cpu, bus) = setup_environment();
run_screentest( run_screentest(
SuiteTest { SuiteTest {
test: 0x0104, test: 0x01,
data: include_bytes!("../chip8-test-suite/bin/5-quirks.ch8"),
screen: include_bytes!("screens/chip8-test-suite/quirks.bin"), screen: include_bytes!("screens/chip8-test-suite/quirks.bin"),
}, },
cpu, cpu,

View File

@ -1,56 +1,50 @@
//! Testing methods on Chirp's public API //! Testing methods on Chirp's public API
use chirp::cpu::mem::Region::*;
use chirp::*; use chirp::*;
use std::{collections::hash_map::DefaultHasher, hash::Hash}; use std::{collections::hash_map::DefaultHasher, hash::Hash};
#[test]
fn chip8() {
let ch8 = Chip8::default(); // Default
let ch82 = ch8.clone(); // Clone
assert_eq!(ch8, ch82); // PartialEq
println!("{ch8:?}"); // Debug
}
mod bus { mod bus {
use super::*; use super::*;
mod region { mod region {
use super::*; use super::*;
// #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] // #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[test] #[test]
fn copy() { fn copy() {
let r1 = Screen; let r1 = Charset;
let r2 = r1; let r2 = r1;
assert_eq!(r1, r2); assert_eq!(r1, r2);
} }
#[test] #[test]
#[allow(clippy::clone_on_copy)] #[allow(clippy::clone_on_copy)]
fn clone() { fn clone() {
let r1 = Screen; let r1 = Charset;
let r2 = r1.clone(); let r2 = r1.clone();
assert_eq!(r1, r2); assert_eq!(r1, r2);
} }
#[test] #[test]
fn display() { fn display() {
println!("{Charset}{Program}{Screen}{Stack}{Count}"); println!("{Charset}{Program}{Count}");
} }
#[test] #[test]
fn debug() { fn debug() {
println!("{Charset:?}{Program:?}{Screen:?}{Stack:?}{Count:?}"); println!("{Charset:?}{Program:?}{Count:?}");
} }
// lmao the things you do for test coverage // lmao the things you do for test coverage
#[test] #[test]
fn eq() { fn eq() {
assert_eq!(Screen, Screen); assert_eq!(Charset, Charset);
assert_ne!(Charset, Program); assert_ne!(Charset, Program);
} }
#[test] #[test]
fn ord() { fn ord() {
assert_eq!(Stack, Charset.max(Program).max(Screen).max(Stack)); assert_eq!(Program, Charset.max(Program));
assert!(Charset < Program && Program < Screen && Screen < Stack); assert!(Charset < Program);
} }
#[test] #[test]
fn hash() { fn hash() {
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
Stack.hash(&mut hasher); Program.hash(&mut hasher);
println!("{hasher:?}"); println!("{hasher:?}");
} }
} }
@ -58,7 +52,7 @@ mod bus {
#[should_panic] #[should_panic]
fn bus_missing_region() { fn bus_missing_region() {
// Print the screen of a bus with no screen // Print the screen of a bus with no screen
bus! {}.print_screen().unwrap() mem! {}.get_region(Charset).unwrap();
} }
} }
@ -125,14 +119,13 @@ mod cpu {
use super::*; use super::*;
//#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] //#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[test] #[test]
#[allow(clippy::redundant_clone)]
fn clone() { fn clone() {
let cf1 = Flags { let cf1 = Flags {
debug: false, debug: false,
pause: false, pause: false,
keypause: false, keypause: false,
draw_wait: false, draw_wait: false,
lastkey: None,
monotonic: None,
..Default::default() ..Default::default()
}; };
let cf2 = cf1.clone(); let cf2 = cf1.clone();
@ -190,7 +183,7 @@ mod cpu {
} }
mod dis { mod dis {
use chirp::cpu::disassembler::Insn; use chirp::cpu::instruction::Insn;
use imperative_rs::InstructionSet; use imperative_rs::InstructionSet;
#[test] #[test]
@ -208,7 +201,7 @@ mod dis {
#[test] #[test]
fn error() { fn error() {
let error = chirp::error::Error::MissingRegion { region: Screen }; let error = chirp::error::Error::InvalidAddressRange { range: (..).into() };
// Print it with Display and Debug // Print it with Display and Debug
println!("{error} {error:?}"); println!("{error} {error:?}");
} }
@ -226,6 +219,7 @@ mod quirks {
bin_ops: true, bin_ops: true,
shift: true, shift: true,
draw_wait: true, draw_wait: true,
screen_wrap: false,
dma_inc: true, dma_inc: true,
stupid_jumps: true, stupid_jumps: true,
} }
@ -241,6 +235,7 @@ mod quirks {
bin_ops: false, bin_ops: false,
shift: false, shift: false,
draw_wait: false, draw_wait: false,
screen_wrap: false,
dma_inc: false, dma_inc: false,
stupid_jumps: false, stupid_jumps: false,
} }
@ -253,9 +248,11 @@ mod quirks {
bin_ops: false, bin_ops: false,
shift: true, shift: true,
draw_wait: false, draw_wait: false,
screen_wrap: false,
dma_inc: true, dma_inc: true,
stupid_jumps: false, stupid_jumps: false,
}; };
#[allow(clippy::clone_on_copy)]
let q2 = q1.clone(); let q2 = q1.clone();
assert_eq!(q1, q2); assert_eq!(q1, q2);
} }

Binary file not shown.