use std::{collections::HashSet, num::NonZero, sync::Arc, thread, time::Duration}; use egui::{ ahash::{HashMap, HashMapExt}, Context, FontData, FontDefinitions, FontFamily, IconData, TextWrapMode, ViewportBuilder, ViewportCommand, ViewportId, ViewportInfo, }; use gilrs::{EventType, Gilrs}; use tracing::{error, warn}; use winit::{ application::ApplicationHandler, event::WindowEvent, event_loop::{ActiveEventLoop, EventLoopProxy}, window::Window, }; use crate::{ controller::ControllerManager, emulator::{EmulatorClient, EmulatorCommand, SimId}, input::MappingProvider, memory::MemoryClient, persistence::Persistence, vram::VramProcessor, window::{ AboutWindow, AppWindow, BgMapWindow, CharacterDataWindow, GameWindow, GdbServerWindow, InputWindow, ObjectWindow, }, }; 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(), }) } pub struct Application { icon: Option>, wgpu: WgpuState, client: EmulatorClient, proxy: EventLoopProxy, mappings: MappingProvider, controllers: ControllerManager, memory: Arc, vram: VramProcessor, persistence: Persistence, viewports: HashMap, focused: Option, init_debug_port: Option, } impl Application { pub fn new( client: EmulatorClient, proxy: EventLoopProxy, debug_port: Option, ) -> Self { let wgpu = WgpuState::new(); let icon = load_icon().ok().map(Arc::new); let persistence = Persistence::new(); let mappings = MappingProvider::new(persistence.clone()); let controllers = ControllerManager::new(client.clone(), &mappings); let memory = Arc::new(MemoryClient::new(client.clone())); let vram = VramProcessor::new(); { let mappings = mappings.clone(); let proxy = proxy.clone(); thread::spawn(|| process_gamepad_input(mappings, proxy)); } Self { icon, wgpu, client, proxy, mappings, memory, vram, controllers, persistence, viewports: HashMap::new(), focused: None, init_debug_port: debug_port, } } fn open(&mut self, event_loop: &ActiveEventLoop, window: Box) { let viewport_id = window.viewport_id(); if self.viewports.contains_key(&viewport_id) { return; } self.viewports.insert( viewport_id, Viewport::new(event_loop, &self.wgpu, self.icon.clone(), window), ); } } impl ApplicationHandler for Application { fn resumed(&mut self, event_loop: &ActiveEventLoop) { if let Some(port) = self.init_debug_port { let mut server = GdbServerWindow::new(SimId::Player1, self.client.clone(), self.proxy.clone()); server.launch(port); self.open(event_loop, Box::new(server)); } let app = GameWindow::new( self.client.clone(), self.proxy.clone(), self.persistence.clone(), SimId::Player1, ); self.open(event_loop, Box::new(app)); } fn window_event( &mut self, event_loop: &ActiveEventLoop, window_id: winit::window::WindowId, event: WindowEvent, ) { let Some(viewport) = self .viewports .values_mut() .find(|v| v.window.id() == window_id) else { return; }; let viewport_id = viewport.id(); let mut queue_redraw = false; let mut inactive_viewports = HashSet::new(); let (consumed, action) = viewport.on_window_event(&event); if !consumed { match event { WindowEvent::KeyboardInput { event, .. } => { if !viewport.app.handle_key_event(&event) { self.controllers.handle_key_event(&event); } } WindowEvent::Focused(new_focused) => { self.focused = new_focused.then_some(viewport_id); } _ => {} } } match action { Some(Action::Redraw) => { for viewport in self.viewports.values_mut() { match viewport.redraw(event_loop) { Some(Action::Redraw) => { queue_redraw = true; } Some(Action::Close) => { inactive_viewports.insert(viewport.id()); } None => {} } } } Some(Action::Close) => { inactive_viewports.insert(viewport_id); } None => {} } self.viewports .retain(|k, _| !inactive_viewports.contains(k)); match self.viewports.get(&ViewportId::ROOT) { Some(viewport) => { if queue_redraw { viewport.window.request_redraw(); } } None => event_loop.exit(), } } 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.state.on_mouse_motion(delta); } } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { match event { UserEvent::GamepadEvent(event) => { if let Some(viewport) = self .focused .as_ref() .and_then(|id| self.viewports.get_mut(id)) { if viewport.app.handle_gamepad_event(&event) { return; } } self.controllers.handle_gamepad_event(&event); } UserEvent::OpenAbout => { let about = AboutWindow; self.open(event_loop, Box::new(about)); } UserEvent::OpenCharacterData(sim_id) => { let vram = CharacterDataWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(vram)); } UserEvent::OpenBgMap(sim_id) => { let bgmap = BgMapWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(bgmap)); } UserEvent::OpenObjects(sim_id) => { let objects = ObjectWindow::new(sim_id, &self.memory, &mut self.vram); self.open(event_loop, Box::new(objects)); } UserEvent::OpenDebugger(sim_id) => { let debugger = GdbServerWindow::new(sim_id, self.client.clone(), self.proxy.clone()); self.open(event_loop, Box::new(debugger)); } UserEvent::OpenInput => { let input = InputWindow::new(self.mappings.clone()); self.open(event_loop, Box::new(input)); } UserEvent::OpenPlayer2 => { let p2 = GameWindow::new( self.client.clone(), self.proxy.clone(), self.persistence.clone(), SimId::Player2, ); self.open(event_loop, Box::new(p2)); } UserEvent::Quit(sim_id) => { self.viewports .retain(|_, viewport| viewport.app.sim_id() != sim_id); if !self.viewports.contains_key(&ViewportId::ROOT) { event_loop.exit(); } } } } fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { if let Some(viewport) = self.viewports.get(&ViewportId::ROOT) { viewport.window.request_redraw(); } } fn exiting(&mut self, _event_loop: &ActiveEventLoop) { let (sender, receiver) = oneshot::channel(); if self.client.send_command(EmulatorCommand::Exit(sender)) { if let Err(error) = receiver.recv_timeout(Duration::from_secs(5)) { error!(%error, "could not gracefully exit."); } } } } struct WgpuState { instance: Arc, adapter: Arc, device: Arc, queue: Arc, } impl WgpuState { fn new() -> Self { #[allow(unused_variables)] let egui_wgpu::WgpuConfiguration { wgpu_setup: egui_wgpu::WgpuSetup::CreateNew { supported_backends, device_descriptor, .. }, .. } = egui_wgpu::WgpuConfiguration::default() else { panic!("required fields not found") }; #[cfg(windows)] let supported_backends = wgpu::util::backend_bits_from_env() .unwrap_or((wgpu::Backends::PRIMARY | wgpu::Backends::GL) - wgpu::Backends::VULKAN); let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: supported_backends, ..wgpu::InstanceDescriptor::default() }); let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, compatible_surface: None, force_fallback_adapter: false, })) .expect("could not create adapter"); let trace_path = std::env::var("WGPU_TRACE"); let (device, queue) = pollster::block_on(adapter.request_device( &(*device_descriptor)(&adapter), trace_path.ok().as_ref().map(std::path::Path::new), )) .expect("could not request device"); Self { instance: Arc::new(instance), adapter: Arc::new(adapter), device: Arc::new(device), queue: Arc::new(queue), } } } struct Viewport { painter: egui_wgpu::winit::Painter, ctx: Context, info: ViewportInfo, commands: Vec, builder: ViewportBuilder, window: Arc, state: egui_winit::State, app: Box, } impl Viewport { pub fn new( event_loop: &ActiveEventLoop, wgpu: &WgpuState, icon: Option>, mut app: Box, ) -> Self { let ctx = Context::default(); 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.style_mut(|s| { s.wrap_mode = Some(TextWrapMode::Extend); s.visuals.menu_rounding = Default::default(); }); egui_extras::install_image_loaders(&ctx); let wgpu_config = egui_wgpu::WgpuConfiguration { present_mode: wgpu::PresentMode::AutoNoVsync, wgpu_setup: egui_wgpu::WgpuSetup::Existing { instance: wgpu.instance.clone(), adapter: wgpu.adapter.clone(), device: wgpu.device.clone(), queue: wgpu.queue.clone(), }, ..egui_wgpu::WgpuConfiguration::default() }; let mut painter = egui_wgpu::winit::Painter::new(ctx.clone(), wgpu_config, 1, None, false, true); let mut info = ViewportInfo::default(); let mut builder = app.initial_viewport(); if let Some(icon) = icon { builder = builder.with_icon(icon); } let (window, state) = create_window_and_state(&ctx, event_loop, &builder, &mut painter); egui_winit::update_viewport_info(&mut info, &ctx, &window, true); app.on_init(&ctx, painter.render_state().as_ref().unwrap()); Self { painter, ctx, info, commands: vec![], builder, window, state, app, } } pub fn id(&self) -> ViewportId { self.app.viewport_id() } pub fn on_window_event(&mut self, event: &WindowEvent) -> (bool, Option) { let response = self.state.on_window_event(&self.window, event); egui_winit::update_viewport_info( &mut self.info, self.state.egui_ctx(), &self.window, false, ); let action = match event { WindowEvent::RedrawRequested => Some(Action::Redraw), WindowEvent::CloseRequested => Some(Action::Close), WindowEvent::Resized(size) => { if let (Some(width), Some(height)) = (NonZero::new(size.width), NonZero::new(size.height)) { self.painter .on_window_resized(ViewportId::ROOT, width, height); } None } _ if response.repaint => Some(Action::Redraw), _ => None, }; (response.consumed, action) } fn redraw(&mut self, event_loop: &ActiveEventLoop) -> Option { let mut input = self.state.take_egui_input(&self.window); input.viewports = std::iter::once((ViewportId::ROOT, self.info.clone())).collect(); let mut output = self.ctx.run(input, |ctx| { self.app.show(ctx); }); let clipped_primitives = self.ctx.tessellate(output.shapes, output.pixels_per_point); self.painter.paint_and_update_textures( ViewportId::ROOT, output.pixels_per_point, [0.0, 0.0, 0.0, 0.0], &clipped_primitives, &output.textures_delta, vec![], ); self.state .handle_platform_output(&self.window, output.platform_output); let Some(mut viewport_output) = output.viewport_output.remove(&ViewportId::ROOT) else { return Some(Action::Close); }; let (mut deferred_commands, recreate) = self.builder.patch(viewport_output.builder); if recreate { let (window, state) = create_window_and_state(&self.ctx, event_loop, &self.builder, &mut self.painter); egui_winit::update_viewport_info(&mut self.info, &self.ctx, &window, true); self.window = window; self.state = state; } self.commands.append(&mut deferred_commands); self.commands.append(&mut viewport_output.commands); egui_winit::process_viewport_commands( &self.ctx, &mut self.info, std::mem::take(&mut self.commands), &self.window, &mut HashSet::default(), ); if self.info.close_requested() { Some(Action::Close) } else { Some(Action::Redraw) } } } impl Drop for Viewport { fn drop(&mut self) { self.app.on_destroy(); } } #[derive(Debug)] pub enum UserEvent { GamepadEvent(gilrs::Event), OpenAbout, OpenCharacterData(SimId), OpenBgMap(SimId), OpenObjects(SimId), OpenDebugger(SimId), OpenInput, OpenPlayer2, Quit(SimId), } pub enum Action { Redraw, Close, } fn create_window_and_state( ctx: &Context, event_loop: &ActiveEventLoop, builder: &ViewportBuilder, painter: &mut egui_wgpu::winit::Painter, ) -> (Arc, egui_winit::State) { pollster::block_on(painter.set_window(ViewportId::ROOT, None)).unwrap(); let window = Arc::new(egui_winit::create_window(ctx, event_loop, builder).unwrap()); pollster::block_on(painter.set_window(ViewportId::ROOT, Some(window.clone()))).unwrap(); let state = egui_winit::State::new( ctx.clone(), ViewportId::ROOT, event_loop, Some(window.scale_factor() as f32), event_loop.system_theme(), painter.max_texture_side(), ); (window, state) } 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; } } }