1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-27 07:03:14 -04:00

eframe error handling (#2433)

* eframe::run_native: return errors instead of crashing

* Detect and handle glutin errors

* egui_demo_app: silence wgpu log spam

* Add trace logs for why eframe is shutting down

* Fix: only save App state once on Mac

* Handle Winit failure

* Log where we load app state from

* Don't panic on zero-sized window

* Clamp loaded window size to not be too tiny to see

* Simplify code: more shared code in window_builder

* Improve code readability

* Fix wasm32 build

* fix android

* Update changelog
This commit is contained in:
Emil Ernerfeldt
2022-12-12 15:16:32 +01:00
committed by GitHub
parent 1437ec8903
commit cb77458f70
27 changed files with 249 additions and 162 deletions

View File

@@ -708,6 +708,7 @@ impl Frame {
#[doc(alias = "exit")]
#[doc(alias = "quit")]
pub fn close(&mut self) {
tracing::debug!("eframe::Frame::close called");
self.output.close = true;
}

View File

@@ -127,7 +127,7 @@ pub async fn start_web(
canvas_id: &str,
web_options: WebOptions,
app_creator: AppCreator,
) -> Result<AppRunnerRef, wasm_bindgen::JsValue> {
) -> std::result::Result<AppRunnerRef, wasm_bindgen::JsValue> {
let handle = web::start(canvas_id, web_options, app_creator).await?;
Ok(handle)
@@ -174,9 +174,16 @@ mod native;
/// }
/// }
/// ```
///
/// # Errors
/// This function can fail if we fail to set up a graphics context.
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::needless_pass_by_value)]
pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: AppCreator) {
pub fn run_native(
app_name: &str,
native_options: NativeOptions,
app_creator: AppCreator,
) -> Result<()> {
let renderer = native_options.renderer;
#[cfg(not(feature = "__screenshot"))]
@@ -189,17 +196,41 @@ pub fn run_native(app_name: &str, native_options: NativeOptions, app_creator: Ap
#[cfg(feature = "glow")]
Renderer::Glow => {
tracing::debug!("Using the glow renderer");
native::run::run_glow(app_name, native_options, app_creator);
native::run::run_glow(app_name, native_options, app_creator)
}
#[cfg(feature = "wgpu")]
Renderer::Wgpu => {
tracing::debug!("Using the wgpu renderer");
native::run::run_wgpu(app_name, native_options, app_creator);
native::run::run_wgpu(app_name, native_options, app_creator)
}
}
}
// ----------------------------------------------------------------------------
/// The different problems that can occur when trying to run `eframe`.
#[derive(thiserror::Error, Debug)]
pub enum EframeError {
#[cfg(not(target_arch = "wasm32"))]
#[error("winit error: {0}")]
Winit(#[from] winit::error::OsError),
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
#[error("glutin error: {0}")]
Glutin(#[from] glutin::error::Error),
#[cfg(all(feature = "glow", not(target_arch = "wasm32")))]
#[error("Found no glutin configs matching the template: {0:?}")]
NoGlutinConfigs(glutin::config::ConfigTemplate),
#[cfg(feature = "wgpu")]
#[error("WGPU error: {0}")]
Wgpu(#[from] wgpu::RequestDeviceError),
}
pub type Result<T> = std::result::Result<T, EframeError>;
// ---------------------------------------------------------------------------
/// Profiling macro for feature "puffin"

View File

@@ -49,6 +49,7 @@ pub fn read_window_info(window: &winit::window::Window, pixels_per_point: f32) -
}
pub fn window_builder(
title: &str,
native_options: &epi::NativeOptions,
window_settings: &Option<WindowSettings>,
) -> winit::window::WindowBuilder {
@@ -73,13 +74,17 @@ pub fn window_builder(
let window_icon = icon_data.clone().and_then(load_icon);
let mut window_builder = winit::window::WindowBuilder::new()
.with_title(title)
.with_always_on_top(*always_on_top)
.with_decorations(*decorated)
.with_fullscreen(fullscreen.then(|| winit::window::Fullscreen::Borderless(None)))
.with_maximized(*maximized)
.with_resizable(*resizable)
.with_transparent(*transparent)
.with_window_icon(window_icon);
.with_window_icon(window_icon)
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false);
#[cfg(target_os = "macos")]
if *fullsize_content {
@@ -308,8 +313,15 @@ impl EpiIntegration {
use winit::event::{ElementState, MouseButton, WindowEvent};
match event {
WindowEvent::CloseRequested => self.close = app.on_close_event(),
WindowEvent::Destroyed => self.close = true,
WindowEvent::CloseRequested => {
tracing::debug!("Received WindowEvent::CloseRequested");
self.close = app.on_close_event();
tracing::debug!("App::on_close_event returned {}", self.close);
}
WindowEvent::Destroyed => {
tracing::debug!("Received WindowEvent::Destroyed");
self.close = true;
}
WindowEvent::MouseInput {
button: MouseButton::Left,
state: ElementState::Pressed,
@@ -351,6 +363,7 @@ impl EpiIntegration {
self.can_drag_window = false;
if app_output.close {
self.close = app.on_close_event();
tracing::debug!("App::on_close_event returned {}", self.close);
}
self.frame.output.visible = app_output.visible; // this is handled by post_present
handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
@@ -432,7 +445,9 @@ const STORAGE_WINDOW_KEY: &str = "window";
pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
#[cfg(feature = "persistence")]
{
epi::get_value(_storage?, STORAGE_WINDOW_KEY)
let mut settings: WindowSettings = epi::get_value(_storage?, STORAGE_WINDOW_KEY)?;
settings.clamp_to_sane_values();
Some(settings)
}
#[cfg(not(feature = "persistence"))]
None

View File

@@ -26,6 +26,7 @@ impl FileStorage {
/// Store the state in this .ron file.
pub fn from_ron_filepath(ron_filepath: impl Into<PathBuf>) -> Self {
let ron_filepath: PathBuf = ron_filepath.into();
tracing::debug!("Loading app state from {:?}…", ron_filepath);
Self {
kv: read_ron(&ron_filepath).unwrap_or_default(),
ron_filepath,

View File

@@ -1,18 +1,21 @@
//! Note that this file contains two similar paths - one for [`glow`], one for [`wgpu`].
//! When making changes to one you often also want to apply it to the other.
use std::time::Duration;
use std::time::Instant;
use std::time::{Duration, Instant};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::winit;
use winit::event_loop::{
ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget,
};
#[cfg(feature = "accesskit")]
use egui_winit::accesskit_winit;
use egui_winit::winit;
use crate::{epi, Result};
use super::epi_integration::{self, EpiIntegration};
use crate::epi;
// ----------------------------------------------------------------------------
#[derive(Debug)]
pub enum UserEvent {
@@ -60,7 +63,7 @@ trait WinitApp {
&mut self,
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> EventResult;
) -> Result<EventResult>;
}
fn create_event_loop_builder(
@@ -79,10 +82,10 @@ fn create_event_loop_builder(
///
/// We reuse the event-loop so we can support closing and opening an eframe window
/// multiple times. This is just a limitation of winit.
fn with_event_loop(
fn with_event_loop<R>(
mut native_options: epi::NativeOptions,
f: impl FnOnce(&mut EventLoop<UserEvent>, NativeOptions),
) {
f: impl FnOnce(&mut EventLoop<UserEvent>, NativeOptions) -> R,
) -> R {
use std::cell::RefCell;
thread_local!(static EVENT_LOOP: RefCell<Option<EventLoop<UserEvent>>> = RefCell::new(None));
@@ -93,22 +96,31 @@ fn with_event_loop(
let mut event_loop = event_loop.borrow_mut();
let event_loop = event_loop
.get_or_insert_with(|| create_event_loop_builder(&mut native_options).build());
f(event_loop, native_options);
});
f(event_loop, native_options)
})
}
fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl WinitApp) {
fn run_and_return(
event_loop: &mut EventLoop<UserEvent>,
mut winit_app: impl WinitApp,
) -> Result<()> {
use winit::platform::run_return::EventLoopExtRunReturn as _;
tracing::debug!("event_loop.run_return");
tracing::debug!("Entering the winit event loop (run_return)…");
let mut next_repaint_time = Instant::now();
let mut returned_result = Ok(());
event_loop.run_return(|event, event_loop, control_flow| {
let event_result = match &event {
winit::event::Event::LoopDestroyed => {
tracing::debug!("winit::event::Event::LoopDestroyed");
EventResult::Exit
// On Mac, Cmd-Q we get here and then `run_return` doesn't return (despite its name),
// so we need to save state now:
tracing::debug!("Received Event::LoopDestroyed - saving app state…");
winit_app.save_and_destroy();
*control_flow = ControlFlow::Exit;
return;
}
// Platform-dependent event handlers to workaround a winit bug
@@ -137,7 +149,14 @@ fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl Win
EventResult::Wait
}
event => winit_app.on_event(event_loop, event),
event => match winit_app.on_event(event_loop, event) {
Ok(event_result) => event_result,
Err(err) => {
returned_result = Err(err);
tracing::debug!("Exiting because of an error");
EventResult::Exit
}
},
};
match event_result {
@@ -155,10 +174,7 @@ fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl Win
next_repaint_time = next_repaint_time.min(repaint_time);
}
EventResult::Exit => {
// On Cmd-Q we get here and then `run_return` doesn't return,
// so we need to save state now:
tracing::debug!("Exiting event loop - saving app state…");
winit_app.save_and_destroy();
tracing::debug!("Asking to exit event loop…");
*control_flow = ControlFlow::Exit;
return;
}
@@ -187,16 +203,21 @@ fn run_and_return(event_loop: &mut EventLoop<UserEvent>, mut winit_app: impl Win
event_loop.run_return(|_, _, control_flow| {
control_flow.set_exit();
});
returned_result
}
fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp + 'static) -> ! {
tracing::debug!("event_loop.run");
tracing::debug!("Entering the winit event loop (run)…");
let mut next_repaint_time = Instant::now();
event_loop.run(move |event, event_loop, control_flow| {
let event_result = match event {
winit::event::Event::LoopDestroyed => EventResult::Exit,
winit::event::Event::LoopDestroyed => {
tracing::debug!("Received Event::LoopDestroyed");
EventResult::Exit
}
// Platform-dependent event handlers to workaround a winit bug
// See: https://github.com/rust-windowing/winit/issues/987
@@ -215,7 +236,12 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
..
}) => EventResult::RepaintNext,
event => winit_app.on_event(event_loop, &event),
event => match winit_app.on_event(event_loop, &event) {
Ok(event_result) => event_result,
Err(err) => {
panic!("eframe encountered a fatal error: {err}");
}
},
};
match event_result {
@@ -231,7 +257,7 @@ fn run_and_exit(event_loop: EventLoop<UserEvent>, mut winit_app: impl WinitApp +
next_repaint_time = next_repaint_time.min(repaint_time);
}
EventResult::Exit => {
tracing::debug!("Quitting…");
tracing::debug!("Quitting - saving app state");
winit_app.save_and_destroy();
#[allow(clippy::exit)]
std::process::exit(0);
@@ -279,6 +305,8 @@ fn center_window_pos(
mod glow_integration {
use std::sync::Arc;
use egui::NumExt as _;
use super::*;
// Note: that the current Glutin API design tightly couples the GL context with
@@ -321,7 +349,7 @@ mod glow_integration {
unsafe fn new(
winit_window: winit::window::Window,
native_options: &epi::NativeOptions,
) -> Self {
) -> Result<Self> {
use glutin::prelude::*;
use raw_window_handle::*;
let hardware_acceleration = match native_options.hardware_acceleration {
@@ -351,8 +379,7 @@ mod glow_integration {
#[cfg(target_os = "android")]
let preference = glutin::display::DisplayApiPreference::Egl;
let gl_display = glutin::display::Display::new(raw_display_handle, preference)
.expect("failed to create glutin display");
let gl_display = glutin::display::Display::new(raw_display_handle, preference)?;
let swap_interval = if native_options.vsync {
glutin::surface::SwapInterval::Wait(std::num::NonZeroU32::new(1).unwrap())
} else {
@@ -383,64 +410,49 @@ mod glow_integration {
// options required by user like multi sampling, srgb, transparency etc..
// TODO: need to figure out a good fallback config template
let config = gl_display
.find_configs(config_template)
.expect("failed to find even a single matching configuration")
.find_configs(config_template.clone())?
.next()
.expect("failed to find a matching configuration for creating opengl context");
.ok_or(crate::EframeError::NoGlutinConfigs(config_template))?;
let context_attributes =
glutin::context::ContextAttributesBuilder::new().build(Some(raw_window_handle));
// for surface creation.
let (width, height): (u32, u32) = winit_window.inner_size().into();
let width = std::num::NonZeroU32::new(width.at_least(1)).unwrap();
let height = std::num::NonZeroU32::new(height.at_least(1)).unwrap();
let surface_attributes =
glutin::surface::SurfaceAttributesBuilder::<glutin::surface::WindowSurface>::new()
.build(
raw_window_handle,
std::num::NonZeroU32::new(width).unwrap(),
std::num::NonZeroU32::new(height).unwrap(),
);
.build(raw_window_handle, width, height);
// start creating the gl objects
let gl_context = gl_display
.create_context(&config, &context_attributes)
.expect("failed to create opengl context");
let gl_context = gl_display.create_context(&config, &context_attributes)?;
let gl_surface = gl_display
.create_window_surface(&config, &surface_attributes)
.expect("failed to create glutin window surface");
let gl_context = gl_context
.make_current(&gl_surface)
.expect("failed to make gl context current");
gl_surface
.set_swap_interval(&gl_context, swap_interval)
.expect("failed to set vsync swap interval");
GlutinWindowContext {
let gl_surface = gl_display.create_window_surface(&config, &surface_attributes)?;
let gl_context = gl_context.make_current(&gl_surface)?;
gl_surface.set_swap_interval(&gl_context, swap_interval)?;
Ok(GlutinWindowContext {
window: winit_window,
gl_context,
gl_display,
gl_surface,
}
})
}
fn window(&self) -> &winit::window::Window {
&self.window
}
fn resize(&self, physical_size: winit::dpi::PhysicalSize<u32>) {
use glutin::surface::GlSurface;
self.gl_surface.resize(
&self.gl_context,
physical_size
.width
.try_into()
.expect("physical size must not be zero"),
physical_size
.height
.try_into()
.expect("physical size must not be zero"),
);
let width = std::num::NonZeroU32::new(physical_size.width.at_least(1)).unwrap();
let height = std::num::NonZeroU32::new(physical_size.height.at_least(1)).unwrap();
self.gl_surface.resize(&self.gl_context, width, height);
}
fn swap_buffers(&self) -> glutin::error::Result<()> {
use glutin::surface::GlSurface;
self.gl_surface.swap_buffers(&self.gl_context)
}
fn get_proc_address(&self, addr: &std::ffi::CStr) -> *const std::ffi::c_void {
use glutin::display::GlDisplay;
self.gl_display.get_proc_address(addr)
@@ -484,25 +496,19 @@ mod glow_integration {
fn create_glutin_windowed_context(
event_loop: &EventLoopWindowTarget<UserEvent>,
storage: Option<&dyn epi::Storage>,
title: &String,
title: &str,
native_options: &NativeOptions,
) -> (GlutinWindowContext, glow::Context) {
) -> Result<(GlutinWindowContext, glow::Context)> {
crate::profile_function!();
let window_settings = epi_integration::load_window_settings(storage);
let window_builder = epi_integration::window_builder(native_options, &window_settings)
.with_title(title)
.with_transparent(native_options.transparent)
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false);
let winit_window = window_builder
.build(event_loop)
.expect("failed to create winit window");
let winit_window =
epi_integration::window_builder(title, native_options, &window_settings)
.build(event_loop)?;
// a lot of the code below has been lifted from glutin example in their repo.
let glutin_window_context =
unsafe { GlutinWindowContext::new(winit_window, native_options) };
unsafe { GlutinWindowContext::new(winit_window, native_options)? };
let gl = unsafe {
glow::Context::from_loader_function(|s| {
let s = std::ffi::CString::new(s)
@@ -512,10 +518,10 @@ mod glow_integration {
})
};
(glutin_window_context, gl)
Ok((glutin_window_context, gl))
}
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) {
fn init_run_state(&mut self, event_loop: &EventLoopWindowTarget<UserEvent>) -> Result<()> {
let storage = epi_integration::create_storage(&self.app_name);
let (gl_window, gl) = Self::create_glutin_windowed_context(
@@ -523,7 +529,7 @@ mod glow_integration {
storage.as_deref(),
&self.app_name,
&self.native_options,
);
)?;
let gl = Arc::new(gl);
let painter =
@@ -585,6 +591,8 @@ mod glow_integration {
integration,
app,
});
Ok(())
}
}
@@ -726,11 +734,11 @@ mod glow_integration {
&mut self,
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> EventResult {
match event {
) -> Result<EventResult> {
Ok(match event {
winit::event::Event::Resumed => {
if self.running.is_none() {
self.init_run_state(event_loop);
self.init_run_state(event_loop)?;
}
EventResult::RepaintNow
}
@@ -793,7 +801,8 @@ mod glow_integration {
winit::event::WindowEvent::CloseRequested
if running.integration.should_close() =>
{
return EventResult::Exit
tracing::debug!("Received WindowEvent::CloseRequested");
return Ok(EventResult::Exit);
}
_ => {}
}
@@ -832,7 +841,7 @@ mod glow_integration {
}
}
_ => EventResult::Wait,
}
})
}
}
@@ -840,7 +849,7 @@ mod glow_integration {
app_name: &str,
mut native_options: epi::NativeOptions,
app_creator: epi::AppCreator,
) {
) -> Result<()> {
if native_options.run_and_return {
with_event_loop(native_options, |event_loop, mut native_options| {
if native_options.centered {
@@ -849,8 +858,8 @@ mod glow_integration {
let glow_eframe =
GlowWinitApp::new(event_loop, app_name, native_options, app_creator);
run_and_return(event_loop, glow_eframe);
});
run_and_return(event_loop, glow_eframe)
})
} else {
let event_loop = create_event_loop_builder(&mut native_options).build();
@@ -923,38 +932,40 @@ mod wgpu_integration {
fn create_window(
event_loop: &EventLoopWindowTarget<UserEvent>,
storage: Option<&dyn epi::Storage>,
title: &String,
title: &str,
native_options: &NativeOptions,
) -> winit::window::Window {
) -> Result<winit::window::Window> {
let window_settings = epi_integration::load_window_settings(storage);
epi_integration::window_builder(native_options, &window_settings)
.with_title(title)
// Keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
// We must also keep the window hidden until AccessKit is initialized.
.with_visible(false)
.build(event_loop)
.unwrap()
Ok(
epi_integration::window_builder(title, native_options, &window_settings)
.build(event_loop)?,
)
}
#[allow(unsafe_code)]
fn set_window(&mut self, window: winit::window::Window) {
fn set_window(
&mut self,
window: winit::window::Window,
) -> std::result::Result<(), wgpu::RequestDeviceError> {
self.window = Some(window);
if let Some(running) = &mut self.running {
unsafe {
running.painter.set_window(self.window.as_ref()).unwrap();
running.painter.set_window(self.window.as_ref())?;
}
}
Ok(())
}
#[allow(unsafe_code)]
#[cfg(target_os = "android")]
fn drop_window(&mut self) {
fn drop_window(&mut self) -> std::result::Result<(), wgpu::RequestDeviceError> {
self.window = None;
if let Some(running) = &mut self.running {
unsafe {
running.painter.set_window(None).unwrap();
running.painter.set_window(None)?;
}
}
Ok(())
}
fn init_run_state(
@@ -962,7 +973,7 @@ mod wgpu_integration {
event_loop: &EventLoopWindowTarget<UserEvent>,
storage: Option<Box<dyn epi::Storage>>,
window: winit::window::Window,
) {
) -> std::result::Result<(), wgpu::RequestDeviceError> {
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let painter = unsafe {
let mut painter = egui_wgpu::winit::Painter::new(
@@ -970,7 +981,7 @@ mod wgpu_integration {
self.native_options.multisampling.max(1) as _,
self.native_options.depth_buffer,
);
painter.set_window(Some(&window)).unwrap();
painter.set_window(Some(&window))?;
painter
};
@@ -1028,6 +1039,8 @@ mod wgpu_integration {
app,
});
self.window = Some(window);
Ok(())
}
}
@@ -1135,8 +1148,8 @@ mod wgpu_integration {
&mut self,
event_loop: &EventLoopWindowTarget<UserEvent>,
event: &winit::event::Event<'_, UserEvent>,
) -> EventResult {
match event {
) -> Result<EventResult> {
Ok(match event {
winit::event::Event::Resumed => {
if let Some(running) = &self.running {
if self.window.is_none() {
@@ -1145,8 +1158,8 @@ mod wgpu_integration {
running.integration.frame.storage(),
&self.app_name,
&self.native_options,
);
self.set_window(window);
)?;
self.set_window(window)?;
}
} else {
let storage = epi_integration::create_storage(&self.app_name);
@@ -1155,14 +1168,14 @@ mod wgpu_integration {
storage.as_deref(),
&self.app_name,
&self.native_options,
);
self.init_run_state(event_loop, storage, window);
)?;
self.init_run_state(event_loop, storage, window)?;
}
EventResult::RepaintNow
}
winit::event::Event::Suspended => {
#[cfg(target_os = "android")]
self.drop_window();
self.drop_window()?;
EventResult::Wait
}
@@ -1212,7 +1225,8 @@ mod wgpu_integration {
winit::event::WindowEvent::CloseRequested
if running.integration.should_close() =>
{
return EventResult::Exit
tracing::debug!("Received WindowEvent::CloseRequested");
return Ok(EventResult::Exit);
}
_ => {}
};
@@ -1250,7 +1264,7 @@ mod wgpu_integration {
}
}
_ => EventResult::Wait,
}
})
}
}
@@ -1258,7 +1272,7 @@ mod wgpu_integration {
app_name: &str,
mut native_options: epi::NativeOptions,
app_creator: epi::AppCreator,
) {
) -> Result<()> {
if native_options.run_and_return {
with_event_loop(native_options, |event_loop, mut native_options| {
if native_options.centered {
@@ -1267,8 +1281,8 @@ mod wgpu_integration {
let wgpu_eframe =
WgpuWinitApp::new(event_loop, app_name, native_options, app_creator);
run_and_return(event_loop, wgpu_eframe);
});
run_and_return(event_loop, wgpu_eframe)
})
} else {
let event_loop = create_event_loop_builder(&mut native_options).build();