UI: Refactor library module to promote code reuse

This commit is contained in:
John 2023-03-27 17:27:05 -05:00
parent dbc96648f1
commit 85956504d7
3 changed files with 274 additions and 154 deletions

202
src/io.rs
View File

@ -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::*; use minifb::*;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Debug)]
pub struct WindowBuilder { pub struct UIBuilder {
pub width: usize, pub width: usize,
pub height: usize, pub height: usize,
pub name: Option<&'static str>, pub name: Option<&'static str>,
pub rom: Option<PathBuf>,
pub window_options: WindowOptions, pub window_options: WindowOptions,
} }
impl WindowBuilder { impl UIBuilder {
pub fn new(height: usize, width: usize) -> Self { pub fn new(height: usize, width: usize) -> Self {
WindowBuilder { UIBuilder {
width, width,
height, height,
..Default::default() ..Default::default()
} }
} }
pub fn build(&self) -> Result<Window> { pub fn rom(&mut self, path: impl AsRef<Path>) -> &mut Self {
Ok(Window::new( self.rom = Some(path.as_ref().into());
self
}
pub fn build(&self) -> Result<UI> {
let ui = UI {
window: Window::new(
self.name.unwrap_or_default(), self.name.unwrap_or_default(),
self.width, self.width,
self.height, self.height,
self.window_options, 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 { fn default() -> Self {
WindowBuilder { UIBuilder {
width: 64, width: 64,
height: 32, height: 32,
name: Some("Chip-8 Interpreter"), name: Some("Chip-8 Interpreter"),
rom: None,
window_options: WindowOptions { window_options: WindowOptions {
title: true, title: true,
resize: false, resize: false,
@ -94,7 +116,7 @@ impl FrameBuffer {
//TODO: NOT THIS //TODO: NOT THIS
window window
.update_with_buffer(&self.buffer, self.width, self.height) .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<Key>,
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 { pub fn identify_key(key: Key) -> usize {
match key { match key {
Key::Key1 => 0x1, 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 debug_dump_screen(ch8: &Chip8, rom: &Path) -> Result<()> {
pub fn get_keys(window: &mut Window, cpu: &mut CPU) { let path = PathBuf::new()
cpu.release(); .join("src/cpu/tests/screens/")
window .join(if rom.is_absolute() {
.get_keys() Path::new("unknown/")
.iter() } else {
.for_each(|key| cpu.press(identify_key(*key))); rom.file_name().unwrap_or(OsStr::new("unknown")).as_ref()
} })
.join(format!("{}.bin", ch8.cpu.cycle()));
pub fn debug_dump_screen(bus: &Bus) -> Result<()> { std::fs::write(
Ok(std::fs::write( &path,
"screen_dump.bin", ch8.bus
bus.get_region(Region::Screen) .get_region(Region::Screen)
.expect("Screen should exist, but does not"), .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(())
} }

View File

@ -26,5 +26,5 @@ pub mod prelude {
pub use bus::{Bus, Read, Region::*, Write}; pub use bus::{Bus, Read, Region::*, Write};
pub use cpu::{disassemble::Disassemble, ControlFlags, CPU}; pub use cpu::{disassemble::Disassemble, ControlFlags, CPU};
pub use error::Result; pub use error::Result;
pub use io::{WindowBuilder, *}; pub use io::{UIBuilder, *};
} }

View File

@ -3,7 +3,6 @@
use chirp::{error::Result, prelude::*}; use chirp::{error::Result, prelude::*};
use gumdrop::*; use gumdrop::*;
use minifb::*;
use std::fs::read; use std::fs::read;
use std::{ use std::{
path::PathBuf, path::PathBuf,
@ -12,28 +11,62 @@ use std::{
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)]
struct Arguments { struct Arguments {
#[options(help = "Load a ROM to run on Chirp.", required, free)]
pub file: PathBuf,
#[options(help = "Print this help message.")] #[options(help = "Print this help message.")]
help: bool, help: bool,
#[options(help = "Enable behavior incompatible with modern software.")] #[options(help = "Enable debug mode at startup.")]
pub authentic: bool, 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 = "Run the emulator as fast as possible for `step` instructions.")]
pub step: Option<usize>,
#[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( #[options(
long = "break", long = "break",
help = "Set breakpoints for the emulator to stop at.", 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<u16>, pub breakpoints: Vec<u16>,
#[options(help = "Enable debug mode at startup.")] #[options(
pub debug: bool, help = "Load additional word at address 0x1fe",
#[options(help = "Enable pause mode at startup.", default = "false")] parse(try_from_str = "parse_hex"),
pub pause: bool, meta = "WORD"
#[options(help = "Load a ROM to run on Chirp.", required, free)] )]
pub file: PathBuf, pub data: u16,
#[options(help = "Set the target framerate.", default = "60")] #[options(help = "Set the target framerate.", default = "60", meta = "FR")]
pub frame_rate: u64, 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<usize>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -42,22 +75,22 @@ struct State {
pub step: Option<usize>, pub step: Option<usize>,
pub rate: u64, pub rate: u64,
pub ch8: Chip8, pub ch8: Chip8,
pub win: Window, pub ui: UI,
pub fb: FrameBuffer,
pub ft: Instant, pub ft: Instant,
} }
impl State { impl State {
fn new(options: Arguments) -> Result<Self> { fn new(options: Arguments) -> Result<Self> {
let mut state = State { let mut state = State {
speed: options.speed, speed: options.speed.unwrap_or(8),
step: options.step, step: options.step,
rate: options.frame_rate, rate: options.frame_rate,
ch8: Chip8 { bus: bus! { ch8: Chip8 {
bus: bus! {
// Load the charset into ROM // Load the charset into ROM
Charset [0x0050..0x00A0] = include_bytes!("mem/charset.bin"), Charset [0x0050..0x00A0] = include_bytes!("mem/charset.bin"),
// Load the ROM file into RAM // Load the ROM file into RAM
Program [0x0200..0x1000] = &read(options.file)?, Program [0x0200..0x1000] = &read(&options.file)?,
// Create a screen // Create a screen
Screen [0x1000..0x1100], Screen [0x1000..0x1100],
// Create a stack // Create a stack
@ -71,103 +104,47 @@ impl State {
Disassemble::default(), Disassemble::default(),
options.breakpoints, options.breakpoints,
ControlFlags { ControlFlags {
authentic: options.authentic, 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, debug: options.debug,
pause: options.pause, pause: options.pause,
monotonic: options.speed,
..Default::default() ..Default::default()
}, },
)}, ),
win: WindowBuilder::default().build()?, },
fb: FrameBuffer::new(64, 32), ui: UIBuilder::default().rom(&options.file).build()?,
ft: Instant::now(), ft: Instant::now(),
}; };
state.fb.render(&mut state.win, &state.ch8.bus); state.ch8.bus.write(0x1feu16, options.data);
Ok(state) 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) { fn tick_cpu(&mut self) {
if !self.ch8.cpu.flags.pause { if !self.ch8.cpu.flags.pause {
let rate = self.speed; let rate = self.speed;
match self.step { match self.step {
Some(ticks) => { 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 // Pause the CPU and clear step
self.ch8.cpu.flags.pause = true; self.ch8.cpu.flags.pause = true;
self.step = None; self.step = None;
}, }
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) { fn wait_for_next_frame(&mut self) {
let rate = 1_000_000_000 / self.rate + 1; let rate = 1_000_000_000 / self.rate + 1;
@ -198,8 +175,3 @@ fn main() -> Result<()> {
fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> { fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> {
u16::from_str_radix(value, 16) 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" })
}