Add option to initialize on existing wgpu setup (#5319)

When mixing and matching eframe with other wgpu applications
(https://github.com/tracel-ai/burn in my case), it can be helpful to use
an existing wgpu setup to initialize eframe with. This PR changes the
WpuConfiguration (in a non-backwards compat way :/), to either take some
options how to create a wgpu setup, or an existing wgpu setup
(consisting of an instance, adapter, device and queue).

* [x] I have followed the instructions in the PR template

---------

Co-authored-by: Andreas Reich <r_andreas2@web.de>
This commit is contained in:
Arthur Brussee 2024-10-29 16:12:28 +00:00 committed by GitHub
parent fba2dc85a3
commit 759a0b2a21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 269 additions and 190 deletions

View File

@ -1,15 +1,13 @@
use std::sync::Arc;
use raw_window_handle::{ use raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle, DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, WebDisplayHandle, WebWindowHandle, WindowHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle, WindowHandle,
}; };
use std::sync::Arc;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement; use web_sys::HtmlCanvasElement;
use egui_wgpu::{RenderState, SurfaceErrorAction};
use crate::WebOptions; use crate::WebOptions;
use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup};
use super::web_painter::WebPainter; use super::web_painter::WebPainter;
@ -89,11 +87,18 @@ impl WebPainterWgpu {
) -> Result<Self, String> { ) -> Result<Self, String> {
log::debug!("Creating wgpu painter"); log::debug!("Creating wgpu painter");
let mut backends = options.wgpu_options.supported_backends; let instance = match &options.wgpu_options.wgpu_setup {
WgpuSetup::CreateNew {
supported_backends: backends,
power_preference,
..
} => {
let mut backends = *backends;
// Don't try WebGPU if we're not in a secure context. // Don't try WebGPU if we're not in a secure context.
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) { if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
let is_secure_context = web_sys::window().map_or(false, |w| w.is_secure_context()); let is_secure_context =
web_sys::window().map_or(false, |w| w.is_secure_context());
if !is_secure_context { if !is_secure_context {
log::info!( log::info!(
"WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost." "WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost."
@ -136,7 +141,7 @@ impl WebPainterWgpu {
log::debug!("Attempting to create WebGPU adapter to check for support."); log::debug!("Attempting to create WebGPU adapter to check for support.");
if let Some(adapter) = instance if let Some(adapter) = instance
.request_adapter(&wgpu::RequestAdapterOptions { .request_adapter(&wgpu::RequestAdapterOptions {
power_preference: options.wgpu_options.power_preference, power_preference: *power_preference,
compatible_surface: None, compatible_surface: None,
force_fallback_adapter: false, force_fallback_adapter: false,
}) })
@ -159,12 +164,21 @@ impl WebPainterWgpu {
}); });
} else { } else {
return Err( return Err(
"Failed to create WebGPU adapter and WebGL was not enabled.".to_owned() "Failed to create WebGPU adapter and WebGL was not enabled."
.to_owned(),
); );
} }
} }
} }
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
#[allow(clippy::arc_with_non_send_sync)]
Arc::new(instance)
}
WgpuSetup::Existing { instance, .. } => instance.clone(),
};
let surface = instance let surface = instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?; .map_err(|err| format!("failed to create wgpu surface: {err}"))?;

View File

