Extend `WgpuSetup`, `egui_kittest` now prefers software rasterizers for testing (#5506)

This commit is contained in:
Andreas Reich 2025-01-08 14:24:58 +01:00 committed by GitHub
parent 7186f72cbe
commit 443df84a22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 478 additions and 253 deletions

View File

@ -1349,7 +1349,6 @@ dependencies = [
"egui_kittest",
"serde",
"unicode_names2",
"wgpu",
]
[[package]]
@ -1927,6 +1926,18 @@ dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "gpu-allocator"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd"
dependencies = [
"log",
"presser",
"thiserror",
"windows",
]
[[package]]
name = "gpu-descriptor"
version = "0.3.0"
@ -3110,6 +3121,12 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "presser"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
@ -3263,6 +3280,12 @@ dependencies = [
"getrandom",
]
[[package]]
name = "range-alloc"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab"
[[package]]
name = "raw-window-handle"
version = "0.5.2"
@ -4513,6 +4536,7 @@ dependencies = [
"android_system_properties",
"arrayvec",
"ash",
"bit-set 0.8.0",
"bitflags 2.6.0",
"block",
"bytemuck",
@ -4521,6 +4545,7 @@ dependencies = [
"glow 0.14.2",
"glutin_wgl_sys",
"gpu-alloc",
"gpu-allocator",
"gpu-descriptor",
"js-sys",
"khronos-egl",
@ -4534,6 +4559,7 @@ dependencies = [
"once_cell",
"parking_lot",
"profiling",
"range-alloc",
"raw-window-handle 0.6.2",
"renderdoc-sys",
"rustc-hash",
@ -4543,6 +4569,7 @@ dependencies = [
"web-sys",
"wgpu-types",
"windows",
"windows-core 0.58.0",
]
[[package]]

View File

@ -183,7 +183,7 @@ impl<'app> WgpuWinitApp<'app> {
) -> crate::Result<&mut WgpuWinitRunning<'app>> {
profiling::function_scope!();
#[allow(unsafe_code, unused_mut, unused_unsafe)]
let mut painter = egui_wgpu::winit::Painter::new(
let mut painter = pollster::block_on(egui_wgpu::winit::Painter::new(
egui_ctx.clone(),
self.native_options.wgpu_options.clone(),
self.native_options.multisampling.max(1) as _,
@ -193,7 +193,7 @@ impl<'app> WgpuWinitApp<'app> {
),
self.native_options.viewport.transparent.unwrap_or(false),
self.native_options.dithering,
);
));
let window = Arc::new(window);

View File

@ -4,7 +4,7 @@ use super::web_painter::WebPainter;
use crate::WebOptions;
use egui::{Event, UserData, ViewportId};
use egui_wgpu::capture::{capture_channel, CaptureReceiver, CaptureSender, CaptureState};
use egui_wgpu::{RenderState, SurfaceErrorAction, WgpuSetup};
use egui_wgpu::{RenderState, SurfaceErrorAction};
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
@ -63,49 +63,7 @@ impl WebPainterWgpu {
) -> Result<Self, String> {
log::debug!("Creating wgpu painter");
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());
if !is_secure_context {
log::info!(
"WebGPU is only available in secure contexts, i.e. on HTTPS and on localhost."
);
// Don't try WebGPU since we established now that it will fail.
backends.remove(wgpu::Backends::BROWSER_WEBGPU);
if backends.is_empty() {
return Err("No available supported graphics backends.".to_owned());
}
}
}
log::debug!("Creating wgpu instance with backends {:?}", backends);
let instance =
wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor {
backends,
..Default::default()
})
.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(instance)
}
WgpuSetup::Existing { instance, .. } => instance.clone(),
};
let instance = options.wgpu_options.wgpu_setup.new_instance().await;
let surface = instance
.create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone()))
.map_err(|err| format!("failed to create wgpu surface: {err}"))?;

View File

