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::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, WebDisplayHandle, WebWindowHandle, WindowHandle,
};
use std::sync::Arc;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
use egui_wgpu::{RenderState, SurfaceErrorAction};
use crate::WebOptions;
use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup};
use super::web_painter::WebPainter;
@ -89,11 +87,18 @@ impl WebPainterWgpu {
) -> Result<Self, String> {
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.
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 {
log::info!(
"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.");
if let Some(adapter) = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: options.wgpu_options.power_preference,
power_preference: *power_preference,
compatible_surface: None,
force_fallback_adapter: false,
})
@ -159,12 +164,21 @@ impl WebPainterWgpu {
});
} else {
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
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;

View File

@ -8,8 +8,8 @@
//! wgpu = { version = "*", features = ["webgpu", "webgl"] }
//! ```
//!
//! You can control whether WebGL or WebGPU will be picked at runtime by setting
//! [`WgpuConfiguration::supported_backends`].
//! You can control whether WebGL or WebGPU will be picked at runtime by configuring
//! [`WgpuConfiguration::wgpu_setup`].
//! The default is to prefer WebGPU and fall back on WebGL.
//!
//! ## Feature flags
@ -24,6 +24,7 @@ pub use wgpu;
mod renderer;
pub use renderer::*;
use wgpu::{Adapter, Device, Instance, Queue};
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")]
@ -98,11 +99,17 @@ impl RenderState {
#[cfg(not(target_arch = "wasm32"))]
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 = {
crate::profile_scope!("request_adapter");
instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: config.power_preference,
power_preference,
compatible_surface: Some(surface),
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 = {
crate::profile_scope!("get_capabilities");
surface.get_capabilities(&adapter).formats
};
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(
&device,
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.
#[allow(clippy::arc_with_non_send_sync)]
Ok(Self {
adapter: Arc::new(adapter),
adapter,
#[cfg(not(target_arch = "wasm32"))]
available_adapters: available_adapters.into(),
device: Arc::new(device),
queue: Arc::new(queue),
device,
queue,
target_format,
renderer: Arc::new(RwLock::new(renderer)),
})
@ -215,13 +235,16 @@ pub enum SurfaceErrorAction {
RecreateSurface,
}
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
///
/// This can also be configured with the environment variables:
/// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu`
/// * `WGPU_POWER_PREF`: `low`, `high` or `none`
#[derive(Clone)]
pub struct WgpuConfiguration {
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:
/// * `WGPU_BACKEND`: `vulkan`, `dx11`, `dx12`, `metal`, `opengl`, `webgpu`
/// * `WGPU_POWER_PREF`: `low`, `high` or `none`
CreateNew {
/// Backends that should be supported (wgpu will pick one of these).
///
/// 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.
/// WebGL will only be used as a fallback,
/// 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
pub device_descriptor:
device_descriptor:
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.
pub present_mode: wgpu::PresentMode,
@ -248,8 +306,8 @@ pub struct WgpuConfiguration {
/// `None` = `wgpu` default.
pub desired_maximum_frame_latency: Option<u32>,
/// Power preference for the adapter.
pub power_preference: wgpu::PowerPreference,
/// How to create the wgpu adapter & device
pub wgpu_setup: WgpuSetup,
/// Callback for surface errors.
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
supported_backends,
device_descriptor: _,
present_mode,
desired_maximum_frame_latency,
power_preference,
wgpu_setup,
on_surface_error: _,
} = self;
f.debug_struct("WgpuConfiguration")
.field("supported_backends", &supported_backends)
.field("present_mode", &present_mode)
.field(
"desired_maximum_frame_latency",
&desired_maximum_frame_latency,
)
.field("power_preference", &power_preference)
.field("wgpu_setup", &wgpu_setup)
.finish_non_exhaustive()
}
}
@ -286,11 +341,22 @@ impl std::fmt::Debug for WgpuConfiguration {
impl Default for WgpuConfiguration {
fn default() -> 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.
// (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()
.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| {
let base_limits = if adapter.get_info().backend == wgpu::Backend::Gl {
wgpu::Limits::downlevel_webgl2_defaults()
@ -310,13 +376,7 @@ impl Default for WgpuConfiguration {
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| {
if err == wgpu::SurfaceError::Outdated {

View File

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