1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 13:20:05 -04:00

Update to wgpu 29

This commit is contained in:
Connor Fitzgerald
2026-03-20 12:45:14 -04:00
parent 8b2315375b
commit f3b938bbcb
13 changed files with 498 additions and 192 deletions

View File

@@ -24,7 +24,10 @@ mod renderer;
mod setup;
pub use renderer::*;
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
pub use setup::{
EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew,
WgpuSetupExisting,
};
/// Helpers for capturing screenshots of the UI.
#[cfg(feature = "capture")]
@@ -191,6 +194,7 @@ impl RenderState {
let (adapter, device, queue) = match config.wgpu_setup.clone() {
WgpuSetup::CreateNew(WgpuSetupCreateNew {
instance_descriptor: _,
display_handle: _,
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
@@ -272,7 +276,58 @@ fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
}
}
/// Specifies which action should be taken as consequence of a [`wgpu::SurfaceError`]
/// Describes a surface error when acquiring a texture for rendering.
///
/// These correspond to the error variants of [`wgpu::CurrentSurfaceTexture`] — everything
/// except `Success` and `Suboptimal`, which contain usable frames. This enum is passed to the
/// [`WgpuConfiguration::on_surface_error`] callback so you can decide how to respond.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurfaceStatus {
/// Timed out waiting for the next frame.
///
/// This is usually transient. Skip the current frame and try again next frame.
Timeout,
/// The surface configuration no longer matches the underlying display surface.
///
/// The surface should be reconfigured (egui will do this automatically if you return
/// [`SurfaceErrorAction::RecreateSurface`]). This commonly happens after a window resize
/// or display settings change.
Outdated,
/// The surface has been lost and must be recreated from scratch.
///
/// This is more severe than [`Self::Outdated`] — the entire surface is invalid. Return
/// [`SurfaceErrorAction::RecreateSurface`] to recover.
Lost,
/// The window is not visible (e.g. minimized or fully behind another window).
///
/// Skip the current frame. There is nothing to render to.
Occluded,
/// A GPU validation error occurred.
///
/// This should not be reachable under normal circumstances, unless
/// you wrap the [`winit::Painter::paint_and_update_textures`] in a wgpu
/// error scope, and there is a validation error in the
/// [`wgpu::Surface::get_current_texture`] call.
Validation,
}
impl std::fmt::Display for SurfaceStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Timeout => write!(f, "Surface timed out"),
Self::Outdated => write!(f, "Surface outdated"),
Self::Lost => write!(f, "Surface lost"),
Self::Occluded => write!(f, "Surface occluded"),
Self::Validation => write!(f, "Surface validation error"),
}
}
}
/// Specifies which action should be taken as consequence of a [`SurfaceStatus`]
pub enum SurfaceErrorAction {
/// Do nothing and skip the current frame.
SkipFrame,
@@ -300,7 +355,7 @@ pub struct WgpuConfiguration {
pub wgpu_setup: WgpuSetup,
/// Callback for surface errors.
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction + Send + Sync>,
pub on_surface_error: Arc<dyn Fn(SurfaceStatus) -> SurfaceErrorAction + Send + Sync>,
}
#[test]
@@ -333,9 +388,11 @@ impl Default for WgpuConfiguration {
Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: None,
wgpu_setup: Default::default(),
// No display handle available at this point — callers should replace this with
// `WgpuSetup::from_display_handle(...)` before creating the instance if one is available.
wgpu_setup: WgpuSetup::without_display_handle(),
on_surface_error: Arc::new(|err| {
if err == wgpu::SurfaceError::Outdated {
if err == SurfaceStatus::Outdated {
// This error occurs when the app is minimized on Windows.
// Silently return here to prevent spamming the console with:
// "The underlying surface has changed, and therefore the swap chain must be updated"

View File

@@ -352,7 +352,10 @@ impl Renderer {
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("egui_pipeline_layout"),
bind_group_layouts: &[&uniform_bind_group_layout, &texture_bind_group_layout],
bind_group_layouts: &[
Some(&uniform_bind_group_layout),
Some(&texture_bind_group_layout),
],
immediate_size: 0,
});
@@ -360,8 +363,8 @@ impl Renderer {
.depth_stencil_format
.map(|format| wgpu::DepthStencilState {
format,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
depth_write_enabled: Some(false),
depth_compare: Some(wgpu::CompareFunction::Always),
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
});
@@ -968,7 +971,8 @@ impl Renderer {
Primitive::Mesh(mesh) => {
let size = mesh.indices.len() * std::mem::size_of::<u32>();
let slice = index_offset..(size + index_offset);
index_buffer_staging[slice.clone()]
index_buffer_staging
.slice(slice.clone())
.copy_from_slice(bytemuck::cast_slice(&mesh.indices));
self.index_buffer.slices.push(slice);
index_offset += size;
@@ -1011,7 +1015,8 @@ impl Renderer {
Primitive::Mesh(mesh) => {
let size = mesh.vertices.len() * std::mem::size_of::<Vertex>();
let slice = vertex_offset..(size + vertex_offset);
vertex_buffer_staging[slice.clone()]
vertex_buffer_staging
.slice(slice.clone())
.copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
self.vertex_buffer.slices.push(slice);
vertex_offset += size;

View File

@@ -1,5 +1,47 @@
use std::sync::Arc;
/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`].
///
/// This trait exists so that a [`winit::event_loop::OwnedDisplayHandle`] (or similar platform
/// display handle) can be stored, cloned, and later passed to wgpu.
///
/// wgpu requires an explicit display handle for GLES on some platforms (notably Wayland).
/// Because [`wgpu::InstanceDescriptor`] contains a `Box<dyn WgpuHasDisplayHandle>` which is
/// not cloneable, we wrap the handle in this trait so it can be cloned alongside the rest of
/// the egui wgpu configuration.
///
/// This is automatically implemented for all types that satisfy the bounds (including
/// [`winit::event_loop::OwnedDisplayHandle`]).
pub trait EguiDisplayHandle:
wgpu::rwh::HasDisplayHandle + std::fmt::Debug + Send + Sync + 'static
{
/// Clone this handle into a `Box<dyn WgpuHasDisplayHandle>` suitable for setting on
/// [`wgpu::InstanceDescriptor::display`].
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
/// Clone this handle into a new `Box<dyn EguiDisplayHandle>`.
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle>;
}
impl Clone for Box<dyn EguiDisplayHandle> {
fn clone(&self) -> Self {
self.clone_display_handle()
}
}
impl<T> EguiDisplayHandle for T
where
T: wgpu::rwh::HasDisplayHandle + Clone + std::fmt::Debug + Send + Sync + 'static,
{
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle> {
Box::new(self.clone())
}
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle> {
Box::new(self.clone())
}
}
#[derive(Clone)]
pub enum WgpuSetup {
/// Construct a wgpu setup using some predefined settings & heuristics.
@@ -22,9 +64,32 @@ pub enum WgpuSetup {
Existing(WgpuSetupExisting),
}
impl Default for WgpuSetup {
fn default() -> Self {
Self::CreateNew(WgpuSetupCreateNew::default())
impl WgpuSetup {
/// Creates a new [`WgpuSetup::CreateNew`] with the given display handle.
///
/// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web)
/// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require
/// one. Providing it unconditionally ensures your app works everywhere.
///
/// If you don't have a display handle available, use [`Self::without_display_handle`]
/// instead — it will still work on the majority of platforms.
///
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
Self::CreateNew(WgpuSetupCreateNew::from_display_handle(display_handle))
}
/// Creates a new [`WgpuSetup::CreateNew`] without a display handle.
///
/// A display handle is not required for headless operation (offscreen rendering, tests,
/// compute-only workloads). It also isn't needed on most platforms even when presenting
/// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one.
///
/// If you do have a display handle available, prefer [`Self::from_display_handle`] for
/// maximum compatibility. With winit you can obtain one via
/// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
pub fn without_display_handle() -> Self {
Self::CreateNew(WgpuSetupCreateNew::without_display_handle())
}
}
@@ -65,8 +130,18 @@ impl WgpuSetup {
}
log::debug!("Creating wgpu instance with backends {backends:?}");
wgpu::util::new_instance_with_webgpu_detection(&create_new.instance_descriptor)
.await
let desc = &create_new.instance_descriptor;
let mut descriptor = wgpu::InstanceDescriptor {
backends: desc.backends,
flags: desc.flags,
backend_options: desc.backend_options.clone(),
memory_budget_thresholds: desc.memory_budget_thresholds,
display: None,
};
if let Some(handle) = &create_new.display_handle {
descriptor.display = Some(handle.clone_for_wgpu());
}
wgpu::util::new_instance_with_webgpu_detection(descriptor).await
}
Self::Existing(existing) => existing.instance.clone(),
}
@@ -98,9 +173,28 @@ pub type NativeAdapterSelectorMethod = Arc<
/// Configuration for creating a new wgpu setup.
///
/// Used for [`WgpuSetup::CreateNew`].
///
/// Use [`Self::from_display_handle`] when you have a display handle available — this is the
/// recommended constructor. With winit you can obtain one via
/// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
/// Most platforms (Windows, macOS/iOS, Android, web) work fine without one, but some
/// (e.g. Wayland on Linux with GLES) require it. Providing it unconditionally ensures your
/// app works everywhere.
///
/// If you don't have a display handle, use [`Self::without_display_handle`] — it will still
/// work on the majority of platforms, and is appropriate for headless rendering, tests, or
/// web targets.
///
/// Note: The [`wgpu::InstanceDescriptor::display`] field is always stored as `None` in
/// [`Self::instance_descriptor`]. The display handle is stored separately so it can be cloned
/// (since [`wgpu::InstanceDescriptor`] itself does not implement `Clone`), and is injected
/// into the descriptor at instance creation time.
pub struct WgpuSetupCreateNew {
/// Instance descriptor for creating a wgpu instance.
///
/// The [`wgpu::InstanceDescriptor::display`] field should be left as `None`; use the
/// [`Self::display_handle`] field instead (it will be injected when the instance is created).
///
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which
/// controls which backends are supported (wgpu will pick one of these).
/// If you only want to support WebGL (and not WebGPU),
@@ -110,6 +204,16 @@ pub struct WgpuSetupCreateNew {
/// and only if you have enabled the `webgl` feature of crate `wgpu`.
pub instance_descriptor: wgpu::InstanceDescriptor,
/// The display handle to pass to wgpu when creating the instance.
///
/// Most platforms (Windows, macOS/iOS, Android, web) work without this, but some
/// (e.g. Wayland on Linux with GLES) require it. If you have a display handle
/// available, providing it ensures maximum compatibility.
///
/// When using winit, this is typically the
/// [`winit::event_loop::OwnedDisplayHandle`] obtained from the event loop.
pub display_handle: Option<Box<dyn EguiDisplayHandle>>,
/// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web.
pub power_preference: wgpu::PowerPreference,
@@ -128,32 +232,34 @@ pub struct WgpuSetupCreateNew {
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
}
impl Clone for WgpuSetupCreateNew {
fn clone(&self) -> Self {
impl WgpuSetupCreateNew {
/// Creates a new configuration with the given display handle.
///
/// This is the recommended constructor. Most platforms (Windows, macOS/iOS, Android, web)
/// work fine without a display handle, but some (e.g. Wayland on Linux with GLES) require
/// one. Providing it unconditionally ensures your app works everywhere.
///
/// If you don't have a display handle available, use [`Self::without_display_handle`]
/// instead — it will still work on the majority of platforms.
///
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
pub fn from_display_handle(display_handle: impl EguiDisplayHandle) -> Self {
Self {
instance_descriptor: self.instance_descriptor.clone(),
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: Arc::clone(&self.device_descriptor),
display_handle: Some(Box::new(display_handle)),
..Self::without_display_handle()
}
}
}
impl std::fmt::Debug for WgpuSetupCreateNew {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WgpuSetupCreateNew")
.field("instance_descriptor", &self.instance_descriptor)
.field("power_preference", &self.power_preference)
.field(
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
)
.finish()
}
}
impl Default for WgpuSetupCreateNew {
fn default() -> Self {
/// Creates a new configuration without a display handle.
///
/// A display handle is not required for headless operation (offscreen rendering, tests,
/// compute-only workloads). It also isn't needed on most platforms even when presenting
/// to a window — only some configurations (e.g. Wayland on Linux with GLES) require one.
///
/// If you do have a display handle available, prefer [`Self::from_display_handle`] for
/// maximum compatibility. With winit you can obtain one via
/// [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
pub fn without_display_handle() -> Self {
Self {
instance_descriptor: wgpu::InstanceDescriptor {
// Add GL backend, primarily because WebGPU is not stable enough yet.
@@ -163,8 +269,11 @@ impl Default for WgpuSetupCreateNew {
flags: wgpu::InstanceFlags::from_build_config().with_env(),
backend_options: wgpu::BackendOptions::from_env_or_default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
display: None,
},
display_handle: None,
power_preference: wgpu::PowerPreference::from_env()
.unwrap_or(wgpu::PowerPreference::HighPerformance),
@@ -192,6 +301,39 @@ impl Default for WgpuSetupCreateNew {
}
}
impl Clone for WgpuSetupCreateNew {
fn clone(&self) -> Self {
let desc = &self.instance_descriptor;
Self {
instance_descriptor: wgpu::InstanceDescriptor {
backends: desc.backends,
flags: desc.flags,
backend_options: desc.backend_options.clone(),
memory_budget_thresholds: desc.memory_budget_thresholds,
display: None,
},
display_handle: self.display_handle.clone(),
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: Arc::clone(&self.device_descriptor),
}
}
}
impl std::fmt::Debug for WgpuSetupCreateNew {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WgpuSetupCreateNew")
.field("instance_descriptor", &self.instance_descriptor)
.field("display_handle", &self.display_handle)
.field("power_preference", &self.power_preference)
.field(
"native_adapter_selector",
&self.native_adapter_selector.is_some(),
)
.finish()
}
}
/// Configuration for using an existing wgpu setup.
///
/// Used for [`WgpuSetup::Existing`].

View File

@@ -3,7 +3,7 @@
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
#![expect(unsafe_code)]
use crate::{RenderState, SurfaceErrorAction, WgpuConfiguration, renderer};
use crate::{RenderState, SurfaceErrorAction, SurfaceStatus, WgpuConfiguration, renderer};
use crate::{
RendererOptions,
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
@@ -368,7 +368,7 @@ impl Painter {
hal_surface
.render_layer()
.lock()
.set_presents_with_transaction(resizing);
.setPresentsWithTransaction(resizing);
Self::configure_surface(
state,
@@ -501,16 +501,53 @@ impl Painter {
};
let output_frame = match output_frame {
Ok(frame) => frame,
Err(err) => match (*self.configuration.on_surface_error)(err) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
return vsync_sec;
wgpu::CurrentSurfaceTexture::Success(frame)
| wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
wgpu::CurrentSurfaceTexture::Timeout => {
match (*self.configuration.on_surface_error)(SurfaceStatus::Timeout) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
}
SurfaceErrorAction::SkipFrame => {}
}
SurfaceErrorAction::SkipFrame => {
return vsync_sec;
return vsync_sec;
}
wgpu::CurrentSurfaceTexture::Outdated => {
match (*self.configuration.on_surface_error)(SurfaceStatus::Outdated) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
}
SurfaceErrorAction::SkipFrame => {}
}
},
return vsync_sec;
}
wgpu::CurrentSurfaceTexture::Lost => {
match (*self.configuration.on_surface_error)(SurfaceStatus::Lost) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
}
SurfaceErrorAction::SkipFrame => {}
}
return vsync_sec;
}
wgpu::CurrentSurfaceTexture::Occluded => {
match (*self.configuration.on_surface_error)(SurfaceStatus::Occluded) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
}
SurfaceErrorAction::SkipFrame => {}
}
return vsync_sec;
}
wgpu::CurrentSurfaceTexture::Validation => {
match (*self.configuration.on_surface_error)(SurfaceStatus::Validation) {
SurfaceErrorAction::RecreateSurface => {
Self::configure_surface(surface_state, render_state, &self.configuration);
}
SurfaceErrorAction::SkipFrame => {}
}
return vsync_sec;
}
};
let mut capture_buffer = None;