@ -23,8 +23,10 @@ pub use wgpu;
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
mod renderer;
mod setup;
pub use renderer::*;
use wgpu::{Adapter, Device, Instance, Queue, TextureFormat};
pub use setup::{NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew, WgpuSetupExisting};
/// Helpers for capturing screenshots of the UI.
pub mod capture;
@ -40,8 +42,8 @@ use epaint::mutex::RwLock;
/// An error produced by egui-wgpu.
#[derive(thiserror::Error, Debug)]
pub enum WgpuError {
#[error("Failed to create wgpu adapter, no suitable adapter found.")]
NoSuitableAdapterFound,
#[error("Failed to create wgpu adapter, no suitable adapter found: {0}")]
NoSuitableAdapterFound(String),
#[error("There was no valid format for the surface at all.")]
NoSurfaceFormatsAvailable,
@ -67,8 +69,9 @@ pub struct RenderState {
///
/// This is not available on web.
/// On web, we always select WebGPU is available, then fall back to WebGL if not.
// TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here once we update to wgpu 24
#[cfg(not(target_arch = "wasm32"))]
pub available_adapters: Arc<[wgpu::Adapter]>,
pub available_adapters: Arc<[Arc<wgpu::Adapter>]>,
/// Wgpu device used for rendering, created from the adapter.
pub device: Arc<wgpu::Device>,
@ -83,6 +86,75 @@ pub struct RenderState {
pub renderer: Arc<RwLock<Renderer>>,
}
async fn request_adapter(
instance: &wgpu::Instance,
power_preference: wgpu::PowerPreference,
compatible_surface: Option<&wgpu::Surface<'_>>,
_available_adapters: &[Arc<wgpu::Adapter>],
) -> Result<Arc<wgpu::Adapter>, WgpuError> {
profiling::function_scope!();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
compatible_surface,
// We don't expose this as an option right now since it's fairly rarely useful:
// * only has an effect on native
// * fails if there's no software rasterizer available
// * can achieve the same with `native_adapter_selector`
force_fallback_adapter: false,
})
.await
.ok_or_else(|| {
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.is_empty() {
log::info!("No wgpu adapters found");
} else if _available_adapters.len() == 1 {
log::info!(
"The only available wgpu adapter was not suitable: {}",
adapter_info_summary(&_available_adapters[0].get_info())
);
} else {
log::info!(
"No suitable wgpu adapter found out of the {} available ones: {}",
_available_adapters.len(),
describe_adapters(_available_adapters)
);
}
WgpuError::NoSuitableAdapterFound("`request_adapters` returned `None`".to_owned())
})?;
#[cfg(target_arch = "wasm32")]
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
#[cfg(not(target_arch = "wasm32"))]
if _available_adapters.len() == 1 {
log::debug!(
"Picked the only available wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
log::info!(
"There were {} available wgpu adapters: {}",
_available_adapters.len(),
describe_adapters(_available_adapters)
);
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
}
// 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)]
Ok(Arc::new(adapter))
}
impl RenderState {
/// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu.
///
@ -100,96 +172,73 @@ impl RenderState {
// This is always an empty list on web.
#[cfg(not(target_arch = "wasm32"))]
let available_adapters = instance.enumerate_adapters(wgpu::Backends::all());
let available_adapters = {
let backends = if let WgpuSetup::CreateNew(create_new) = &config.wgpu_setup {
create_new.instance_descriptor.backends
} else {
wgpu::Backends::all()
};
instance
.enumerate_adapters(backends)
// TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here once we update to wgpu 24.
.into_iter()
.map(Arc::new)
.collect::<Vec<_>>()
};
let (adapter, device, queue) = match config.wgpu_setup.clone() {
WgpuSetup::CreateNew {
supported_backends: _,
WgpuSetup::CreateNew(WgpuSetupCreateNew {
instance_descriptor: _,
power_preference,
native_adapter_selector: _native_adapter_selector,
device_descriptor,
} => {
trace_path,
}) => {
let adapter = {
profiling::scope!("request_adapter");
instance
.request_adapter(&wgpu::RequestAdapterOptions {
#[cfg(target_arch = "wasm32")]
{
request_adapter(instance, power_preference, compatible_surface, &[]).await
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(native_adapter_selector) = _native_adapter_selector {
native_adapter_selector(&available_adapters, compatible_surface)
.map_err(WgpuError::NoSuitableAdapterFound)
} else {
request_adapter(
instance,
power_preference,
compatible_surface,
force_fallback_adapter: false,
})
&available_adapters,
)
.await
.ok_or_else(|| {
#[cfg(not(target_arch = "wasm32"))]
if available_adapters.is_empty() {
log::info!("No wgpu adapters found");
} else if available_adapters.len() == 1 {
log::info!(
"The only available wgpu adapter was not suitable: {}",
adapter_info_summary(&available_adapters[0].get_info())
);
} else {
log::info!(
"No suitable wgpu adapter found out of the {} available ones: {}",
available_adapters.len(),
describe_adapters(&available_adapters)
);
}
}
}?;
WgpuError::NoSuitableAdapterFound
})?
};
#[cfg(target_arch = "wasm32")]
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
#[cfg(not(target_arch = "wasm32"))]
if available_adapters.len() == 1 {
log::debug!(
"Picked the only available wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
} else {
log::info!(
"There were {} available wgpu adapters: {}",
available_adapters.len(),
describe_adapters(&available_adapters)
);
log::debug!(
"Picked wgpu adapter: {}",
adapter_info_summary(&adapter.get_info())
);
}
let trace_path = std::env::var("WGPU_TRACE");
let (device, queue) = {
profiling::scope!("request_device");
adapter
.request_device(
&(*device_descriptor)(&adapter),
trace_path.ok().as_ref().map(std::path::Path::new),
)
.request_device(&(*device_descriptor)(&adapter), trace_path.as_deref())
.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))
(adapter, Arc::new(device), Arc::new(queue))
}
WgpuSetup::Existing {
WgpuSetup::Existing(WgpuSetupExisting {
instance: _,
adapter,
device,
queue,
} => (adapter, device, queue),
}) => (adapter, device, queue),
};
let surface_formats = {
profiling::scope!("get_capabilities");
compatible_surface.map_or_else(
|| vec![TextureFormat::Rgba8Unorm],
|| vec![wgpu::TextureFormat::Rgba8Unorm],
|s| s.get_capabilities(&adapter).formats,
)
};
@ -219,20 +268,17 @@ impl RenderState {
}
#[cfg(not(target_arch = "wasm32"))]
fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
fn describe_adapters(adapters: &[Arc<wgpu::Adapter>]) -> String {
if adapters.is_empty() {
"(none)".to_owned()
} else if adapters.len() == 1 {
adapter_info_summary(&adapters[0].get_info())
} else {
let mut list_string = String::new();
for adapter in adapters {
if !list_string.is_empty() {
list_string += ", ";
}
list_string += &format!("{{{}}}", adapter_info_summary(&adapter.get_info()));
}
list_string
adapters
.iter()
.map(|a| format!("{{{}}}", adapter_info_summary(&a.get_info())))
.collect::<Vec<_>>()
.join(", ")
}
}
@ -245,62 +291,6 @@ pub enum SurfaceErrorAction {
RecreateSurface,
}
#[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:
/// * `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),
/// you can set this to [`wgpu::Backends::GL`].
///
/// 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`.
supported_backends: wgpu::Backends,
/// Power preference for the adapter.
power_preference: wgpu::PowerPreference,
/// Configuration passed on device request, given an adapter
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 {
@ -352,42 +342,8 @@ 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()
} else {
wgpu::Limits::default()
};
wgpu::DeviceDescriptor {
label: Some("egui wgpu device"),
required_features: wgpu::Features::default(),
required_limits: wgpu::Limits {
// When using a depth buffer, we have to be able to create a texture
// large enough for the entire surface, and we want to support 4k+ displays.
max_texture_dimension_2d: 8192,
..base_limits
},
memory_hints: wgpu::MemoryHints::default(),
}
}),
},
wgpu_setup: Default::default(),
on_surface_error: Arc::new(|err| {
if err == wgpu::SurfaceError::Outdated {
// This error occurs when the app is minimized on Windows.
@ -468,8 +424,14 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
summary += &format!(", driver_info: {driver_info:?}");
}
if *vendor != 0 {
// TODO(emilk): decode using https://github.com/gfx-rs/wgpu/blob/767ac03245ee937d3dc552edc13fe7ab0a860eec/wgpu-hal/src/auxil/mod.rs#L7
summary += &format!(", vendor: 0x{vendor:04X}");
#[cfg(not(target_arch = "wasm32"))]
{
summary += &format!(", vendor: {} (0x{vendor:04X})", parse_vendor_id(*vendor));
}
#[cfg(target_arch = "wasm32")]
{
summary += &format!(", vendor: 0x{vendor:04X}");
}
}
if *device != 0 {
summary += &format!(", device: 0x{device:02X}");
@ -477,3 +439,20 @@ pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
summary
}
/// Tries to parse the adapter's vendor ID to a human-readable string.
#[cfg(not(target_arch = "wasm32"))]
pub fn parse_vendor_id(vendor_id: u32) -> &'static str {
match vendor_id {
wgpu::hal::auxil::db::amd::VENDOR => "AMD",
wgpu::hal::auxil::db::apple::VENDOR => "Apple",
wgpu::hal::auxil::db::arm::VENDOR => "ARM",
wgpu::hal::auxil::db::broadcom::VENDOR => "Broadcom",
wgpu::hal::auxil::db::imgtec::VENDOR => "Imagination Technologies",
wgpu::hal::auxil::db::intel::VENDOR => "Intel",
wgpu::hal::auxil::db::mesa::VENDOR => "Mesa",
wgpu::hal::auxil::db::nvidia::VENDOR => "NVIDIA",
wgpu::hal::auxil::db::qualcomm::VENDOR => "Qualcomm",
_ => "Unknown",
}
}

View File

@ -0,0 +1,237 @@
use std::sync::Arc;
#[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.
///
/// By default can also be configured with various environment variables:
/// * `WGPU_BACKEND`: `vulkan`, `dx12`, `metal`, `opengl`, `webgpu`
/// * `WGPU_POWER_PREF`: `low`, `high` or `none`
/// * `WGPU_TRACE`: Path to a file to output a wgpu trace file.
///
/// Each instance flag also comes with an environment variable (for details see [`wgpu::InstanceFlags`]):
/// * `WGPU_VALIDATION`: Enables validation (enabled by default in debug builds).
/// * `WGPU_DEBUG`: Generate debug information in shaders and objects (enabled by default in debug builds).
/// * `WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER`: Whether wgpu should expose adapters that run on top of non-compliant adapters.
/// * `WGPU_GPU_BASED_VALIDATION`: Enable GPU-based validation.
CreateNew(WgpuSetupCreateNew),
/// Run on an existing wgpu setup.
Existing(WgpuSetupExisting),
}
impl Default for WgpuSetup {
fn default() -> Self {
Self::CreateNew(WgpuSetupCreateNew::default())
}
}
impl std::fmt::Debug for WgpuSetup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CreateNew(create_new) => f
.debug_tuple("WgpuSetup::CreateNew")
.field(create_new)
.finish(),
Self::Existing { .. } => f.debug_tuple("WgpuSetup::Existing").finish(),
}
}
}
impl WgpuSetup {
/// Creates a new [`wgpu::Instance`] or clones the existing one.
///
/// Does *not* store the wgpu instance, so calling this repeatedly may
/// create a new instance every time!
pub async fn new_instance(&self) -> Arc<wgpu::Instance> {
match self {
Self::CreateNew(create_new) => {
#[allow(unused_mut)]
let mut backends = create_new.instance_descriptor.backends;
// Don't try WebGPU if we're not in a secure context.
#[cfg(target_arch = "wasm32")]
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
let is_secure_context =
wgpu::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."
);
backends.remove(wgpu::Backends::BROWSER_WEBGPU);
}
}
log::debug!("Creating wgpu instance with backends {:?}", backends);
#[allow(clippy::arc_with_non_send_sync)]
Arc::new(
wgpu::util::new_instance_with_webgpu_detection(wgpu::InstanceDescriptor {
backends: create_new.instance_descriptor.backends,
flags: create_new.instance_descriptor.flags,
dx12_shader_compiler: create_new
.instance_descriptor
.dx12_shader_compiler
.clone(),
gles_minor_version: create_new.instance_descriptor.gles_minor_version,
})
.await,
)
}
Self::Existing(existing) => existing.instance.clone(),
}
}
}
impl From<WgpuSetupCreateNew> for WgpuSetup {
fn from(create_new: WgpuSetupCreateNew) -> Self {
Self::CreateNew(create_new)
}
}
impl From<WgpuSetupExisting> for WgpuSetup {
fn from(existing: WgpuSetupExisting) -> Self {
Self::Existing(existing)
}
}
/// Method for selecting an adapter on native.
///
/// This can be used for fully custom adapter selection.
/// If available, `wgpu::Surface` is passed to allow checking for surface compatibility.
// TODO(gfx-rs/wgpu#6665): Remove layer of `Arc` here.
pub type NativeAdapterSelectorMethod = Arc<
dyn Fn(&[Arc<wgpu::Adapter>], Option<&wgpu::Surface<'_>>) -> Result<Arc<wgpu::Adapter>, String>
+ Send
+ Sync,
>;
/// Configuration for creating a new wgpu setup.
///
/// Used for [`WgpuSetup::CreateNew`].
pub struct WgpuSetupCreateNew {
/// Instance descriptor for creating a wgpu instance.
///
/// 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),
/// you can set this to [`wgpu::Backends::GL`].
/// 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 instance_descriptor: wgpu::InstanceDescriptor,
/// Power preference for the adapter if [`Self::native_adapter_selector`] is not set or targeting web.
pub power_preference: wgpu::PowerPreference,
/// Optional selector for native adapters.
///
/// This field has no effect when targeting web!
/// Otherwise, if set [`Self::power_preference`] is ignored and the adapter is instead selected by this method.
/// Note that [`Self::instance_descriptor`]'s [`wgpu::InstanceDescriptor::backends`]
/// are still used to filter the adapter enumeration in the first place.
///
/// Defaults to `None`.
pub native_adapter_selector: Option<NativeAdapterSelectorMethod>,
/// Configuration passed on device request, given an adapter
pub device_descriptor:
Arc<dyn Fn(&wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> + Send + Sync>,
/// Option path to output a wgpu trace file.
///
/// This only works if this feature is enabled in `wgpu-core`.
/// Does not work when running with WebGPU.
/// Defaults to the path set in the `WGPU_TRACE` environment variable.
pub trace_path: Option<std::path::PathBuf>,
}
impl Clone for WgpuSetupCreateNew {
fn clone(&self) -> Self {
Self {
// TODO(gfx-rs/wgpu/#6849): use .clone()
instance_descriptor: wgpu::InstanceDescriptor {
backends: self.instance_descriptor.backends,
flags: self.instance_descriptor.flags,
dx12_shader_compiler: self.instance_descriptor.dx12_shader_compiler.clone(),
gles_minor_version: self.instance_descriptor.gles_minor_version,
},
power_preference: self.power_preference,
native_adapter_selector: self.native_adapter_selector.clone(),
device_descriptor: self.device_descriptor.clone(),
trace_path: self.trace_path.clone(),
}
}
}
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(),
)
.field("trace_path", &self.trace_path)
.finish()
}
}
impl Default for WgpuSetupCreateNew {
fn default() -> Self {
Self {
instance_descriptor: wgpu::InstanceDescriptor {
// 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")
backends: wgpu::util::backend_bits_from_env()
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
flags: wgpu::InstanceFlags::from_build_config().with_env(),
dx12_shader_compiler: wgpu::Dx12Compiler::default(),
gles_minor_version: wgpu::Gles3MinorVersion::Automatic,
},
power_preference: wgpu::util::power_preference_from_env()
.unwrap_or(wgpu::PowerPreference::HighPerformance),
native_adapter_selector: None,
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 {
// When using a depth buffer, we have to be able to create a texture
// large enough for the entire surface, and we want to support 4k+ displays.
max_texture_dimension_2d: 8192,
..base_limits
},
memory_hints: wgpu::MemoryHints::default(),
}
}),
trace_path: std::env::var("WGPU_TRACE")
.ok()
.map(std::path::PathBuf::from),
}
}
}
/// Configuration for using an existing wgpu setup.
///
/// Used for [`WgpuSetup::Existing`].
#[derive(Clone)]
pub struct WgpuSetupExisting {
pub instance: Arc<wgpu::Instance>,
pub adapter: Arc<wgpu::Adapter>,
pub device: Arc<wgpu::Device>,
pub queue: Arc<wgpu::Queue>,
}

