From 85956504d7084e5aa55e5dafbc0ea33a4af391cd Mon Sep 17 00:00:00 2001 From: John Breaux Date: Mon, 27 Mar 2023 17:27:05 -0500 Subject: [PATCH] UI: Refactor library module to promote code reuse --- src/io.rs | 210 ++++++++++++++++++++++++++++++++++++++++++-------- src/lib.rs | 2 +- src/main.rs | 216 +++++++++++++++++++++++----------------------------- 3 files changed, 274 insertions(+), 154 deletions(-) diff --git a/src/io.rs b/src/io.rs index c7ddedc..bcba03a 100644 --- a/src/io.rs +++ b/src/io.rs @@ -1,40 +1,62 @@ -//! +//! Platform-specific IO/UI code, and some debug functionality. +//! TODO: Break this into its own crate. -use crate::{bus::{Bus, Region}, cpu::CPU, error::Result}; +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use crate::{ + bus::{Bus, Region}, + error::Result, + Chip8, +}; use minifb::*; -#[derive(Clone, Copy, Debug)] -pub struct WindowBuilder { +#[derive(Clone, Debug)] +pub struct UIBuilder { pub width: usize, pub height: usize, pub name: Option<&'static str>, + pub rom: Option, pub window_options: WindowOptions, } -impl WindowBuilder { +impl UIBuilder { pub fn new(height: usize, width: usize) -> Self { - WindowBuilder { + UIBuilder { width, height, ..Default::default() } } - pub fn build(&self) -> Result { - Ok(Window::new( - self.name.unwrap_or_default(), - self.width, - self.height, - self.window_options, - )?) + pub fn rom(&mut self, path: impl AsRef) -> &mut Self { + self.rom = Some(path.as_ref().into()); + self + } + pub fn build(&self) -> Result { + let ui = UI { + window: Window::new( + self.name.unwrap_or_default(), + self.width, + self.height, + self.window_options, + )?, + keyboard: Default::default(), + fb: Default::default(), + rom: self.rom.to_owned().unwrap_or_default(), + }; + Ok(ui) } } -impl Default for WindowBuilder { +impl Default for UIBuilder { fn default() -> Self { - WindowBuilder { + UIBuilder { width: 64, height: 32, name: Some("Chip-8 Interpreter"), + rom: None, window_options: WindowOptions { title: true, resize: false, @@ -94,7 +116,7 @@ impl FrameBuffer { //TODO: NOT THIS window .update_with_buffer(&self.buffer, self.width, self.height) - .expect("The window manager has encountered an issue I don't want to deal with"); + .expect("The window manager should update the buffer."); } } @@ -104,6 +126,121 @@ impl Default for FrameBuffer { } } +#[derive(Debug)] +pub struct UI { + window: Window, + keyboard: Vec, + fb: FrameBuffer, + rom: PathBuf, +} + +impl UI { + pub fn frame(&mut self, ch8: &mut Chip8) -> Option<()> { + { + if ch8.cpu.flags.pause { + self.window.set_title("Chirp ⏸") + } else { + self.window.set_title("Chirp ▶"); + } + // update framebuffer + self.fb.render(&mut self.window, &mut ch8.bus); + } + Some(()) + } + + pub fn keys(&mut self, ch8: &mut Chip8) -> Option<()> { + // TODO: Remove this hacky workaround for minifb's broken get_keys_* functions. + let get_keys_pressed = || { + self.window + .get_keys() + .into_iter() + .filter(|key| !self.keyboard.contains(key)) + }; + let get_keys_released = || { + self.keyboard + .clone() + .into_iter() + .filter(|key| !self.window.get_keys().contains(key)) + }; + use crate::io::Region::*; + for key in get_keys_released() { + ch8.cpu.release(identify_key(key)); + } + // handle keybinds for the UI + for key in get_keys_pressed() { + use Key::*; + match key { + F1 | Comma => ch8.cpu.dump(), + F2 | Period => ch8 + .bus + .print_screen() + .expect("The 'screen' memory region should exist"), + F3 => { + debug_dump_screen(&ch8, &self.rom).expect("Unable to write debug screen dump"); + } + F4 | Slash => { + eprintln!("Debug {}.", { + ch8.cpu.flags.debug(); + if ch8.cpu.flags.debug { + "enabled" + } else { + "disabled" + } + }) + } + F5 | Backslash => eprintln!("{}.", { + ch8.cpu.flags.pause(); + if ch8.cpu.flags.pause { + "Paused" + } else { + "Unpaused" + } + }), + F6 | Enter => { + eprintln!("Step"); + ch8.cpu.singlestep(&mut ch8.bus); + } + F7 => { + eprintln!("Set breakpoint {:03x}.", ch8.cpu.pc()); + ch8.cpu.set_break(ch8.cpu.pc()); + } + F8 => { + eprintln!("Unset breakpoint {:03x}.", ch8.cpu.pc()); + ch8.cpu.unset_break(ch8.cpu.pc()); + } + F9 | Delete => { + eprintln!("Soft reset state.cpu {:03x}", ch8.cpu.pc()); + ch8.cpu.soft_reset(); + ch8.bus.clear_region(Screen); + } + Escape => return None, + key => ch8.cpu.press(identify_key(key)), + } + } + self.keyboard = self.window.get_keys(); + Some(()) + } +} + +pub const KEYMAP: [Key; 16] = [ + Key::X, + Key::Key1, + Key::Key2, + Key::Key3, + Key::Q, + Key::W, + Key::E, + Key::A, + Key::S, + Key::D, + Key::Z, + Key::C, + Key::Key4, + Key::R, + Key::F, + Key::V, +]; + pub fn identify_key(key: Key) -> usize { match key { Key::Key1 => 0x1, @@ -126,19 +263,30 @@ pub fn identify_key(key: Key) -> usize { } } -/// Gets keys from the Window, and feeds them directly to the CPU -pub fn get_keys(window: &mut Window, cpu: &mut CPU) { - cpu.release(); - window - .get_keys() - .iter() - .for_each(|key| cpu.press(identify_key(*key))); -} - -pub fn debug_dump_screen(bus: &Bus) -> Result<()> { - Ok(std::fs::write( - "screen_dump.bin", - bus.get_region(Region::Screen) - .expect("Screen should exist, but does not"), - )?) +pub fn debug_dump_screen(ch8: &Chip8, rom: &Path) -> Result<()> { + let path = PathBuf::new() + .join("src/cpu/tests/screens/") + .join(if rom.is_absolute() { + Path::new("unknown/") + } else { + rom.file_name().unwrap_or(OsStr::new("unknown")).as_ref() + }) + .join(format!("{}.bin", ch8.cpu.cycle())); + std::fs::write( + &path, + ch8.bus + .get_region(Region::Screen) + .expect("Region::Screen should exist"), + ) + .unwrap_or_else(|_| { + std::fs::write( + "screendump.bin", + ch8.bus + .get_region(Region::Screen) + .expect("Region::Screen should exist"), + ) + .ok(); // lmao + }); + eprintln!("Saved to {}", &path.display()); + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 1804803..0d78d6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,5 +26,5 @@ pub mod prelude { pub use bus::{Bus, Read, Region::*, Write}; pub use cpu::{disassemble::Disassemble, ControlFlags, CPU}; pub use error::Result; - pub use io::{WindowBuilder, *}; + pub use io::{UIBuilder, *}; } diff --git a/src/main.rs b/src/main.rs index 7a89d8d..30ab28e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use chirp::{error::Result, prelude::*}; use gumdrop::*; -use minifb::*; use std::fs::read; use std::{ path::PathBuf, @@ -12,28 +11,62 @@ use std::{ #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)] 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 behavior incompatible with modern software.")] - pub authentic: 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, + #[options(help = "Run the emulator as fast as possible for `step` instructions.")] + pub step: Option, + + #[options( + short = "z", + help = "Disable setting vF to 0 after a bitwise operation." + )] + pub vfreset: bool, + #[options( + short = "x", + help = "Disable waiting for vblank after issuing a draw call." + )] + pub drawsync: bool, + + #[options( + short = "c", + help = "Use CHIP-48 style DMA instructions, which don't touch I." + )] + pub memory: bool, + #[options( + short = "v", + help = "Use CHIP-48 style bit-shifts, which don't touch vY." + )] + pub shift: bool, + #[options( + short = "b", + help = "Use SUPER-CHIP style indexed jump, which is indexed relative to v[adr]." + )] + pub jumping: bool, + #[options( long = "break", help = "Set breakpoints for the emulator to stop at.", - parse(try_from_str = "parse_hex") + parse(try_from_str = "parse_hex"), + meta = "BP" )] pub breakpoints: Vec, - #[options(help = "Enable debug mode at startup.")] - pub debug: bool, - #[options(help = "Enable pause mode at startup.", default = "false")] - pub pause: bool, - #[options(help = "Load a ROM to run on Chirp.", required, free)] - pub file: PathBuf, - #[options(help = "Set the target framerate.", default = "60")] + #[options( + help = "Load additional word at address 0x1fe", + parse(try_from_str = "parse_hex"), + meta = "WORD" + )] + pub data: u16, + #[options(help = "Set the target framerate.", default = "60", meta = "FR")] pub frame_rate: u64, - #[options(help = "Set the instructions-per-frame rate.", default = "8")] - pub speed: usize, - #[options(help = "Run the emulator as fast as possible for `step` instructions.")] - pub step: Option, } #[derive(Debug)] @@ -42,133 +75,77 @@ struct State { pub step: Option, pub rate: u64, pub ch8: Chip8, - pub win: Window, - pub fb: FrameBuffer, + pub ui: UI, pub ft: Instant, } impl State { fn new(options: Arguments) -> Result { let mut state = State { - speed: options.speed, + speed: options.speed.unwrap_or(8), step: options.step, rate: options.frame_rate, - 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( - 0x1000, - 0x50, - 0x200, - 0xefe, - Disassemble::default(), - options.breakpoints, - ControlFlags { - authentic: options.authentic, - debug: options.debug, - pause: options.pause, - ..Default::default() + 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], }, - )}, - win: WindowBuilder::default().build()?, - fb: FrameBuffer::new(64, 32), + cpu: CPU::new( + 0x1000, + 0x50, + 0x200, + 0xefe, + Disassemble::default(), + options.breakpoints, + ControlFlags { + quirks: chirp::cpu::Quirks { + bin_ops: !options.vfreset, + shift: !options.shift, + draw_wait: !options.drawsync, + dma_inc: !options.memory, + stupid_jumps: options.jumping, + }, + debug: options.debug, + pause: options.pause, + monotonic: options.speed, + ..Default::default() + }, + ), + }, + ui: UIBuilder::default().rom(&options.file).build()?, ft: Instant::now(), }; - state.fb.render(&mut state.win, &state.ch8.bus); + state.ch8.bus.write(0x1feu16, options.data); Ok(state) } + fn keys(&mut self) -> Option<()> { + self.ui.keys(&mut self.ch8) + } + fn frame(&mut self) -> Option<()> { + self.ui.frame(&mut self.ch8) + } fn tick_cpu(&mut self) { if !self.ch8.cpu.flags.pause { let rate = self.speed; match self.step { Some(ticks) => { - self.ch8.cpu.multistep(&mut self.ch8.bus, ticks, rate); + self.ch8.cpu.multistep(&mut self.ch8.bus, ticks); // Pause the CPU and clear step self.ch8.cpu.flags.pause = true; self.step = None; - }, + } None => { - self.ch8.cpu.multistep(&mut self.ch8.bus, rate, rate); - }, + self.ch8.cpu.multistep(&mut self.ch8.bus, rate); + } } } } - fn frame(&mut self) -> Option<()> { - { - if self.ch8.cpu.flags.pause { - self.win.set_title("Chirp ⏸") - } else { - self.win.set_title("Chirp ▶"); - } - // update framebuffer - self.fb.render(&mut self.win, &mut self.ch8.bus); - // get key input (has to happen after render) - chirp::io::get_keys(&mut self.win, &mut self.ch8.cpu); - } - Some(()) - } - fn keys(&mut self) -> Option<()> { - // handle keybinds for the UI - for key in self.win.get_keys_pressed(KeyRepeat::No) { - use Key::*; - match key { - F1 | Comma => self.ch8.cpu.dump(), - F2 | Period => self - .ch8.bus - .print_screen() - .expect("The 'screen' memory region exists"), - F3 => {chirp::io::debug_dump_screen(&self.ch8.bus).expect("Unable to write debug screen dump"); eprintln!("Screen dumped to file.")}, - F4 | Slash => { - eprintln!( - "{}", - endis("Debug", { - self.ch8.cpu.flags.debug(); - self.ch8.cpu.flags.debug - }) - ) - } - F5 | Backslash => eprintln!( - "{}", - endis("Pause", { - self.ch8.cpu.flags.pause(); - self.ch8.cpu.flags.pause - }) - ), - F6 | Enter => { - eprintln!("Step"); - self.ch8.cpu.singlestep(&mut self.ch8.bus); - } - F7 => { - eprintln!("Set breakpoint {:x}", self.ch8.cpu.pc()); - self.ch8.cpu.set_break(self.ch8.cpu.pc()); - } - F8 => { - eprintln!("Unset breakpoint {:x}", self.ch8.cpu.pc()); - self.ch8.cpu.unset_break(self.ch8.cpu.pc()); - } - F9 | Delete => { - eprintln!("Soft reset state.cpu {:x}", self.ch8.cpu.pc()); - self.ch8.cpu.soft_reset(); - self.ch8.bus.clear_region(Screen); - } - F10 | Backspace => { - eprintln!("Hard reset state.cpu"); - self.ch8.cpu = CPU::default(); - self.ch8.bus.clear_region(Screen); - } - Escape => return None, - _ => (), - } - } - Some(()) - } fn wait_for_next_frame(&mut self) { let rate = 1_000_000_000 / self.rate + 1; std::thread::sleep(Duration::from_nanos(rate).saturating_sub(self.ft.elapsed())); @@ -198,8 +175,3 @@ fn main() -> Result<()> { fn parse_hex(value: &str) -> std::result::Result { u16::from_str_radix(value, 16) } - -/// Transforms a bool into "enabled"/"disabled" -fn endis(name: &str, state: bool) -> String { - format!("{name} {}", if state { "enabled" } else { "disabled" }) -} \ No newline at end of file