diff --git a/src/bin/chirp-iced/main.rs b/src/bin/chirp-iced/main.rs deleted file mode 100644 index e5dd2c5..0000000 --- a/src/bin/chirp-iced/main.rs +++ /dev/null @@ -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, -} diff --git a/src/bin/chirp-imgui/error.rs b/src/bin/chirp-imgui/error.rs new file mode 100644 index 0000000..722f58c --- /dev/null +++ b/src/bin/chirp-imgui/error.rs @@ -0,0 +1,28 @@ +//! Error type for chirp-imgui + +use thiserror::Error; + +#[derive(Debug, Error)] +pub(crate) 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), +} diff --git a/src/bin/chirp-imgui/gui.rs b/src/bin/chirp-imgui/gui.rs new file mode 100644 index 0000000..8f9427b --- /dev/null +++ b/src/bin/chirp-imgui/gui.rs @@ -0,0 +1,217 @@ +//! 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 menubar; +use menubar::Menubar; + +/// 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, + pub menubar: Menubar, +} + +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::Default, + ); + + // 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: 13.0, + 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); + } + + let menu = &mut self.menubar; + let settings_menu = || { + use chirp::Mode::*; + let settings = &mut menu.settings; + const MODES: [chirp::Mode; 3] = [Chip8, SChip, XOChip]; + if ui.combo_simple_string("Mode", &mut settings.mode_index, &MODES) { + settings.quirks = MODES[settings.mode_index].into(); + settings.applied = true; + } + settings.applied = { + ui.input_scalar("IPF", &mut settings.target_ipf) + .chars_decimal(true) + .build() + | ui.checkbox("Bin-ops don't clear vF", &mut settings.quirks.bin_ops) + | ui.checkbox("DMA doesn't modify I", &mut settings.quirks.dma_inc) + | ui.checkbox("Draw calls are instant", &mut settings.quirks.draw_wait) + | ui.checkbox("Screen wraps at edge", &mut settings.quirks.screen_wrap) + | ui.checkbox("Shift ops ignore vY", &mut settings.quirks.shift) + | ui.checkbox("Jumps behave eratically", &mut settings.quirks.stupid_jumps) + } + }; + // let file_menu = || { + // let file = &mut menu.file; + // file.quit = ui.menu_item("Quit"); + // }; + let debug_menu = || { + menu.debug.reset = ui.menu_item("Reset"); + ui.checkbox("Live Disassembly", &mut menu.debug.dis); + }; + let help_menu = || { + let about = &mut menu.about; + about.open = ui.menu_item("About..."); + }; + + // Draw windows and GUI elements here + if menu.active { + ui.main_menu_bar(|| { + //ui.menu("File", file_menu); + ui.menu("Settings", settings_menu); + ui.menu("Debug", debug_menu); + ui.menu("Help", help_menu); + }); + } + + if menu.file.settings {} + if self.menubar.about.open { + ui.show_about_window(&mut self.menubar.about.open); + } + + // 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) { + match visible { + Some(visible) => self.menubar.active = visible, + None => self.menubar.active ^= true, + } + } + + pub fn wants_reset(&mut self) -> bool { + let reset = self.menubar.debug.reset; + self.menubar.debug.reset = false; + reset + } + + pub fn wants_disassembly(&self) -> bool { + self.menubar.debug.dis + } + + pub fn wants_quit(&self) -> bool { + self.menubar.file.quit + } +} diff --git a/src/bin/chirp-imgui/gui/menubar.rs b/src/bin/chirp-imgui/gui/menubar.rs new file mode 100644 index 0000000..6845cbb --- /dev/null +++ b/src/bin/chirp-imgui/gui/menubar.rs @@ -0,0 +1,62 @@ +//! The menubar that shows at the top of the screen + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Menubar { + pub(super) active: bool, + pub file: File, + pub settings: Settings, + pub debug: Debug, + pub about: About, +} + +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) settings: bool, + pub(super) reset: bool, + pub(super) quit: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Debug { + pub(super) reset: bool, + pub(super) dis: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct About { + pub(super) open: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Settings { + pub(super) target_ipf: usize, + pub(super) quirks: chirp::Quirks, + pub(super) mode_index: usize, + pub(super) colors: [[u8; 4]; 2], + pub(super) applied: bool, +} + +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 applied(&mut self) -> Option<(usize, chirp::Quirks)> { + self.applied.then_some((self.target_ipf, self.quirks)) + } +} diff --git a/src/bin/chirp-imgui/main.rs b/src/bin/chirp-imgui/main.rs new file mode 100644 index 0000000..a1ff4a1 --- /dev/null +++ b/src/bin/chirp-imgui/main.rs @@ -0,0 +1,247 @@ +#![forbid(unsafe_code)] +#![deny(clippy::all)] +#![allow(dead_code)] // TODO: finish writing the code +use crate::gui::*; +use chirp::*; +use core::panic; +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; + +mod error; +mod gui; + +const FOREGROUND: &[u8; 4] = &0xFFFF00FF_u32.to_be_bytes(); +const BACKGROUND: &[u8; 4] = &0x623701FF_u32.to_be_bytes(); + +/// The state of the application +#[derive(Debug)] +struct Emulator { + gui: Gui, + screen: Bus, + cpu: CPU, + ipf: usize, + colors: [[u8; 4]; 2], +} + +fn main() -> Result<(), error::Error> { + let event_loop = EventLoop::new(); + let mut input = WinitInputHelper::new(); + + let size = LogicalSize::new(128 * 6, 64 * 6); + 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 emu = Emulator::new( + Gui::new(&window, &pixels), + bus! { + Screen [0x000..0x100], + }, + CPU::default(), + 10, + ); + + // set initial parameters + *emu.gui.menubar.settings.target_ipf() = emu.cpu.flags.monotonic.unwrap_or(10); + *emu.gui.menubar.settings.quirks() = emu.cpu.flags.quirks; + if let Some(path) = std::env::args().nth(1) { + emu.cpu.load_program(path)?; + } else { + panic!("Supply a rom!"); + } + + // Run event loop + event_loop.run(move |event, _, control_flow| { + 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 world texture + 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 Emulator, + 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_quit() => { + *control_flow = ControlFlow::Exit; + return Ok(()); + }, + F1 => state.cpu.dump(), + F2 => state.screen.print_screen()?, + F3 => eprintln!("TODO: Dump screen"), + F4 => state.cpu.flags.debug(), + F5 => state.cpu.flags.pause(), + F6 => state.cpu.singlestep(&mut state.screen)?, + F7 => state.cpu.set_break(state.cpu.pc()), + F8 => state.cpu.unset_break(state.cpu.pc()), + Delete => state.cpu.reset(), + F11 => window.set_maximized(!window.is_maximized()), + LAlt => state.gui.show_menubar(None), + }); + state.input(&input)?; + + // Apply settings + if let Some((ipf, quirks)) = state.gui.menubar.settings.applied() { + state.ipf = ipf; + state.cpu.flags.monotonic = Some(ipf); + state.cpu.flags.quirks = quirks; + } + + // 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.cpu.flags.debug = state.gui.wants_disassembly(); + if state.gui.wants_reset() { + state.cpu.reset(); + } + + // Run the game loop + state.update()?; + state.draw(pixels)?; + + // redraw the window + window.request_redraw(); + } + Ok(()) + }; + + if let Event::RedrawRequested(_) = event { + if let Err(e) = redraw(&mut emu.gui, &mut pixels) { + eprintln!("{e}"); + *control_flow = ControlFlow::Exit; + } + } + + if let Err(e) = handle_events(&mut emu, &mut pixels, control_flow) { + eprintln!("{e}"); + *control_flow = ControlFlow::Exit; + } + }); +} + +impl Emulator { + pub fn new(gui: Gui, mem: Bus, cpu: CPU, ipf: usize) -> Self { + Self { + gui, + screen: mem, + cpu, + ipf, + colors: [*FOREGROUND, *BACKGROUND], + } + } + + pub fn update(&mut self) -> Result<(), error::Error> { + self.cpu.multistep(&mut self.screen, self.ipf)?; + Ok(()) + } + + pub fn draw(&mut self, pixels: &mut Pixels) -> Result<(), error::Error> { + if let Some(screen) = self.screen.get_region(Screen) { + 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(()) + } + + 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(()) + } +} + +#[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; + } + )+ + }; +}