@ -8,8 +8,8 @@
//! wgpu = { version = "*", features = ["webgpu", "webgl"] } //! wgpu = { version = "*", features = ["webgpu", "webgl"] }
//! ``` //! ```
//! //!
//! You can control whether WebGL or WebGPU will be picked at runtime by setting //! You can control whether WebGL or WebGPU will be picked at runtime by configuring
//! [`WgpuConfiguration::supported_backends`]. //! [`WgpuConfiguration::wgpu_setup`].
//! The default is to prefer WebGPU and fall back on WebGL. //! The default is to prefer WebGPU and fall back on WebGL.
//! //!
//! ## Feature flags //! ## Feature flags
@ -24,6 +24,7 @@ pub use wgpu;
mod renderer; mod renderer;
pub use renderer::*; pub use renderer::*;
use wgpu::{Adapter, Device, Instance, Queue};
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`]. /// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")] #[cfg(feature = "winit")]
@ -98,11 +99,17 @@ impl RenderState {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let available_adapters = instance.enumerate_adapters(wgpu::Backends::all()); let available_adapters = instance.enumerate_adapters(wgpu::Backends::all());
let (adapter, device, queue) = match config.wgpu_setup.clone() {
WgpuSetup::CreateNew {
supported_backends: _,
power_preference,
device_descriptor,
} => {
let adapter = { let adapter = {
crate::profile_scope!("request_adapter"); crate::profile_scope!("request_adapter");
instance instance
.request_adapter(&wgpu::RequestAdapterOptions { .request_adapter(&wgpu::RequestAdapterOptions {
power_preference: config.power_preference, power_preference,
compatible_surface: Some(surface), compatible_surface: Some(surface),
force_fallback_adapter: false, force_fallback_adapter: false,
}) })
@ -152,19 +159,32 @@ impl RenderState {
); );
} }
let (device, queue) = {
crate::profile_scope!("request_device");
adapter
.request_device(&(*device_descriptor)(&adapter), None)
.await?
};
// On wasm, depending on feature flags, wgpu objects may or may not implement sync.
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
#[allow(clippy::arc_with_non_send_sync)]
(Arc::new(adapter), Arc::new(device), Arc::new(queue))
}
WgpuSetup::Existing {
instance: _,
adapter,
device,
queue,
} => (adapter, device, queue),
};
let capabilities = { let capabilities = {
crate::profile_scope!("get_capabilities"); crate::profile_scope!("get_capabilities");
surface.get_capabilities(&adapter).formats surface.get_capabilities(&adapter).formats
}; };
let target_format = crate::preferred_framebuffer_format(&capabilities)?; let target_format = crate::preferred_framebuffer_format(&capabilities)?;
let (device, queue) = {
crate::profile_scope!("request_device");
adapter
.request_device(&(*config.device_descriptor)(&adapter), None)
.await?
};
let renderer = Renderer::new( let renderer = Renderer::new(
&device, &device,
target_format, target_format,
@ -177,11 +197,11 @@ impl RenderState {
// It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint. // It doesn't make sense to switch to Rc for that special usecase, so simply disable the lint.
#[allow(clippy::arc_with_non_send_sync)] #[allow(clippy::arc_with_non_send_sync)]
Ok(Self { Ok(Self {
adapter: Arc::new(adapter), adapter,
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
available_adapters: available_adapters.into(), available_adapters: available_adapters.into(),
device: Arc::new(device), device,
queue: Arc::new(queue), queue,
target_format, target_format,
renderer: Arc::new(RwLock::new(renderer)), renderer: Arc::new(RwLock::new(renderer)),
}) })
@ -215,13 +235,16 @@ pub enum SurfaceErrorAction {
RecreateSurface, RecreateSurface,
} }
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature. #[derive(Clone)]
pub enum WgpuSetup {
/// Construct a wgpu setup using some predefined settings & heuristics.
/// This is the default option. You can customize most behaviours overriding the
/// supported backends, power preferences, and device description.
/// ///
/// This can also be configured with the environment variables: /// This can also be configured with the environment variables:
/// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu` /// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu`
/// * `WGPU_POWER_PREF`: `low`, `high` or `none` /// * `WGPU_POWER_PREF`: `low`, `high` or `none`
#[derive(Clone)] CreateNew {
pub struct WgpuConfiguration {
/// Backends that should be supported (wgpu will pick one of these). /// Backends that should be supported (wgpu will pick one of these).
/// ///
/// For instance, if you only want to support WebGL (and not WebGPU), /// For instance, if you only want to support WebGL (and not WebGPU),
@ -230,12 +253,47 @@ pub struct WgpuConfiguration {
/// By default on web, WebGPU will be used if available. /// By default on web, WebGPU will be used if available.
/// WebGL will only be used as a fallback, /// WebGL will only be used as a fallback,
/// and only if you have enabled the `webgl` feature of crate `wgpu`. /// and only if you have enabled the `webgl` feature of crate `wgpu`.
pub supported_backends: wgpu::Backends, supported_backends: wgpu::Backends,
/// Power preference for the adapter.
power_preference: wgpu::PowerPreference,
/// Configuration passed on device request, given an adapter /// Configuration passed on device request, given an adapter
pub device_descriptor: device_descriptor:
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>, Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
},
/// Run on an existing wgpu setup.
Existing {
instance: Arc<Instance>,
adapter: Arc<Adapter>,
device: Arc<Device>,
queue: Arc<Queue>,
},
}
impl std::fmt::Debug for WgpuSetup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CreateNew {
supported_backends,
power_preference,
device_descriptor: _,
} => f
.debug_struct("AdapterSelection::Standard")
.field("supported_backends", &supported_backends)
.field("power_preference", &power_preference)
.finish(),
Self::Existing { .. } => f
.debug_struct("AdapterSelection::Existing")
.finish_non_exhaustive(),
}
}
}
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
#[derive(Clone)]
pub struct WgpuConfiguration {
/// Present mode used for the primary surface. /// Present mode used for the primary surface.
pub present_mode: wgpu::PresentMode, pub present_mode: wgpu::PresentMode,
@ -248,8 +306,8 @@ pub struct WgpuConfiguration {
/// `None` = `wgpu` default. /// `None` = `wgpu` default.
pub desired_maximum_frame_latency: Option<u32>, pub desired_maximum_frame_latency: Option<u32>,
/// Power preference for the adapter. /// How to create the wgpu adapter & device
pub power_preference: wgpu::PowerPreference, pub wgpu_setup: WgpuSetup,
/// Callback for surface errors. /// Callback for surface errors.
pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction + Send + Sync>, pub on_surface_error: Arc<dyn Fn(wgpu::SurfaceError) -> SurfaceErrorAction + Send + Sync>,
@ -264,21 +322,18 @@ fn wgpu_config_impl_send_sync() {
impl std::fmt::Debug for WgpuConfiguration { impl std::fmt::Debug for WgpuConfiguration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { let Self {
supported_backends,
device_descriptor: _,
present_mode, present_mode,
desired_maximum_frame_latency, desired_maximum_frame_latency,
power_preference, wgpu_setup,
on_surface_error: _, on_surface_error: _,
} = self; } = self;
f.debug_struct("WgpuConfiguration") f.debug_struct("WgpuConfiguration")
.field("supported_backends", &supported_backends)
.field("present_mode", &present_mode) .field("present_mode", &present_mode)
.field( .field(
"desired_maximum_frame_latency", "desired_maximum_frame_latency",
&desired_maximum_frame_latency, &desired_maximum_frame_latency,
) )
.field("power_preference", &power_preference) .field("wgpu_setup", &wgpu_setup)
.finish_non_exhaustive() .finish_non_exhaustive()
} }
} }
@ -286,11 +341,22 @@ impl std::fmt::Debug for WgpuConfiguration {
impl Default for WgpuConfiguration { impl Default for WgpuConfiguration {
fn default() -> Self { fn default() -> Self {
Self { Self {
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: None,
// By default, create a new wgpu setup. This will create a new instance, adapter, device and queue.
// This will create an instance for the supported backends (which can be configured by
// `WGPU_BACKEND`), and will pick an adapter by iterating adapters based on their power preference. The power
// preference can also be configured by `WGPU_POWER_PREF`.
wgpu_setup: WgpuSetup::CreateNew {
// Add GL backend, primarily because WebGPU is not stable enough yet. // Add GL backend, primarily because WebGPU is not stable enough yet.
// (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl") // (note however, that the GL backend needs to be opted-in via the wgpu feature flag "webgl")
supported_backends: wgpu::util::backend_bits_from_env() supported_backends: wgpu::util::backend_bits_from_env()
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL), .unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
power_preference: wgpu::util::power_preference_from_env()
.unwrap_or(wgpu::PowerPreference::HighPerformance),
device_descriptor: Arc::new(|adapter| { device_descriptor: Arc::new(|adapter| {
let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl { let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
wgpu::Limits::downlevel_webgl2_defaults() wgpu::Limits::downlevel_webgl2_defaults()
@ -310,13 +376,7 @@ impl Default for WgpuConfiguration {
memory_hints: wgpu::MemoryHints::default(), memory_hints: wgpu::MemoryHints::default(),
} }
}), }),
},
present_mode: wgpu::PresentMode::AutoVsync,
desired_maximum_frame_latency: None,
power_preference: wgpu::util::power_preference_from_env()
.unwrap_or(wgpu::PowerPreference::HighPerformance),
on_surface_error: Arc::new(|err| { on_surface_error: Arc::new(|err| {
if err == wgpu::SurfaceError::Outdated { if err == wgpu::SurfaceError::Outdated {

View File

@ -87,7 +87,7 @@ pub struct Painter {
depth_format: Option<wgpu::TextureFormat>, depth_format: Option<wgpu::TextureFormat>,
screen_capture_state: Option<CaptureState>, screen_capture_state: Option<CaptureState>,
instance: wgpu::Instance, instance: Arc<wgpu::Instance>,
render_state: Option<RenderState>, render_state: Option<RenderState>,
// Per viewport/window: // Per viewport/window:
@ -116,10 +116,15 @@ impl Painter {
support_transparent_backbuffer: bool, support_transparent_backbuffer: bool,
dithering: bool, dithering: bool,
) -> Self { ) -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { let instance = match &configuration.wgpu_setup {
backends: configuration.supported_backends, crate::WgpuSetup::CreateNew {
supported_backends, ..
} => Arc::new(wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: *supported_backends,
..Default::default() ..Default::default()
}); })),
crate::WgpuSetup::Existing { instance, .. } => instance.clone(),
};
Self { Self {
configuration, configuration,