View File

@ -51,7 +51,7 @@ impl Painter {
/// [`set_window()`](Self::set_window) once you have
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
/// associated.
pub fn new(
pub async fn new(
context: Context,
configuration: WgpuConfiguration,
msaa_samples: u32,
@ -59,17 +59,8 @@ impl Painter {
support_transparent_backbuffer: bool,
dithering: bool,
) -> Self {
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(),
};
let (capture_tx, capture_rx) = capture_channel();
let instance = configuration.wgpu_setup.new_instance().await;
Self {
context,

View File

@ -57,7 +57,6 @@ serde = { workspace = true, optional = true }
[dev-dependencies]
criterion.workspace = true
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
wgpu = { workspace = true, features = ["metal"] }
egui = { workspace = true, features = ["default_fonts"] }
[[bench]]

View File

@ -20,7 +20,13 @@ include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"]
[features]
# Adds a wgpu-based test renderer.
wgpu = ["dep:egui-wgpu", "dep:pollster", "dep:image", "eframe?/wgpu"]
wgpu = [
"dep:egui-wgpu",
"dep:pollster",
"dep:image",
"dep:wgpu",
"eframe?/wgpu",
]
# Adds a dify-based image snapshot utility.
snapshot = ["dep:dify", "dep:image", "image/png"]
@ -38,6 +44,8 @@ eframe = { workspace = true, optional = true }
egui-wgpu = { workspace = true, optional = true }
pollster = { workspace = true, optional = true }
image = { workspace = true, optional = true }
# Enable DX12 because it always comes with a software rasterizer.
wgpu = { workspace = true, features = ["metal", "dx12"], optional = true }
# snapshot dependencies
dify = { workspace = true, optional = true }
@ -48,7 +56,6 @@ document-features = { workspace = true, optional = true }
[dev-dependencies]
egui = { workspace = true, features = ["default_fonts"] }
image = { workspace = true, features = ["png"] }
wgpu = { workspace = true, features = ["metal"] }
[lints]
workspace = true

View File

@ -1,27 +1,56 @@
use crate::texture_to_image::texture_to_image;
use eframe::epaint::TextureId;
use egui::TexturesDelta;
use egui_wgpu::wgpu::{Backends, StoreOp, TextureFormat};
use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup};
use image::RgbaImage;
use std::iter::once;
use std::sync::Arc;
use wgpu::Maintain;
// TODO(#5506): Replace this with the setup from https://github.com/emilk/egui/pull/5506
use egui::TexturesDelta;
use egui_wgpu::{wgpu, RenderState, ScreenDescriptor, WgpuSetup};
use image::RgbaImage;
use crate::texture_to_image::texture_to_image;
/// Default wgpu setup used for the wgpu renderer.
pub fn default_wgpu_setup() -> egui_wgpu::WgpuSetup {
egui_wgpu::WgpuSetup::CreateNew {
supported_backends: Backends::all(),
device_descriptor: Arc::new(|_| wgpu::DeviceDescriptor::default()),
power_preference: wgpu::PowerPreference::default(),
}
let mut setup = egui_wgpu::WgpuSetupCreateNew::default();
// WebGPU not supported yet since we rely on blocking screenshots.
setup
.instance_descriptor
.backends
.remove(wgpu::Backends::BROWSER_WEBGPU);
// Prefer software rasterizers.
setup.native_adapter_selector = Some(Arc::new(|adapters, _surface| {
let mut adapters = adapters.iter().collect::<Vec<_>>();
// Adapters are already sorted by preferred backend by wgpu, but let's be explicit.
adapters.sort_by_key(|a| match a.get_info().backend {
wgpu::Backend::Metal => 0,
wgpu::Backend::Vulkan => 1,
wgpu::Backend::Dx12 => 2,
wgpu::Backend::Gl => 4,
wgpu::Backend::BrowserWebGpu => 6,
wgpu::Backend::Empty => 7,
});
// Prefer CPU adapters, otherwise if we can't, prefer discrete GPU over integrated GPU.
adapters.sort_by_key(|a| match a.get_info().device_type {
wgpu::DeviceType::Cpu => 0, // CPU is the best for our purposes!
wgpu::DeviceType::DiscreteGpu => 1,
wgpu::DeviceType::Other
| wgpu::DeviceType::IntegratedGpu
| wgpu::DeviceType::VirtualGpu => 2,
});
adapters
.first()
.map(|a| (*a).clone())
.ok_or("No adapter found".to_owned())
}));
egui_wgpu::WgpuSetup::CreateNew(setup)
}
pub fn create_render_state(setup: WgpuSetup) -> egui_wgpu::RenderState {
let instance = match &setup {
WgpuSetup::Existing { instance, .. } => instance.clone(),
WgpuSetup::CreateNew { .. } => Default::default(),
};
let instance = pollster::block_on(setup.new_instance());
pollster::block_on(egui_wgpu::RenderState::create(
&egui_wgpu::WgpuConfiguration {
@ -72,7 +101,7 @@ impl WgpuTestRenderer {
render_state
.renderer
.read()
.texture(&TextureId::Managed(0))
.texture(&egui::epaint::TextureId::Managed(0))
.is_none(),
"The RenderState passed in has been used before, pass in a fresh RenderState instead."
);
@ -143,7 +172,7 @@ impl crate::TestRenderer for WgpuTestRenderer {
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TextureFormat::Rgba8Unorm,
format: self.render_state.target_format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
@ -159,12 +188,10 @@ impl crate::TestRenderer for WgpuTestRenderer {
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
store: StoreOp::Store,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
..Default::default()
})
.forget_lifetime();
@ -175,7 +202,7 @@ impl crate::TestRenderer for WgpuTestRenderer {
.queue
.submit(user_buffers.into_iter().chain(once(encoder.finish())));
self.render_state.device.poll(Maintain::Wait);
self.render_state.device.poll(wgpu::Maintain::Wait);
Ok(texture_to_image(
&self.render_state.device,