From 6f5e86364506cb17078acc78fcb2d083ab3eb75b Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Mon, 15 Nov 2021 21:34:24 -0400 Subject: [PATCH] chore: drop pixels-rs and add egui w/ wgpu backend --- Cargo.lock | 166 ++++++++++++++++++++++----------- Cargo.toml | 7 +- src/bus.rs | 5 +- src/cartridge.rs | 2 +- src/emu.rs | 8 +- src/gui.rs | 238 +++++++++++++++++++++++++++++++++++++++++++++++ src/joypad.rs | 85 +++++------------ src/lib.rs | 1 + src/main.rs | 184 +++++++++++++++++++++--------------- 9 files changed, 501 insertions(+), 195 deletions(-) create mode 100644 src/gui.rs diff --git a/Cargo.lock b/Cargo.lock index ed1e204..ef5a2da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b228f2c198f98d4337ceb560333fb12cbb2f4948a953bf8c57d09deb219603" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser 0.13.2", +] + [[package]] name = "ab_glyph_rasterizer" version = "0.1.4" @@ -14,6 +24,17 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "alsa" version = "0.5.0" @@ -88,6 +109,12 @@ dependencies = [ "libloading 0.7.1", ] +[[package]] +name = "atomic_refcell" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b5e5f48b927f04e952dedc932f31995a65a0bf65ec971c74436e51bf6e970d" + [[package]] name = "atty" version = "0.2.14" @@ -678,6 +705,66 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "egui" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c8d416a3343cbfc6f4d17bb1cba46b4d7efecb9ee541967763e0b5e04e5fae7" +dependencies = [ + "ahash 0.7.6", + "epaint", + "nohash-hasher", +] + +[[package]] +name = "egui_wgpu_backend" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7d53df01ecac5951500cd5637b99c0334dad353fce9b203a9bf66b5f791a6b" +dependencies = [ + "bytemuck", + "epi", + "wgpu", +] + +[[package]] +name = "egui_winit_platform" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ae306ae9cd296ed3178d518fd0d9edf45f176cfdfe31ff77b7051c5f1d5278" +dependencies = [ + "egui", + "winit", +] + +[[package]] +name = "emath" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a1aaa922d55da6a2bf32957c3d153e7fb9d52ed8d69777a75092240172eb6e" + +[[package]] +name = "epaint" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16bb4d3b8bbbd132c99d2a5efec8567e8b6d09b742f758ae6cf1e4b104fe0231" +dependencies = [ + "ab_glyph", + "ahash 0.7.6", + "atomic_refcell", + "emath", + "nohash-hasher", +] + +[[package]] +name = "epi" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5e4e08127f9b86e2c450c96a3032764b63546eb170c2fc54684dc70ff3fc82" +dependencies = [ + "egui", +] + [[package]] name = "fnv" version = "1.0.7" @@ -716,14 +803,17 @@ dependencies = [ "bitfield", "clap", "directories-next", + "egui", + "egui_wgpu_backend", + "egui_winit_platform", "gilrs", - "pixels", + "pollster", "rodio", "rtrb", "tracing", "tracing-subscriber", + "wgpu", "winit", - "winit_input_helper", ] [[package]] @@ -832,7 +922,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" dependencies = [ - "ahash", + "ahash 0.4.7", ] [[package]] @@ -1285,6 +1375,12 @@ dependencies = [ "libc", ] +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "nom" version = "5.1.2" @@ -1420,7 +1516,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f923fb806c46266c02ab4a5b239735c144bdeda724a50ed058e5226f594cde3" dependencies = [ - "ttf-parser", + "ttf-parser 0.6.2", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ee3f72636e6f164cc41c9f9057f4e58c4e13507699ea7f5e5242b64b8198ee" +dependencies = [ + "ttf-parser 0.13.2", ] [[package]] @@ -1466,20 +1571,6 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" -[[package]] -name = "pixels" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ab1e297051c39cc7b7511e7e2b3ab151f14aff6a44e73bdde652b1e2190950" -dependencies = [ - "bytemuck", - "pollster", - "raw-window-handle", - "thiserror", - "ultraviolet", - "wgpu", -] - [[package]] name = "pkg-config" version = "0.3.20" @@ -1643,7 +1734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc7c727aded0be18c5b80c1640eae0ac8e396abf6fa8477d96cb37d18ee5ec59" dependencies = [ "ab_glyph_rasterizer", - "owned_ttf_parser", + "owned_ttf_parser 0.6.0", ] [[package]] @@ -1663,15 +1754,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" -[[package]] -name = "safe_arch" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ff3d6d9696af502cc3110dacce942840fb06ff4514cad92236ecc455f2ce05" -dependencies = [ - "bytemuck", -] - [[package]] name = "same-file" version = "1.0.6" @@ -2031,13 +2113,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" [[package]] -name = "ultraviolet" -version = "0.8.1" +name = "ttf-parser" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b9e3507eba17043af05c8a72fce3ec2c24b58945f45732e71dbc6646d904a7" -dependencies = [ - "wide", -] +checksum = "3e835d06ed78a500d3d0e431a20c18ff5544b3f6e11376e834370cfd35e8948e" [[package]] name = "unicode-width" @@ -2324,16 +2403,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "wide" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bbe7c604a27ca0b05c5503221e76da628225b568e6f1280b42dbad3b72d89b" -dependencies = [ - "bytemuck", - "safe_arch", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2397,15 +2466,6 @@ dependencies = [ "x11-dl", ] -[[package]] -name = "winit_input_helper" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d8af691f04e6d8a892e80a2176221b2c13d5832a8929d8c0fed1e3e3d4fe831" -dependencies = [ - "winit", -] - [[package]] name = "x11-dl" version = "2.19.1" diff --git a/Cargo.toml b/Cargo.toml index b33b0a3..e2faa5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,12 @@ anyhow = "1.0" bitfield = "0.13" clap = "2.33" gilrs = "0.8" -pixels = "0.7" winit = "0.25" -winit_input_helper = "0.10" +egui = "0.15" +wgpu = "0.11" +egui_wgpu_backend = "0.14" +egui_winit_platform = "0.11" +pollster = "0.2" rodio = "0.14" rtrb = "0.2" directories-next = "2.0" diff --git a/src/bus.rs b/src/bus.rs index 66f7ceb..66bb7ff 100644 --- a/src/bus.rs +++ b/src/bus.rs @@ -73,7 +73,10 @@ impl Bus { fn tick(&mut self, limit: u8) { for _ in 0..limit { self.timer.tick(); - self.cart.as_mut().map(|cart| cart.tick()); + + if let Some(c) = self.cart.as_mut() { + c.tick() + } self.ppu.tick(); self.apu.tick(self.timer.divider); self.dma_tick() diff --git a/src/cartridge.rs b/src/cartridge.rs index 562c94f..d2cf3ca 100644 --- a/src/cartridge.rs +++ b/src/cartridge.rs @@ -545,7 +545,7 @@ impl MBCIo for MBC3 { } Minute => self.rtc.min = byte & 0x3F, Hour => self.rtc.hr = byte & 0x1F, - DayLow => self.rtc.day_low = byte & 0xFF, + DayLow => self.rtc.day_low = byte, DayHigh => self.rtc.day_high = (byte & 0xC1).into(), }, _ => {} diff --git a/src/emu.rs b/src/emu.rs index 1bcde89..5342de0 100644 --- a/src/emu.rs +++ b/src/emu.rs @@ -8,14 +8,14 @@ use std::fs::File; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::Duration; -use winit_input_helper::WinitInputHelper; +use winit::event::KeyboardInput; pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED); pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz const DEFAULT_TITLE: &str = "DMG-01 Emulator"; -pub fn run_frame(emu: &mut Emulator, gamepad: &mut Gilrs, key: &WinitInputHelper) -> Cycle { +pub fn run_frame(emu: &mut Emulator, gamepad: &mut Gilrs, key: KeyboardInput) -> Cycle { let mut elapsed = 0; if let Some(event) = gamepad.next_event() { @@ -30,8 +30,8 @@ pub fn run_frame(emu: &mut Emulator, gamepad: &mut Gilrs, key: &WinitInputHelper elapsed } -pub fn draw_frame(emu: &Emulator, buf: &mut [u8; GB_HEIGHT * GB_WIDTH * 4]) { - buf.copy_from_slice(emu.cpu.bus.ppu.frame_buf.as_ref()); +pub fn pixel_buf(emu: &Emulator) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] { + emu.cpu.bus.ppu.frame_buf.as_ref() } pub struct Emulator { diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..e52fa77 --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,238 @@ +use egui::{ClippedMesh, CtxRef, TextureId}; +use egui_wgpu_backend::{BackendError, RenderPass, ScreenDescriptor}; +use egui_winit_platform::Platform; +use wgpu::{ + Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError, + Surface, SurfaceConfiguration, SurfaceTexture, Texture, TextureFormat, TextureUsages, + TextureView, +}; +use winit::error::OsError; +use winit::event::{ElementState, KeyboardInput}; +use winit::event_loop::EventLoop; +use winit::window::Window; + +use crate::{GB_HEIGHT, GB_WIDTH}; + +const EGUI_DIMENSIONS: (usize, usize) = (1280, 720); +const FILTER_MODE: FilterMode = FilterMode::Nearest; + +const SCALE: f32 = 3.0; + +/// Holds GUI State +#[derive(Debug, Clone)] +pub struct GuiState { + /// When true, egui winit should exit the application + pub quit: bool, + pub title: String, +} + +impl GuiState { + pub fn new(title: String) -> Self { + Self { + title, + quit: Default::default(), + } + } +} + +/// This is meant to serve as a Default value. Therefore this KeyboardInput should **always** represent an **unused** key +/// performing an **unused** action +/// +pub fn unused_key() -> KeyboardInput { + #![allow(deprecated)] + + KeyboardInput { + scancode: Default::default(), + state: ElementState::Released, + virtual_keycode: Default::default(), + modifiers: Default::default(), // this argument is deprecated + } +} + +pub fn build_window(event_loop: &EventLoop) -> Result { + use winit::dpi::PhysicalSize; + use winit::window::WindowBuilder; + + WindowBuilder::new() + .with_decorations(true) + .with_resizable(true) + .with_transparent(false) + .with_title("DMG-01 Emulator") + .with_inner_size(PhysicalSize { + width: EGUI_DIMENSIONS.0 as f32, + height: EGUI_DIMENSIONS.1 as f32, + }) + .build(event_loop) +} + +pub fn create_surface(window: &Window) -> (Instance, Surface) { + use wgpu::Backends; + + let instance = Instance::new(Backends::PRIMARY); + let surface = unsafe { instance.create_surface(window) }; + (instance, surface) +} + +pub fn request_adapter(instance: &Instance, surface: &Surface) -> Option { + use wgpu::{PowerPreference, RequestAdapterOptions}; + + pollster::block_on(instance.request_adapter(&RequestAdapterOptions { + power_preference: PowerPreference::HighPerformance, + force_fallback_adapter: false, // TODO: What do I want to do with this? + compatible_surface: Some(surface), + })) +} + +pub fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> { + use wgpu::{DeviceDescriptor, Features, Limits}; + + pollster::block_on(adapter.request_device( + &DeviceDescriptor { + label: None, + features: Features::default(), + limits: Limits::default(), + }, + None, + )) +} + +pub fn surface_config(window: &Window, format: TextureFormat) -> SurfaceConfiguration { + use wgpu::PresentMode; + + let size = window.inner_size(); + SurfaceConfiguration { + usage: TextureUsages::RENDER_ATTACHMENT, + format, + width: size.width as u32, + height: size.height as u32, + present_mode: PresentMode::Mailbox, + } +} + +pub fn platform_desc(window: &Window) -> Platform { + use egui::FontDefinitions; + use egui_winit_platform::PlatformDescriptor; + + let size = window.inner_size(); + Platform::new(PlatformDescriptor { + physical_width: size.width as u32, + physical_height: size.height as u32, + scale_factor: window.scale_factor(), + font_definitions: FontDefinitions::default(), + ..Default::default() + }) +} + +pub fn texture_size() -> Extent3d { + Extent3d { + width: GB_WIDTH as u32, + height: GB_HEIGHT as u32, + ..Default::default() + } +} + +pub fn create_texture(device: &Device, size: Extent3d) -> Texture { + use wgpu::{TextureDescriptor, TextureDimension}; + + device.create_texture(&TextureDescriptor { + size, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8UnormSrgb, + usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, + label: Some("gb_pixel_buffer"), + }) +} + +#[inline] +pub fn write_to_texture( + queue: &Queue, + texture: &Texture, + data: &[u8; GB_WIDTH * 4 * GB_HEIGHT], + size: Extent3d, +) { + use std::num::NonZeroU32; + use wgpu::{ImageCopyTexture, ImageDataLayout, Origin3d, TextureAspect}; + + queue.write_texture( + ImageCopyTexture { + texture, + mip_level: 0, + origin: Origin3d::ZERO, + aspect: TextureAspect::All, + }, + data, + ImageDataLayout { + offset: 0, + bytes_per_row: NonZeroU32::new(4 * GB_WIDTH as u32), + rows_per_image: NonZeroU32::new(GB_HEIGHT as u32), + }, + size, + ); +} + +pub fn expose_texture_to_egui( + render_pass: &mut RenderPass, + device: &Device, + texture: &Texture, +) -> TextureId { + render_pass.egui_texture_from_wgpu_texture(device, texture, FILTER_MODE) +} + +#[inline] +pub fn create_view(frame: &SurfaceTexture) -> TextureView { + use wgpu::TextureViewDescriptor; + + frame.texture.create_view(&TextureViewDescriptor::default()) +} + +#[inline] +pub fn create_command_encoder(device: &Device) -> CommandEncoder { + use wgpu::CommandEncoderDescriptor; + + device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("encoder"), + }) +} + +#[inline] +pub fn create_screen_descriptor( + window: &Window, + config: &SurfaceConfiguration, +) -> ScreenDescriptor { + ScreenDescriptor { + physical_width: config.width, + physical_height: config.height, + scale_factor: window.scale_factor() as f32, + } +} + +#[inline] +pub fn execute_render_pass( + render_pass: &mut RenderPass, + encoder: &mut CommandEncoder, + view: &TextureView, + jobs: Vec, + descriptor: &ScreenDescriptor, +) -> Result<(), BackendError> { + render_pass.execute(encoder, view, &jobs, descriptor, Some(wgpu::Color::BLACK)) +} + +#[inline] +pub fn draw_egui(app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) { + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + egui::menu::menu(ui, "File", |ui| { + if ui.button("Quit").clicked() { + app.quit = true; + } + }); + + egui::Window::new(&app.title).show(ctx, |ui| { + ui.image( + texture_id, + [GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE], + ); + }) + }); +} diff --git a/src/joypad.rs b/src/joypad.rs index 28479e0..ff072f1 100644 --- a/src/joypad.rs +++ b/src/joypad.rs @@ -1,5 +1,5 @@ use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType}; -use winit_input_helper::WinitInputHelper; +use winit::event::{ElementState, KeyboardInput, VirtualKeyCode}; #[derive(Debug)] pub struct Joypad { @@ -111,68 +111,33 @@ impl ButtonEvent { } #[inline] -pub(crate) fn handle_keyboard_input(pad: &mut Joypad, input: &WinitInputHelper) { - use winit::event::VirtualKeyCode; - - // TODO: What do I have to do to get a match statement here? - +pub(crate) fn handle_keyboard_input(pad: &mut Joypad, key: KeyboardInput) { let state = &mut pad.ext; let irq = &mut pad.interrupt; - if input.key_pressed(VirtualKeyCode::Down) { - state.dpad_down.update(true, irq); - } - if input.key_released(VirtualKeyCode::Down) { - state.dpad_down.update(false, irq); - } - - if input.key_pressed(VirtualKeyCode::Up) { - state.dpad_up.update(true, irq); - } - if input.key_released(VirtualKeyCode::Up) { - state.dpad_up.update(false, irq); - } - - if input.key_pressed(VirtualKeyCode::Left) { - state.dpad_left.update(true, irq); - } - if input.key_released(VirtualKeyCode::Left) { - state.dpad_left.update(false, irq); - } - - if input.key_pressed(VirtualKeyCode::Right) { - state.dpad_right.update(true, irq); - } - if input.key_released(VirtualKeyCode::Right) { - state.dpad_right.update(false, irq); - } - - if input.key_pressed(VirtualKeyCode::Return) { - state.start.update(true, irq); - } - if input.key_released(VirtualKeyCode::Return) { - state.start.update(false, irq); - } - - if input.key_pressed(VirtualKeyCode::RShift) { - state.select.update(true, irq); - } - if input.key_released(VirtualKeyCode::RShift) { - state.select.update(false, irq); - } - - if input.key_pressed(VirtualKeyCode::Z) { - state.south.update(true, irq); - } - if input.key_released(VirtualKeyCode::Z) { - state.south.update(false, irq); - } - - if input.key_pressed(VirtualKeyCode::X) { - state.east.update(true, irq); - } - if input.key_released(VirtualKeyCode::X) { - state.east.update(false, irq); + match key.state { + ElementState::Pressed => match key.virtual_keycode { + Some(VirtualKeyCode::Down) => state.dpad_down.update(true, irq), + Some(VirtualKeyCode::Up) => state.dpad_up.update(true, irq), + Some(VirtualKeyCode::Left) => state.dpad_left.update(true, irq), + Some(VirtualKeyCode::Right) => state.dpad_right.update(true, irq), + Some(VirtualKeyCode::Return) => state.start.update(true, irq), + Some(VirtualKeyCode::RShift) => state.select.update(true, irq), + Some(VirtualKeyCode::Z) => state.south.update(true, irq), + Some(VirtualKeyCode::X) => state.east.update(true, irq), + None | Some(_) => {} + }, + ElementState::Released => match key.virtual_keycode { + Some(VirtualKeyCode::Down) => state.dpad_down.update(false, irq), + Some(VirtualKeyCode::Up) => state.dpad_up.update(false, irq), + Some(VirtualKeyCode::Left) => state.dpad_left.update(false, irq), + Some(VirtualKeyCode::Right) => state.dpad_right.update(false, irq), + Some(VirtualKeyCode::Return) => state.start.update(false, irq), + Some(VirtualKeyCode::RShift) => state.select.update(false, irq), + Some(VirtualKeyCode::Z) => state.south.update(false, irq), + Some(VirtualKeyCode::X) => state.east.update(false, irq), + None | Some(_) => {} + }, } } diff --git a/src/lib.rs b/src/lib.rs index b2457f9..913df40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ mod bus; mod cartridge; mod cpu; pub mod emu; +pub mod gui; mod high_ram; mod instruction; mod interrupt; diff --git a/src/main.rs b/src/main.rs index 37bbc9b..cdf3d37 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,18 @@ -use anyhow::Result; +use std::time::Instant; + use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; -use gb::emu::{Emulator, CYCLES_IN_FRAME}; -use gb::{Cycle, GB_HEIGHT, GB_WIDTH}; +use egui_wgpu_backend::RenderPass; +use gb::emu::Emulator; +use gb::gui::GuiState; use gilrs::Gilrs; -use pixels::{PixelsBuilder, SurfaceTexture}; use rodio::{OutputStream, Sink}; use tracing_subscriber::EnvFilter; -use winit::dpi::{LogicalSize, PhysicalSize}; -use winit::event::{Event, VirtualKeyCode}; +use winit::event::{Event, WindowEvent}; use winit::event_loop::{ControlFlow, EventLoop}; -use winit::window::{Window, WindowBuilder}; -use winit_input_helper::WinitInputHelper; -const WINDOW_SCALE: usize = 3; const AUDIO_ENABLED: bool = true; -fn main() -> Result<()> { +fn main() { let app = App::new(crate_name!()) .version(crate_version!()) .author(crate_authors!()) @@ -48,10 +45,28 @@ fn main() -> Result<()> { .with_env_filter(EnvFilter::from_default_env()) .init(); + // --Here lies a lot of Winit + WGPU Boilerplate-- + let event_loop: EventLoop> = EventLoop::with_user_event(); + let window = gb::gui::build_window(&event_loop).expect("build window"); + + let (instance, surface) = gb::gui::create_surface(&window); + let adapter = gb::gui::request_adapter(&instance, &surface).expect("request adaptor"); + let (device, queue) = gb::gui::request_device(&adapter).expect("request device"); + let format = surface + .get_preferred_format(&adapter) + .expect("get surface format"); + + let mut config = gb::gui::surface_config(&window, format); + surface.configure(&device, &config); + let mut platform = gb::gui::platform_desc(&window); + let mut render_pass = RenderPass::new(&device, format, 1); + + // We interrupt your boiler plate to initialize the emulator so that + // we can copy it's empty pixel buffer to the GPU let mut emu = match m.value_of("boot") { Some(path) => { tracing::info!("User-provided boot ROM"); - Emulator::from_boot_rom(path)? + Emulator::from_boot_rom(path).expect("initialize emulator with custom boot rom") } None => { tracing::info!("Built-in boot ROM"); @@ -59,39 +74,33 @@ fn main() -> Result<()> { } }; + // Set up the WGPU (and then EGUI) texture we'll be working with. + let texture_size = gb::gui::texture_size(); + let texture = gb::gui::create_texture(&device, texture_size); + gb::gui::write_to_texture(&queue, &texture, gb::emu::pixel_buf(&emu), texture_size); + let texture_id = gb::gui::expose_texture_to_egui(&mut render_pass, &device, &texture); + + // Load ROM if filepath was provided if let Some(path) = m.value_of("rom") { tracing::info!("User-provided cartridge ROM"); - emu.read_game_rom(path)?; + emu.read_game_rom(path).expect("read game rom from path"); } - // Load Save file if it exists + // Load Save File if it exists + // FIXME: Shouldn't the API be better than this? emu.try_load_sav().expect("Load save if exists"); - let rom_title = emu.title(); + let rom_title = emu.title().to_string(); tracing::info!("Initialize Gamepad"); let mut gamepad = Gilrs::new().expect("Initialize Controller Support"); - // Initialize GUI - let event_loop = EventLoop::new(); - let mut input = WinitInputHelper::new(); - let window = create_window(&event_loop, rom_title)?; - - let mut pixels = { - let size = window.inner_size(); - let surface_texture = SurfaceTexture::new(size.width, size.height, &window); - - PixelsBuilder::new(GB_WIDTH as u32, GB_HEIGHT as u32, surface_texture) - .enable_vsync(false) - .build()? - }; - // Initialize Audio let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio"); if AUDIO_ENABLED { let (prod, cons) = gb::spsc_init(); let sink = { - let s = Sink::try_new(&stream_handle)?; + let s = Sink::try_new(&stream_handle).expect("create sink from audio stream handle"); s.append(cons); s.set_volume(0.1); s @@ -105,58 +114,85 @@ fn main() -> Result<()> { }); } - let mut cycle_count: Cycle = Default::default(); + // Set up state for the Immediate-mode GUI + let mut app = GuiState::new(rom_title); + let mut last_key = gb::gui::unused_key(); + + // used for egui animations + let start_time = Instant::now(); event_loop.run(move |event, _, control_flow| { - if let Event::RedrawRequested(_) = event { - if pixels.render().is_err() { - emu.try_write_sav().expect("Write game save if need be"); + platform.handle_event(&event); - *control_flow = ControlFlow::Exit; - return; - } - } + match event { + Event::MainEventsCleared => { + if app.quit { + *control_flow = ControlFlow::Exit; + } - if input.update(&event) { - if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { - emu.try_write_sav().expect("Write game save if need be"); + gb::emu::run_frame(&mut emu, &mut gamepad, last_key); - *control_flow = ControlFlow::Exit; - return; - } - - if let Some(size) = input.window_resized() { - pixels.resize_surface(size.width, size.height); - } - - cycle_count += gb::emu::run_frame(&mut emu, &mut gamepad, &input); - - if cycle_count >= CYCLES_IN_FRAME { - cycle_count %= CYCLES_IN_FRAME; - - let buf: &mut [u8; GB_WIDTH * GB_HEIGHT * 4] = pixels - .get_frame() - .try_into() - .expect("Size of Pixel Buffer is GB_WIDTH * GB_HEIGHT * 4"); - - gb::emu::draw_frame(&emu, buf); window.request_redraw(); } + Event::RedrawRequested(..) => { + platform.update_time(start_time.elapsed().as_secs_f64()); + + let data = gb::emu::pixel_buf(&emu); + gb::gui::write_to_texture(&queue, &texture, data, texture_size); + + let output_frame = match surface.get_current_texture() { + Ok(frame) => frame, + Err(e) => { + eprintln!("Dropped frame with error: {}", e); + return; + } + }; + let output_view = gb::gui::create_view(&output_frame); + + // Begin to draw Egui components + platform.begin_frame(); + gb::gui::draw_egui(&mut app, &platform.context(), texture_id); + // End the UI frame. We could now handle the output and draw the UI with the backend. + let (_, paint_commands) = platform.end_frame(Some(&window)); + let paint_jobs = platform.context().tessellate(paint_commands); + + let mut encoder = gb::gui::create_command_encoder(&device); + let screen_descriptor = gb::gui::create_screen_descriptor(&window, &config); + + // Upload all resources for the GPU. + render_pass.update_texture(&device, &queue, &platform.context().texture()); + render_pass.update_user_textures(&device, &queue); + render_pass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor); + + // Record all render passes. + gb::gui::execute_render_pass( + &mut render_pass, + &mut encoder, + &output_view, + paint_jobs, + &screen_descriptor, + ) + .expect("record render passes"); + + // Submit the commands. + queue.submit(std::iter::once(encoder.finish())); + + // Redraw egui + output_frame.present(); + } + Event::WindowEvent { event, .. } => match event { + WindowEvent::Resized(size) => { + config.width = size.width; + config.height = size.height; + surface.configure(&device, &config); + } + WindowEvent::CloseRequested => { + *control_flow = ControlFlow::Exit; + } + WindowEvent::KeyboardInput { input, .. } => last_key = input, + _ => {} + }, + _ => {} } }); } - -fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result { - let logical = LogicalSize::new(GB_WIDTH as f64, GB_HEIGHT as f64); - let physical = PhysicalSize::new( - (GB_WIDTH * WINDOW_SCALE) as f32, - (GB_HEIGHT * WINDOW_SCALE) as f32, - ); - - Ok(WindowBuilder::new() - .with_title(title) - .with_min_inner_size(logical) - .with_inner_size(physical) - .with_resizable(true) - .build(event_loop)?) -}