diff --git a/tetanes/src/nes.rs b/tetanes/src/nes.rs index 01a0ef6a..a308efcf 100644 --- a/tetanes/src/nes.rs +++ b/tetanes/src/nes.rs @@ -113,6 +113,12 @@ impl Nes { } } + /// Request renderer resources (creating gui context, window, painter, etc). + /// + /// # Errors + /// + /// Returns an error if any resources can't be created correctly or `init_running` has already + /// been called. pub(crate) fn request_resources( &mut self, event_loop: &EventLoopWindowTarget, @@ -130,15 +136,15 @@ impl Nes { let window = Arc::clone(&window); let event_tx = tx.clone(); async move { + debug!("creating painter..."); match Renderer::create_painter(window).await { Ok(painter) => { painter_tx.send(painter).expect("failed to send painter"); event_tx.nes_event(RendererEvent::ResourcesReady); } Err(err) => { - event_tx.nes_event(UiEvent::Error(format!( - "failed to create painter: {err:?}" - ))); + error!("failed to create painter: {err:?}"); + event_tx.nes_event(UiEvent::Terminate); } } } diff --git a/tetanes/src/nes/event.rs b/tetanes/src/nes/event.rs index 04c88a37..3b397379 100644 --- a/tetanes/src/nes/event.rs +++ b/tetanes/src/nes/event.rs @@ -222,7 +222,7 @@ impl Nes { trace!("event: {event:?}"); } - match event { + match &event { Event::Resumed => { let state = if let State::Running(state) = &mut self.state { if platform::supports(platform::Feature::Suspend) { @@ -242,37 +242,49 @@ impl Nes { state.repaint_times.insert(window_id, Instant::now()); } } - Event::UserEvent(NesEvent::Renderer(RendererEvent::ResourcesReady)) => { - if let Err(err) = self.init_running(event_loop) { - error!("failed to create window: {err:?}"); - event_loop.exit(); - return; - } + Event::UserEvent(event) => match event { + NesEvent::Renderer(RendererEvent::ResourcesReady) => { + if let Err(err) = self.init_running(event_loop) { + error!("failed to create window: {err:?}"); + event_loop.exit(); + return; + } - // Disable device events to save some cpu as they're mostly duplicated in - // WindowEvents - event_loop.listen_device_events(DeviceEvents::Never); + // Disable device events to save some cpu as they're mostly duplicated in + // WindowEvents + event_loop.listen_device_events(DeviceEvents::Never); - if let State::Running(state) = &mut self.state { - if let Some(window) = state.renderer.root_window() { - if window.is_visible().unwrap_or(true) { - state.repaint_times.insert(window.id(), Instant::now()); - } else { - // Immediately redraw the root window on start if not - // visible. Fixes a bug where `window.request_redraw()` events - // may not be sent if the window isn't visible, which is the - // case until the first frame is drawn. - if let Err(err) = state.renderer.redraw( - window.id(), - event_loop, - &mut state.gamepads, - &mut state.cfg, - ) { - state.renderer.on_error(err); + if let State::Running(state) = &mut self.state { + if let Some(window) = state.renderer.root_window() { + if window.is_visible().unwrap_or(true) { + state.repaint_times.insert(window.id(), Instant::now()); + } else { + // Immediately redraw the root window on start if not + // visible. Fixes a bug where `window.request_redraw()` events + // may not be sent if the window isn't visible, which is the + // case until the first frame is drawn. + if let Err(err) = state.renderer.redraw( + window.id(), + event_loop, + &mut state.gamepads, + &mut state.cfg, + ) { + state.renderer.on_error(err); + } } } } } + NesEvent::Ui(UiEvent::Terminate) => event_loop.exit(), + _ => (), + }, + Event::LoopExiting => { + #[cfg(feature = "profiling")] + puffin::set_scopes_on(false); + + // Wasm should never be able to exit + #[cfg(target_arch = "wasm32")] + panic!("exited unexpectedly"); } _ => (), } @@ -350,7 +362,7 @@ impl Running { Event::WindowEvent { window_id, event, .. } => { - let res = self.renderer.on_window_event(window_id, &event, &self.cfg); + let res = self.renderer.on_window_event(window_id, &event); if res.repaint && event != WindowEvent::RedrawRequested { self.repaint_times.insert(window_id, Instant::now()); } @@ -466,24 +478,19 @@ impl Running { ); } } - NesEvent::Ui(event) => { - if let UiEvent::Terminate = event { - event_loop.exit() - } else { - self.on_ui_event(event); - } - } + NesEvent::Ui(event) => self.on_ui_event(event), _ => (), } } Event::LoopExiting => { - #[cfg(feature = "profiling")] - puffin::set_scopes_on(false); - if let Err(err) = self.renderer.save(&self.cfg) { error!("failed to save rendererer state: {err:?}"); } self.renderer.destroy(); + + // Wasm should never be able to exit + #[cfg(target_arch = "wasm32")] + panic!("exited unexpectedly"); } _ => (), } diff --git a/tetanes/src/nes/renderer.rs b/tetanes/src/nes/renderer.rs index 574bc526..c3084887 100644 --- a/tetanes/src/nes/renderer.rs +++ b/tetanes/src/nes/renderer.rs @@ -728,22 +728,54 @@ impl Renderer { use wgpu::Backends; // TODO: Support webgpu when more widely supported let supported_backends = Backends::VULKAN | Backends::METAL | Backends::DX12 | Backends::GL; - let mut painter = Painter::new( - egui_wgpu::WgpuConfiguration { - supported_backends, - present_mode: wgpu::PresentMode::AutoVsync, - desired_maximum_frame_latency: Some(2), - ..Default::default() - }, - 1, - None, - false, - ); // The window must be ready with a non-zero size before `Painter::set_window` is called, // otherwise the wgpu surface won't be configured correctly. Self::wait_for_window(&window).await; - painter.set_window(ViewportId::ROOT, Some(window)).await?; + + let wgpu_cfg = egui_wgpu::WgpuConfiguration { + supported_backends, + present_mode: wgpu::PresentMode::AutoVsync, + ..Default::default() + }; + let mut painter = Painter::new(wgpu_cfg.clone(), 1, None, false); + + // Creating device may fail if adapter doesn't support our requested cfg above, so try to + // recover with lower limits. Specifically max_texture_dimension_2d has a downlevel default + // of 2048. egui_wgpu wants 8192 for 4k displays, but not all platforms support that yet. + if let Err(err) = painter + .set_window(ViewportId::ROOT, Some(Arc::clone(&window))) + .await + { + if let egui_wgpu::WgpuError::RequestDeviceError(_) = err { + painter = Painter::new( + egui_wgpu::WgpuConfiguration { + device_descriptor: Arc::new(|adapter| { + let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { + wgpu::Limits::downlevel_webgl2_defaults() + } else { + wgpu::Limits::default() + }; + wgpu::DeviceDescriptor { + label: Some("egui wgpu device"), + required_features: wgpu::Features::default(), + required_limits: wgpu::Limits { + max_texture_dimension_2d: 4096, + ..base_limits + }, + } + }), + ..wgpu_cfg + }, + 1, + None, + false, + ); + painter.set_window(ViewportId::ROOT, Some(window)).await?; + } else { + return Err(err.into()); + } + } let adapter_info = painter.render_state().map(|state| state.adapter.get_info()); if let Some(info) = adapter_info {