chirp: Break frontends into separate projects
This commit is contained in:
23
chirp-imgui/Cargo.toml
Normal file
23
chirp-imgui/Cargo.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "chirp-imgui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
chirp = { path = ".." }
|
||||
|
||||
gumdrop = "0.8.1"
|
||||
owo-colors = "3"
|
||||
thiserror = "1.0.39"
|
||||
|
||||
imgui = { version = "^0.11" }
|
||||
imgui-winit-support = { version = "^0.11" }
|
||||
imgui-wgpu = { version = "^0.23" }
|
||||
pixels = { version = "^0" }
|
||||
# TODO: When imgui-winit-support updates to 0.28 (Soon:tm:), update winit and winit_input_helper
|
||||
winit = { version = "0.27.5", features = ["default", "x11"] }
|
||||
winit_input_helper = { version = "^0.13.0" }
|
||||
52
chirp-imgui/src/args.rs
Normal file
52
chirp-imgui/src/args.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Parses arguments into a struct
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::Emulator;
|
||||
use chirp::Mode;
|
||||
use gumdrop::*;
|
||||
|
||||
/// Parses a hexadecimal string into a u16
|
||||
fn parse_hex(value: &str) -> std::result::Result<u16, std::num::ParseIntError> {
|
||||
u16::from_str_radix(value, 16)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Options, Hash)]
|
||||
pub struct Arguments {
|
||||
#[options(help = "Load a ROM to run on Chirp.", required, free)]
|
||||
pub file: PathBuf,
|
||||
#[options(help = "Print this help message.")]
|
||||
help: bool,
|
||||
#[options(help = "Enable debug mode at startup.")]
|
||||
pub debug: bool,
|
||||
#[options(help = "Enable pause mode at startup.")]
|
||||
pub pause: bool,
|
||||
#[options(help = "Set the instructions-per-frame rate.")]
|
||||
pub speed: Option<usize>,
|
||||
// #[options(help = "Enable performance benchmarking on stderr (requires -S)")]
|
||||
// pub perf: bool,
|
||||
#[options(help = "Run in (Chip8, SChip, XOChip) mode.")]
|
||||
pub mode: Option<Mode>,
|
||||
#[options(help = "Set the target framerate.", default = "60", meta = "FR")]
|
||||
pub frame_rate: u64,
|
||||
}
|
||||
|
||||
impl Arguments {
|
||||
pub fn parse() -> Arguments {
|
||||
Arguments::parse_args_default_or_exit()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arguments> for Emulator {
|
||||
fn from(value: Arguments) -> Self {
|
||||
let mut emu = Emulator::new(value.speed.unwrap_or(10), value.file);
|
||||
if let Some(mode) = value.mode {
|
||||
emu.set_quirks(mode.into());
|
||||
}
|
||||
if value.pause {
|
||||
emu.pause()
|
||||
}
|
||||
emu.set_disasm(value.debug);
|
||||
emu
|
||||
}
|
||||
}
|
||||
176
chirp-imgui/src/emu.rs
Normal file
176
chirp-imgui/src/emu.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
//! The emulator's state, including the screen data
|
||||
|
||||
use super::{error, BACKGROUND, FOREGROUND};
|
||||
use chirp::{Grab, Screen, CPU};
|
||||
use pixels::Pixels;
|
||||
use std::path::{Path, PathBuf};
|
||||
use winit::event::VirtualKeyCode;
|
||||
use winit_input_helper::WinitInputHelper;
|
||||
|
||||
/// The state of the application
|
||||
#[derive(Debug)]
|
||||
pub struct Emulator {
|
||||
screen: Screen,
|
||||
cpu: CPU,
|
||||
pub ipf: usize,
|
||||
pub rom: PathBuf,
|
||||
pub colors: [[u8; 4]; 2],
|
||||
}
|
||||
|
||||
impl Emulator {
|
||||
/// Constructs a new CPU, with the provided ROM loaded at 0x0200
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if the provided ROM does not exist
|
||||
pub fn new(ipf: usize, rom: impl AsRef<Path>) -> Self {
|
||||
let screen = Screen::default();
|
||||
let mut cpu = CPU::default();
|
||||
cpu.load_program(&rom).expect("Loaded file MUST exist.");
|
||||
Self {
|
||||
cpu,
|
||||
ipf,
|
||||
rom: rom.as_ref().into(),
|
||||
screen,
|
||||
colors: [*FOREGROUND, *BACKGROUND],
|
||||
}
|
||||
}
|
||||
/// Runs a single epoch
|
||||
pub fn update(&mut self) -> Result<(), error::Error> {
|
||||
self.cpu.multistep(&mut self.screen, self.ipf)?;
|
||||
Ok(())
|
||||
}
|
||||
/// Rasterizes the screen into a [Pixels] buffer
|
||||
pub fn draw(&mut self, pixels: &mut Pixels) -> Result<(), error::Error> {
|
||||
if let Some(screen) = self.screen.grab(..) {
|
||||
let len_log2 = screen.len().ilog2() / 2;
|
||||
#[allow(unused_variables)]
|
||||
let (width, height) = (2u32.pow(len_log2 + 2), 2u32.pow(len_log2 + 1));
|
||||
pixels.resize_buffer(width, height)?;
|
||||
for (idx, pixel) in pixels.frame_mut().iter_mut().enumerate() {
|
||||
let (byte, bit, component) = (idx >> 5, (idx >> 2) % 8, idx & 0b11);
|
||||
*pixel = if screen[byte] & (0x80 >> bit) > 0 {
|
||||
self.colors[0][component]
|
||||
} else {
|
||||
self.colors[1][component]
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Processes keyboard input for the Emulator
|
||||
pub fn input(&mut self, input: &WinitInputHelper) -> Result<(), error::Error> {
|
||||
const KEYMAP: [VirtualKeyCode; 16] = [
|
||||
VirtualKeyCode::X,
|
||||
VirtualKeyCode::Key1,
|
||||
VirtualKeyCode::Key2,
|
||||
VirtualKeyCode::Key3,
|
||||
VirtualKeyCode::Q,
|
||||
VirtualKeyCode::W,
|
||||
VirtualKeyCode::E,
|
||||
VirtualKeyCode::A,
|
||||
VirtualKeyCode::S,
|
||||
VirtualKeyCode::D,
|
||||
VirtualKeyCode::Z,
|
||||
VirtualKeyCode::C,
|
||||
VirtualKeyCode::Key4,
|
||||
VirtualKeyCode::R,
|
||||
VirtualKeyCode::F,
|
||||
VirtualKeyCode::V,
|
||||
];
|
||||
for (id, &key) in KEYMAP.iter().enumerate() {
|
||||
if input.key_released(key) {
|
||||
self.cpu.release(id)?;
|
||||
}
|
||||
if input.key_pressed(key) {
|
||||
self.cpu.press(id)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quirks(&mut self) -> chirp::Quirks {
|
||||
self.cpu.flags.quirks
|
||||
}
|
||||
pub fn set_quirks(&mut self, quirks: chirp::Quirks) {
|
||||
self.cpu.flags.quirks = quirks
|
||||
}
|
||||
/// Prints the CPU registers and cycle count to stderr
|
||||
pub fn print_registers(&self) {
|
||||
self.cpu.dump()
|
||||
}
|
||||
/// Prints the screen (using the highest resolution available printer) to stdout
|
||||
pub fn print_screen(&self) -> Result<(), error::Error> {
|
||||
self.screen.print_screen();
|
||||
Ok(())
|
||||
}
|
||||
/// Dumps the raw screen bytes to a file named `{rom}_{cycle}.bin`,
|
||||
/// or, failing that, `screen_dump.bin`
|
||||
pub fn dump_screen(&self) -> Result<(), error::Error> {
|
||||
let mut path = PathBuf::new().join(format!(
|
||||
"{}_{}.bin",
|
||||
self.rom.file_stem().unwrap_or_default().to_string_lossy(),
|
||||
self.cpu.cycle()
|
||||
));
|
||||
path.set_extension("bin");
|
||||
if std::fs::write(&path, self.screen.as_slice()).is_ok() {
|
||||
eprintln!("Saved to {}", &path.display());
|
||||
} else if std::fs::write("screen_dump.bin", self.screen.as_slice()).is_ok() {
|
||||
eprintln!("Saved to screen_dump.bin");
|
||||
} else {
|
||||
eprintln!("Failed to dump screen to file.")
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Sets live disassembly
|
||||
pub fn set_disasm(&mut self, enabled: bool) {
|
||||
self.cpu.flags.debug = enabled;
|
||||
}
|
||||
/// Checks live disassembly
|
||||
pub fn is_disasm(&mut self) {
|
||||
eprintln!(
|
||||
"Live Disassembly {}abled",
|
||||
if self.cpu.flags.debug { "En" } else { "Dis" }
|
||||
);
|
||||
}
|
||||
/// Toggles emulator pause
|
||||
pub fn pause(&mut self) {
|
||||
self.cpu.flags.pause();
|
||||
eprintln!("{}aused", if self.cpu.flags.pause { "P" } else { "Unp" });
|
||||
}
|
||||
/// Single-steps the emulator, pausing afterward
|
||||
pub fn singlestep(&mut self) -> Result<(), error::Error> {
|
||||
self.cpu.singlestep(&mut self.screen)?;
|
||||
Ok(())
|
||||
}
|
||||
/// Sets a breakpoint at the current address
|
||||
pub fn set_break(&mut self) {
|
||||
self.cpu.set_break(self.cpu.pc());
|
||||
eprintln!("Set breakpoint at {}", self.cpu.pc());
|
||||
}
|
||||
/// Unsets a breakpoint at the current address
|
||||
pub fn unset_break(&mut self) {
|
||||
self.cpu.unset_break(self.cpu.pc());
|
||||
eprintln!("Unset breakpoint at {}", self.cpu.pc());
|
||||
}
|
||||
/// Soft-resets the CPU, keeping the program in memory
|
||||
pub fn soft_reset(&mut self) {
|
||||
self.cpu.reset();
|
||||
self.screen.clear();
|
||||
eprintln!("Soft Reset");
|
||||
}
|
||||
|
||||
/// Creates a new CPU with the current CPU's flags
|
||||
pub fn hard_reset(&mut self) {
|
||||
self.cpu.reset();
|
||||
self.screen.clear();
|
||||
// keep the flags
|
||||
let flags = self.cpu.flags.clone();
|
||||
// instantiate a completely new CPU, and reload the ROM from disk
|
||||
self.cpu = CPU::default();
|
||||
self.cpu.flags = flags;
|
||||
self.cpu
|
||||
.load_program(&self.rom)
|
||||
.expect("Previously loaded ROM no longer exists (was it moved?)");
|
||||
eprintln!("Hard Reset");
|
||||
}
|
||||
}
|
||||
28
chirp-imgui/src/error.rs
Normal file
28
chirp-imgui/src/error.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! Error type for chirp-imgui
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// Error originated in [`chirp`]
|
||||
#[error(transparent)]
|
||||
Chirp(#[from] chirp::error::Error),
|
||||
/// Error originated in [`std::io`]
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Error originated in [`pixels`]
|
||||
#[error(transparent)]
|
||||
Pixels(#[from] pixels::Error),
|
||||
/// Error originated in [`pixels`]
|
||||
#[error(transparent)]
|
||||
PixelsTexture(#[from] pixels::TextureError),
|
||||
/// Error originated from [`winit::error::OsError`]
|
||||
#[error(transparent)]
|
||||
WinitOs(#[from] winit::error::OsError),
|
||||
/// Error originated from [`winit::error::ExternalError`]
|
||||
#[error(transparent)]
|
||||
WinitExternal(#[from] winit::error::ExternalError),
|
||||
/// Error originated from [`winit::error::NotSupportedError`]
|
||||
#[error(transparent)]
|
||||
WinitNotSupported(#[from] winit::error::NotSupportedError),
|
||||
}
|
||||
194
chirp-imgui/src/gui.rs
Normal file
194
chirp-imgui/src/gui.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Represents the Dear Imgui
|
||||
//!
|
||||
//! Adapted from the [ImGui-winit Example]
|
||||
//!
|
||||
//! [ImGui-winit Example]: https://github.com/parasyte/pixels/blob/main/examples/imgui-winit/src/gui.rs
|
||||
|
||||
use pixels::{wgpu, PixelsContext};
|
||||
use std::time::Instant;
|
||||
|
||||
mod about;
|
||||
mod menubar;
|
||||
use menubar::Menubar;
|
||||
|
||||
/// Lays out the imgui widgets for a thing
|
||||
pub trait Drawable {
|
||||
// Lay out the ImGui widgets for this thing
|
||||
fn draw(&mut self, ui: &imgui::Ui);
|
||||
}
|
||||
|
||||
/// Holds state of GUI
|
||||
pub(crate) struct Gui {
|
||||
imgui: imgui::Context,
|
||||
platform: imgui_winit_support::WinitPlatform,
|
||||
renderer: imgui_wgpu::Renderer,
|
||||
last_frame: Instant,
|
||||
last_cursor: Option<imgui::MouseCursor>,
|
||||
pub menubar: Menubar,
|
||||
}
|
||||
|
||||
/// Queries the state of the [Gui]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum Wants {
|
||||
Quit,
|
||||
Disasm,
|
||||
SoftReset,
|
||||
HardReset,
|
||||
Reset,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Gui {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Gui")
|
||||
.field("imgui", &self.imgui)
|
||||
.field("platform", &self.platform)
|
||||
.field("last_frame", &self.last_frame)
|
||||
.field("last_cursor", &self.last_cursor)
|
||||
.field("menubar", &self.menubar)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl Gui {
|
||||
pub fn new(window: &winit::window::Window, pixels: &pixels::Pixels) -> Self {
|
||||
// Create Dear Imgui context
|
||||
let mut imgui = imgui::Context::create();
|
||||
imgui.set_ini_filename(None);
|
||||
|
||||
// winit init
|
||||
let mut platform = imgui_winit_support::WinitPlatform::init(&mut imgui);
|
||||
platform.attach_window(
|
||||
imgui.io_mut(),
|
||||
window,
|
||||
imgui_winit_support::HiDpiMode::Locked(2.2),
|
||||
);
|
||||
|
||||
// Configure fonts
|
||||
let dpi_scale = window.scale_factor();
|
||||
let font_size = (13.0 * dpi_scale) as f32;
|
||||
imgui.io_mut().font_global_scale = (1.0 / dpi_scale) as f32;
|
||||
imgui
|
||||
.fonts()
|
||||
.add_font(&[imgui::FontSource::DefaultFontData {
|
||||
config: Some(imgui::FontConfig {
|
||||
size_pixels: font_size,
|
||||
oversample_h: 2,
|
||||
oversample_v: 2,
|
||||
pixel_snap_h: true,
|
||||
..Default::default()
|
||||
}),
|
||||
}]);
|
||||
|
||||
// Create WGPU renderer
|
||||
let renderer = imgui_wgpu::Renderer::new(
|
||||
&mut imgui,
|
||||
pixels.device(),
|
||||
pixels.queue(),
|
||||
imgui_wgpu::RendererConfig {
|
||||
texture_format: pixels.render_texture_format(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
// Return Gui context
|
||||
Self {
|
||||
imgui,
|
||||
platform,
|
||||
renderer,
|
||||
last_frame: Instant::now(),
|
||||
last_cursor: None,
|
||||
menubar: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare Dear ImGui.
|
||||
pub(crate) fn prepare(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
) -> Result<(), winit::error::ExternalError> {
|
||||
// Prepare Dear ImGui
|
||||
let now = Instant::now();
|
||||
self.imgui.io_mut().update_delta_time(now - self.last_frame);
|
||||
self.last_frame = now;
|
||||
self.platform.prepare_frame(self.imgui.io_mut(), window)
|
||||
}
|
||||
|
||||
pub(crate) fn render(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
render_target: &wgpu::TextureView,
|
||||
context: &PixelsContext,
|
||||
) -> imgui_wgpu::RendererResult<()> {
|
||||
// Start a new Dear ImGui frame and update the cursor
|
||||
let ui = self.imgui.new_frame();
|
||||
|
||||
let mouse_cursor = ui.mouse_cursor();
|
||||
if self.last_cursor != mouse_cursor {
|
||||
self.last_cursor = mouse_cursor;
|
||||
self.platform.prepare_render(ui, window);
|
||||
}
|
||||
|
||||
self.menubar.draw(ui);
|
||||
// Draw windows and GUI elements here
|
||||
|
||||
if self.menubar.about.about_open {
|
||||
ui.open_popup("About");
|
||||
self.menubar.about.about_open = false;
|
||||
}
|
||||
|
||||
// Render Dear ImGui with WGPU
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("imgui"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: render_target,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: true,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
});
|
||||
|
||||
self.renderer.render(
|
||||
self.imgui.render(),
|
||||
&context.queue,
|
||||
&context.device,
|
||||
&mut rpass,
|
||||
)
|
||||
}
|
||||
|
||||
/// Handle any outstanding events
|
||||
pub fn handle_event(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event: &winit::event::Event<()>,
|
||||
) {
|
||||
self.platform
|
||||
.handle_event(self.imgui.io_mut(), window, event);
|
||||
}
|
||||
|
||||
/// Shows or hides the [Menubar]. If `visible` is [None], toggles visibility.
|
||||
pub fn show_menubar(&mut self, visible: Option<bool>) {
|
||||
match visible {
|
||||
Some(visible) => self.menubar.active = visible,
|
||||
None => self.menubar.active ^= true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the state of the Gui through a unified interface
|
||||
pub fn wants(&mut self, wants: Wants) -> bool {
|
||||
match wants {
|
||||
Wants::Quit => self.menubar.file.quit,
|
||||
Wants::Disasm => self.menubar.debug.dis,
|
||||
Wants::Reset => {
|
||||
let reset = self.menubar.debug.reset;
|
||||
self.menubar.debug.reset = false;
|
||||
reset
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
28
chirp-imgui/src/gui/about.rs
Normal file
28
chirp-imgui/src/gui/about.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! Information about the crate at compile time
|
||||
|
||||
use imgui::Ui;
|
||||
|
||||
/// Project authors
|
||||
const AUTHORS: Option<&str> = option_env!("CARGO_PKG_AUTHORS");
|
||||
/// Compiled binary name
|
||||
const BIN: Option<&str> = option_env!("CARGO_BIN_NAME");
|
||||
/// Crate description
|
||||
const DESCRIPTION: Option<&str> = option_env!("CARGO_PKG_DESCRIPTION");
|
||||
/// Crate name
|
||||
const NAME: Option<&str> = option_env!("CARGO_PKG_NAME");
|
||||
/// Crate repository
|
||||
const REPO: Option<&str> = option_env!("CARGO_PKG_REPOSITORY");
|
||||
/// Crate version
|
||||
const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub(crate) fn about(ui: &Ui) {
|
||||
ui.popup("About", || {
|
||||
ui.text(format!(
|
||||
"{} v{}",
|
||||
NAME.unwrap_or("Chirp"),
|
||||
VERSION.unwrap_or("None"),
|
||||
));
|
||||
ui.text(format!("Crafted by: {}", AUTHORS.unwrap_or("some people")));
|
||||
ui.text(REPO.unwrap_or("Repo unavailable"));
|
||||
});
|
||||
}
|
||||
164
chirp-imgui/src/gui/menubar.rs
Normal file
164
chirp-imgui/src/gui/menubar.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
//! The menubar that shows at the top of the screen
|
||||
|
||||
use super::Drawable;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub struct Menubar {
|
||||
pub(super) active: bool,
|
||||
pub file: File,
|
||||
pub settings: Settings,
|
||||
pub debug: Debug,
|
||||
pub about: Help,
|
||||
}
|
||||
|
||||
impl Drawable for Menubar {
|
||||
fn draw(&mut self, ui: &imgui::Ui) {
|
||||
if self.active {
|
||||
ui.main_menu_bar(|| {
|
||||
self.file.draw(ui);
|
||||
self.settings.draw(ui);
|
||||
self.debug.draw(ui);
|
||||
self.about.draw(ui);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Menubar {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: true,
|
||||
file: Default::default(),
|
||||
settings: Default::default(),
|
||||
debug: Default::default(),
|
||||
about: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct File {
|
||||
pub(super) reset: bool,
|
||||
pub(super) quit: bool,
|
||||
}
|
||||
|
||||
impl Drawable for File {
|
||||
fn draw(&mut self, ui: &imgui::Ui) {
|
||||
ui.menu("File", || {
|
||||
self.reset = ui.menu_item("Reset");
|
||||
self.quit = ui.menu_item("Quit");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Debug {
|
||||
pub(super) reset: bool,
|
||||
pub(super) dis: bool,
|
||||
}
|
||||
|
||||
impl Drawable for Debug {
|
||||
fn draw(&mut self, ui: &imgui::Ui) {
|
||||
ui.menu("Debug", || {
|
||||
self.reset = ui.menu_item("Reset");
|
||||
ui.checkbox("Live Disassembly", &mut self.dis);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug {
|
||||
pub fn reset(&self) -> bool {
|
||||
self.reset
|
||||
}
|
||||
pub fn dis(&self) -> bool {
|
||||
self.dis
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Help {
|
||||
pub(super) about_open: bool,
|
||||
}
|
||||
|
||||
impl Drawable for Help {
|
||||
fn draw(&mut self, ui: &imgui::Ui) {
|
||||
ui.menu("Help", || self.about_open = ui.menu_item("About..."))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
|
||||
pub struct Settings {
|
||||
pub(super) target_ipf: usize,
|
||||
pub(super) quirks: chirp::Quirks,
|
||||
pub(super) mode_index: usize,
|
||||
pub(super) colors: [[f32; 4]; 2],
|
||||
pub(super) applied: bool,
|
||||
}
|
||||
|
||||
impl Drawable for Settings {
|
||||
fn draw(&mut self, ui: &imgui::Ui) {
|
||||
self.applied = false;
|
||||
ui.menu("Settings", || {
|
||||
use chirp::Mode::*;
|
||||
ui.menu("Foreground Color", || {
|
||||
self.applied |= ui.color_picker4("", &mut self.colors[0])
|
||||
});
|
||||
ui.menu("Background Color", || {
|
||||
self.applied |= ui.color_picker4("", &mut self.colors[1])
|
||||
});
|
||||
const MODES: [chirp::Mode; 3] = [Chip8, SChip, XOChip];
|
||||
if ui.combo_simple_string("Mode", &mut self.mode_index, &MODES) {
|
||||
self.quirks = MODES[self.mode_index].into();
|
||||
self.applied |= true;
|
||||
}
|
||||
self.applied |= {
|
||||
ui.input_scalar("IPF", &mut self.target_ipf)
|
||||
.chars_decimal(true)
|
||||
.build()
|
||||
| ui.checkbox("Bin-ops don't clear vF", &mut self.quirks.bin_ops)
|
||||
| ui.checkbox("DMA doesn't modify I", &mut self.quirks.dma_inc)
|
||||
| ui.checkbox("Draw calls are instant", &mut self.quirks.draw_wait)
|
||||
| ui.checkbox("Screen wraps at edge", &mut self.quirks.screen_wrap)
|
||||
| ui.checkbox("Shift ops ignore vY", &mut self.quirks.shift)
|
||||
| ui.checkbox("Jumps behave eratically", &mut self.quirks.stupid_jumps)
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn target_ipf(&mut self) -> &mut usize {
|
||||
&mut self.target_ipf
|
||||
}
|
||||
pub fn quirks(&mut self) -> &mut chirp::Quirks {
|
||||
&mut self.quirks
|
||||
}
|
||||
pub fn set_mode(&mut self, mode: chirp::Mode) {
|
||||
self.mode_index = mode as usize;
|
||||
}
|
||||
pub fn set_color(&mut self, fg: &[u8; 4], bg: &[u8; 4]) {
|
||||
for (idx, component) in fg.iter().enumerate() {
|
||||
self.colors[0][idx] = *component as f32 / 255.0;
|
||||
}
|
||||
for (idx, component) in bg.iter().enumerate() {
|
||||
self.colors[1][idx] = *component as f32 / 255.0;
|
||||
}
|
||||
}
|
||||
pub fn applied(&mut self) -> Option<(usize, chirp::Quirks, [u8; 4], [u8; 4])> {
|
||||
let (fg, bg) = (self.colors[0], self.colors[1]);
|
||||
let fg = [
|
||||
(fg[0] * 255.0) as u8,
|
||||
(fg[1] * 255.0) as u8,
|
||||
(fg[2] * 255.0) as u8,
|
||||
(fg[3] * 255.0) as u8,
|
||||
];
|
||||
let bg = [
|
||||
(bg[0] * 255.0) as u8,
|
||||
(bg[1] * 255.0) as u8,
|
||||
(bg[2] * 255.0) as u8,
|
||||
(bg[3] * 255.0) as u8,
|
||||
];
|
||||
self.applied
|
||||
.then_some((self.target_ipf, self.quirks, fg, bg))
|
||||
}
|
||||
}
|
||||
205
chirp-imgui/src/main.rs
Normal file
205
chirp-imgui/src/main.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::all)]
|
||||
#![allow(dead_code)] // TODO: finish writing the code
|
||||
|
||||
mod args;
|
||||
mod emu;
|
||||
mod error;
|
||||
mod gui;
|
||||
|
||||
use crate::args::Arguments;
|
||||
use crate::emu::*;
|
||||
use crate::gui::*;
|
||||
use owo_colors::OwoColorize;
|
||||
use pixels::{Pixels, SurfaceTexture};
|
||||
use std::result::Result;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::{Event, VirtualKeyCode};
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
use winit::window::WindowBuilder;
|
||||
use winit_input_helper::WinitInputHelper;
|
||||
|
||||
// TODO: Make these configurable in the frontend
|
||||
|
||||
const FOREGROUND: &[u8; 4] = &0xFFFF00FF_u32.to_be_bytes();
|
||||
const BACKGROUND: &[u8; 4] = &0x623701FF_u32.to_be_bytes();
|
||||
const INIT_SPEED: usize = 10;
|
||||
|
||||
struct Application {
|
||||
gui: Gui,
|
||||
emu: Emulator,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), error::Error> {
|
||||
let args = Arguments::parse();
|
||||
// Make sure the ROM file exists
|
||||
if !args.file.is_file() {
|
||||
eprintln!(
|
||||
"{} not found. If the file exists, you might not have permission you access it.",
|
||||
args.file.display().italic().red()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let event_loop = EventLoop::new();
|
||||
let mut input = WinitInputHelper::new();
|
||||
|
||||
let size = LogicalSize::new(128 * 8, 64 * 8);
|
||||
let window = WindowBuilder::new()
|
||||
.with_title("Chirp")
|
||||
.with_inner_size(size)
|
||||
.with_always_on_top(true)
|
||||
.build(&event_loop)?;
|
||||
let mut scale_factor = window.scale_factor();
|
||||
|
||||
let mut pixels = {
|
||||
let window_size = window.inner_size();
|
||||
let surface_texture = SurfaceTexture::new(window_size.width, window_size.height, &window);
|
||||
Pixels::new(128, 64, surface_texture)?
|
||||
};
|
||||
|
||||
let mut gui = Gui::new(&window, &pixels);
|
||||
// set initial parameters
|
||||
if let Some(mode) = args.mode {
|
||||
gui.menubar.settings.set_mode(mode);
|
||||
}
|
||||
|
||||
gui.menubar.settings.set_color(FOREGROUND, BACKGROUND);
|
||||
*gui.menubar.settings.target_ipf() = args.speed.unwrap_or(INIT_SPEED);
|
||||
|
||||
let mut app = Application::new(args.into(), gui);
|
||||
|
||||
// Copy quirks from the running Emulator, for consistency
|
||||
*app.gui.menubar.settings.quirks() = app.emu.quirks();
|
||||
|
||||
// Run event loop
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
let toggle_fullscreen = || {
|
||||
window.set_fullscreen(if window.fullscreen().is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(winit::window::Fullscreen::Borderless(None))
|
||||
})
|
||||
};
|
||||
|
||||
let redraw = |gui: &mut Gui, pixels: &mut Pixels| -> Result<(), error::Error> {
|
||||
// Prepare gui for redraw
|
||||
gui.prepare(&window)?;
|
||||
|
||||
// Render everything together
|
||||
pixels.render_with(|encoder, render_target, context| {
|
||||
// Render the emulator's screen
|
||||
context.scaling_renderer.render(encoder, render_target);
|
||||
// Render Dear ImGui
|
||||
gui.render(&window, encoder, render_target, context)?;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut handle_events = |state: &mut Application,
|
||||
pixels: &mut Pixels,
|
||||
control_flow: &mut ControlFlow|
|
||||
-> Result<(), error::Error> {
|
||||
state.gui.handle_event(&window, &event);
|
||||
if input.update(&event) {
|
||||
use VirtualKeyCode::*;
|
||||
match_pressed!( match input {
|
||||
Escape | input.quit() | state.gui.wants(Wants::Quit) => {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return Ok(());
|
||||
},
|
||||
F1 => state.emu.print_registers(),
|
||||
F2 => state.emu.print_screen()?,
|
||||
F3 => state.emu.dump_screen()?,
|
||||
F4 => state.emu.is_disasm(),
|
||||
F5 => state.emu.pause(),
|
||||
F6 => state.emu.singlestep()?,
|
||||
F7 => state.emu.set_break(),
|
||||
F8 => state.emu.unset_break(),
|
||||
Delete => state.emu.soft_reset(),
|
||||
Insert => state.emu.hard_reset(),
|
||||
F11 => toggle_fullscreen(),
|
||||
LAlt => state.gui.show_menubar(None),
|
||||
});
|
||||
state.emu.input(&input)?;
|
||||
|
||||
// Apply settings
|
||||
if let Some((ipf, quirks, fg, bg)) = state.gui.menubar.settings.applied() {
|
||||
state.emu.ipf = ipf;
|
||||
state.emu.set_quirks(quirks);
|
||||
state.emu.colors = [fg, bg];
|
||||
}
|
||||
|
||||
// Update the scale factor
|
||||
if let Some(factor) = input.scale_factor() {
|
||||
scale_factor = factor;
|
||||
}
|
||||
|
||||
// Resize the window
|
||||
if let Some(size) = input.window_resized() {
|
||||
if size.width > 0 && size.height > 0 {
|
||||
// Resize the surface texture
|
||||
pixels.resize_surface(size.width, size.height)?;
|
||||
// Resize the world
|
||||
let LogicalSize { width, height } = size.to_logical(scale_factor);
|
||||
pixels.resize_buffer(width, height)?;
|
||||
}
|
||||
}
|
||||
|
||||
state.emu.set_disasm(state.gui.wants(Wants::Disasm));
|
||||
if state.gui.wants(Wants::Reset) {
|
||||
state.emu.hard_reset();
|
||||
}
|
||||
|
||||
// Run the game loop
|
||||
state.emu.update()?;
|
||||
state.emu.draw(pixels)?;
|
||||
|
||||
// redraw the window
|
||||
window.request_redraw();
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if let Event::RedrawRequested(_) = event {
|
||||
if let Err(e) = redraw(&mut app.gui, &mut pixels) {
|
||||
eprintln!("{e}");
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = handle_events(&mut app, &mut pixels, control_flow) {
|
||||
eprintln!("{e}");
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl Application {
|
||||
fn new(emu: Emulator, gui: Gui) -> Self {
|
||||
Self { gui, emu }
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! match_pressed {
|
||||
(match $input:ident {$($key:path $( | $cond:expr)? => $action:expr),+ $(,)?}) => {
|
||||
$(
|
||||
if $input.key_pressed($key) $( || $cond )? {
|
||||
$action;
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! match_released {
|
||||
(match $input:ident {$($key:path $( | $cond:expr)? => $action:expr),+ $(,)?}) => {
|
||||
$(
|
||||
if $input.key_released($key) $( || $cond )? {
|
||||
$action;
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user