use std::{ collections::hash_map::Entry, num::NonZero, sync::Arc, thread, time::{Duration, Instant}, }; use egui::{ Context, FontData, FontDefinitions, FontFamily, IconData, PlatformOutput, RawInput, TextWrapMode, ViewportBuilder, ViewportCommand, ViewportEvent, ViewportId, ViewportIdMap, ViewportIdSet, ViewportInfo, style::ScrollStyle, }; use egui_wgpu::winit::Painter; use egui_winit::EventResponse; use gilrs::{EventType, Gilrs}; use tracing::{error, warn}; use winit::{ application::ApplicationHandler, event::WindowEvent, event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy}, window::{Window, WindowId}, }; use crate::{ config::CliArgs, controller::ControllerManager, emulator::{EmulatorClient, EmulatorCommand, SimId}, images::ImageTextureLoader, input::{MappingProvider, ShortcutProvider}, memory::MemoryClient, persistence::Persistence, window::{AppWindow, ChildWindow, GameScreen, GameWindow, InitArgs}, }; const EGUI_FILENAME: &str = "egui"; fn load_icon() -> anyhow::Result { let bytes = include_bytes!("../assets/lemur-256x256.png"); let img = image::load_from_memory_with_format(bytes, image::ImageFormat::Png)?; let rgba = img.into_rgba8(); Ok(IconData { width: rgba.width(), height: rgba.height(), rgba: rgba.into_vec(), }) } struct SharedViewportState { viewport_info: ViewportIdMap, painter: Painter, resized_viewport: Option, } pub struct Application { client: EmulatorClient, persistence: Persistence, ctx: Context, shared: SharedViewportState, icon: Option>, app: GameWindow, controllers: ControllerManager, viewports: ViewportIdMap, focused: Option, redraw_times: ViewportIdMap, initial_windows: Vec, } impl Application { pub fn new( client: EmulatorClient, proxy: EventLoopProxy, persistence: Persistence, args: CliArgs, ) -> Self { let wgpu = WgpuState::new(); let icon = load_icon().ok().map(Arc::new); let mappings = MappingProvider::new(persistence.clone(), args.player2_controller); let shortcuts = ShortcutProvider::new(persistence.clone()); let controllers = ControllerManager::new(client.clone(), &mappings); let memory = Arc::new(MemoryClient::new(client.clone())); let images = Arc::new(ImageTextureLoader::new()); { let mappings = mappings.clone(); let proxy = proxy.clone(); thread::spawn(|| process_gamepad_input(mappings, proxy)); } let app = GameWindow::new( ViewportId::ROOT, client.clone(), proxy.clone(), persistence.clone(), shortcuts.clone(), &memory, &images, mappings.clone(), SimId::Player1, ); let ctx = Context::default(); let data = persistence.load_config(EGUI_FILENAME).unwrap_or_default(); ctx.data_mut(|d| *d = data); let mut fonts = FontDefinitions::default(); fonts.font_data.insert( "Selawik".into(), Arc::new(FontData::from_static(include_bytes!( "../assets/selawik.ttf" ))), ); fonts .families .get_mut(&FontFamily::Proportional) .unwrap() .insert(0, "Selawik".into()); ctx.set_fonts(fonts); ctx.global_style_mut(|s| { s.wrap_mode = Some(TextWrapMode::Extend); s.visuals.menu_corner_radius = Default::default(); s.spacing.scroll = ScrollStyle::thin(); }); egui_extras::install_image_loaders(&ctx); ctx.add_texture_loader(images.clone()); ctx.set_embed_viewports(false); { let proxy = proxy.clone(); ctx.set_request_repaint_callback(move |info| { let _ = proxy.send_event(UserEvent::RequestRedraw( info.viewport_id, Instant::now() + info.delay, )); }); } let wgpu_config = egui_wgpu::WgpuConfiguration { present_mode: wgpu::PresentMode::AutoVsync, wgpu_setup: egui_wgpu::WgpuSetup::Existing(egui_wgpu::WgpuSetupExisting { instance: wgpu.instance.clone(), adapter: wgpu.adapter.clone(), device: wgpu.device.clone(), queue: wgpu.queue.clone(), }), ..egui_wgpu::WgpuConfiguration::default() }; let options = egui_wgpu::RendererOptions::default(); let painter = pollster::block_on(Painter::new(ctx.clone(), wgpu_config, false, options)); let mut initial_windows = vec![]; if let Some(port) = args.debug_port { initial_windows.push(ChildWindow::Debugger { port: Some(port) }); } if args.profile { initial_windows.push(ChildWindow::Profiler { launch: true }); } if args.character_data { initial_windows.push(ChildWindow::CharacterData); } if args.bgmap_data { initial_windows.push(ChildWindow::BgMap); } if args.object_data { initial_windows.push(ChildWindow::Objects); } if args.worlds { initial_windows.push(ChildWindow::Worlds); } if args.frame_buffers { initial_windows.push(ChildWindow::FrameBuffers); } if args.registers { initial_windows.push(ChildWindow::Registers); } if args.terminal { initial_windows.push(ChildWindow::Terminal); } if args.player2 { client.send_command(EmulatorCommand::StartSecondSim(args.rom.clone())); initial_windows.push(ChildWindow::Player2); } Self { client, persistence, ctx, shared: SharedViewportState { viewport_info: ViewportIdMap::default(), painter, resized_viewport: None, }, icon, app, controllers, viewports: ViewportIdMap::default(), focused: None, redraw_times: ViewportIdMap::default(), initial_windows, } } fn check_repaint(&mut self, event_loop: &ActiveEventLoop) { let now = Instant::now(); self.redraw_times.retain(|viewport_id, time| { if *time > now { return true; } if let Some(viewport) = self.viewports.get(viewport_id) { viewport.window.request_redraw(); } false }); if let Some(next_repaint_time) = self.redraw_times.values().min().copied() { event_loop.set_control_flow(ControlFlow::WaitUntil(next_repaint_time)); } } fn repaint_all(&mut self, event_loop: &ActiveEventLoop) { enum ViewportUpdate { Keep { recreate: bool, builder: ViewportBuilder, parent: ViewportId, commands: Vec, }, Remove, } let mut updates = ViewportIdMap::::default(); for viewport in self.viewports.values() { updates.insert( viewport.id, ViewportUpdate::Keep { recreate: false, builder: viewport.builder.clone(), parent: self .shared .viewport_info .get(&viewport.id) .and_then(|vp| vp.parent) .unwrap_or(ViewportId::ROOT), commands: vec![], }, ); } for viewport in self.viewports.values_mut() { let mut input = viewport.take_egui_input(); if !self.shared.viewport_info.contains_key(&viewport.id) { continue; } input.viewports = self.shared.viewport_info.clone(); let output = self.ctx.run_ui(input, |ui| { if viewport.id == ViewportId::ROOT { self.app.show(ui); } else if let Some(cb) = ui.viewport(|v| v.viewport_ui_cb.clone()) { cb(ui); } }); viewport.handle_platform_output(output.platform_output); for (id, update) in updates.iter_mut() { if !output.viewport_output.contains_key(id) { self.shared.viewport_info.remove(id); *update = ViewportUpdate::Remove; } } for (id, mut vp) in output.viewport_output { match updates.entry(id) { Entry::Vacant(e) => { e.insert(ViewportUpdate::Keep { recreate: true, builder: vp.builder, parent: vp.parent, commands: vp.commands, }); } Entry::Occupied(e) => { let ViewportUpdate::Keep { recreate, builder, commands, .. } = e.into_mut() else { continue; }; let (_, should_recreate) = builder.patch(vp.builder); *recreate |= should_recreate; commands.append(&mut vp.commands); } } } let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point); self.shared.painter.paint_and_update_textures( viewport.id, output.pixels_per_point, [0.0, 0.0, 0.0, 0.0], &clipped_primitives, &output.textures_delta, vec![], ); viewport.window.pre_present_notify(); } let mut active_viewports = ViewportIdSet::default(); for (id, update) in updates { match update { ViewportUpdate::Keep { recreate, builder, parent, commands, } => { active_viewports.insert(id); let manager = self .viewports .remove(&id) .filter(|_| !recreate) .unwrap_or_else(|| { let manager = ViewportManager::new( &self.ctx, id, parent, event_loop, builder, &mut self.shared, ); self.app.handle_init( id, InitArgs { window: &manager.window, render_state: &self.shared.painter.render_state().unwrap(), }, ); manager }); manager.process_viewport_commands(commands, &mut self.shared); self.viewports.insert(id, manager); } ViewportUpdate::Remove => { self.viewports.remove(&id); } } } if !active_viewports.contains(&ViewportId::ROOT) { event_loop.exit(); } self.shared.painter.gc_viewports(&active_viewports); } } impl ApplicationHandler for Application { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let mut viewport_builder = self.app.initial_viewport(); if let Some(icon) = &self.icon { viewport_builder = viewport_builder.with_icon(icon.clone()); } let manager = ViewportManager::new( &self.ctx, ViewportId::ROOT, ViewportId::ROOT, event_loop, viewport_builder, &mut self.shared, ); let render_state = self.shared.painter.render_state().unwrap(); GameScreen::init_pipeline(&render_state); self.app.handle_init( ViewportId::ROOT, InitArgs { window: &manager.window, render_state: &render_state, }, ); self.viewports.insert(ViewportId::ROOT, manager); for window in std::mem::take(&mut self.initial_windows) { self.app.open(window); } } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) { viewport.window.request_redraw(); } } fn window_event( &mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent, ) { let Some(viewport) = self .viewports .values_mut() .find(|v| v.has_window_id(window_id)) else { return; }; let viewport_id = viewport.id; let (response, close_requested) = viewport.on_window_event(&event, &mut self.shared); if close_requested && viewport_id == ViewportId::ROOT { event_loop.exit(); } if !response.consumed { match event { WindowEvent::KeyboardInput { event, .. } => { if !self.app.handle_key_event(viewport_id, &event) { self.controllers.handle_key_event(&event); } } WindowEvent::Focused(new_focused) => { self.focused = new_focused.then_some(viewport_id); } _ => {} } } if response.repaint { self.repaint_all(event_loop); } self.check_repaint(event_loop); } fn device_event( &mut self, _event_loop: &ActiveEventLoop, _device_id: winit::event::DeviceId, event: winit::event::DeviceEvent, ) { if let winit::event::DeviceEvent::MouseMotion { delta } = event { let Some(viewport) = self .focused .as_ref() .and_then(|id| self.viewports.get_mut(id)) else { return; }; viewport.on_mouse_motion(delta); } } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { match event { UserEvent::GamepadEvent(event) => { if !self .focused .is_some_and(|id| self.app.handle_gamepad_event(id, &event)) { self.controllers.handle_gamepad_event(&event); } } UserEvent::Quit(sim_id) => match sim_id { SimId::Player1 => event_loop.exit(), SimId::Player2 => self.app.close(ChildWindow::Player2), }, UserEvent::RequestRedraw(viewport, when) => { let scheduled = self.redraw_times.entry(viewport).or_insert(when); if *scheduled > when { *scheduled = when; } } } self.check_repaint(event_loop); } fn exiting(&mut self, _event_loop: &ActiveEventLoop) { if let Err(error) = self .ctx .data(|d| self.persistence.save_config(EGUI_FILENAME, d)) { error!(%error, "could not save egui state."); } let (sender, receiver) = oneshot::channel(); if self.client.send_command(EmulatorCommand::Exit(sender)) && let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) { error!(%error, "could not gracefully exit."); } } } struct WgpuState { instance: wgpu::Instance, adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, } impl WgpuState { fn new() -> Self { #[allow(unused_variables)] let egui_wgpu::WgpuConfiguration { wgpu_setup: egui_wgpu::WgpuSetup::CreateNew(egui_wgpu::WgpuSetupCreateNew { instance_descriptor: wgpu::InstanceDescriptor { backends, .. }, device_descriptor, .. }), .. } = egui_wgpu::WgpuConfiguration::default() else { panic!("required fields not found") }; #[cfg(windows)] let backends = wgpu::Backends::from_env() .unwrap_or((wgpu::Backends::PRIMARY | wgpu::Backends::GL) - wgpu::Backends::VULKAN); let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends, ..wgpu::InstanceDescriptor::new_without_display_handle_from_env() }); let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::LowPower), compatible_surface: None, force_fallback_adapter: false, })) .expect("could not create adapter"); let (device, queue) = pollster::block_on(adapter.request_device(&(*device_descriptor)(&adapter))) .expect("could not request device"); Self { instance, adapter, device, queue, } } } struct ViewportManager { id: ViewportId, window: Arc, state: egui_winit::State, builder: ViewportBuilder, } impl ViewportManager { fn new( ctx: &Context, viewport_id: ViewportId, parent_id: ViewportId, event_loop: &ActiveEventLoop, builder: ViewportBuilder, shared: &mut SharedViewportState, ) -> Self { let window = Arc::new(egui_winit::create_window(ctx, event_loop, &builder).unwrap()); pollster::block_on(shared.painter.set_window(viewport_id, Some(window.clone()))).unwrap(); let state = egui_winit::State::new( ctx.clone(), viewport_id, event_loop, Some(window.scale_factor() as f32), event_loop.system_theme(), shared.painter.max_texture_side(), ); let mut info = ViewportInfo { parent: Some(parent_id), ..ViewportInfo::default() }; egui_winit::update_viewport_info(&mut info, ctx, &window, true); shared.viewport_info.insert(viewport_id, info); Self { id: viewport_id, window, state, builder, } } fn has_window_id(&self, window_id: WindowId) -> bool { self.window.id() == window_id } fn on_mouse_motion(&mut self, delta: (f64, f64)) { self.state.on_mouse_motion(delta); } fn on_window_event( &mut self, event: &WindowEvent, shared: &mut SharedViewportState, ) -> (EventResponse, bool) { if let WindowEvent::Resized(size) = event && let (Some(width), Some(height)) = (NonZero::new(size.width), NonZero::new(size.height)) { if shared.resized_viewport != Some(self.id) { shared.resized_viewport = Some(self.id); shared.painter.on_window_resize_state_change(self.id, true); } shared.painter.on_window_resized(self.id, width, height); } let response = self.state.on_window_event(&self.window, event); let info = shared.viewport_info.get_mut(&self.id).unwrap(); if let WindowEvent::CloseRequested = event { info.events.push(ViewportEvent::Close); } egui_winit::update_viewport_info(info, self.state.egui_ctx(), &self.window, false); (response, info.close_requested()) } fn take_egui_input(&mut self) -> RawInput { self.state.take_egui_input(&self.window) } fn handle_platform_output(&mut self, platform_output: PlatformOutput) { self.state .handle_platform_output(&self.window, platform_output); } fn process_viewport_commands( &self, commands: Vec, shared: &mut SharedViewportState, ) { let info = shared.viewport_info.get_mut(&self.id).unwrap(); egui_winit::process_viewport_commands( self.state.egui_ctx(), info, commands, &self.window, &mut vec![], ); } } #[derive(Debug)] pub enum UserEvent { GamepadEvent(gilrs::Event), Quit(SimId), RequestRedraw(ViewportId, Instant), } fn process_gamepad_input(mappings: MappingProvider, proxy: EventLoopProxy) { let mut gilrs = match Gilrs::new() { Ok(gilrs) => gilrs, Err(error) => { warn!(%error, "could not connect gamepad listener"); return; } }; while let Some(event) = gilrs.next_event_blocking(None) { if event.event == EventType::Connected { let Some(gamepad) = gilrs.connected_gamepad(event.id) else { continue; }; mappings.handle_gamepad_connect(&gamepad); } if event.event == EventType::Disconnected { mappings.handle_gamepad_disconnect(event.id); } if proxy.send_event(UserEvent::GamepadEvent(event)).is_err() { // main thread has closed! we done return; } } }