Fix desktop launch, app icons and keyboard parameter editing
This commit is contained in:
1 parent
8fa52c8e5e
commit
6dd202d277
45 files changed
+5993
-272
No files matched your search
Vendored
+267
@@ -0,0 +1,267 @@
|
||||
use egui::{UserData, ViewportId};
|
||||
use epaint::ColorImage;
|
||||
use std::sync::{Arc, mpsc};
|
||||
use wgpu::{BindGroupLayout, MultisampleState, StoreOp};
|
||||
|
||||
/// A texture and a buffer for reading the rendered frame back to the cpu.
|
||||
///
|
||||
/// The texture is required since [`wgpu::TextureUsages::COPY_SRC`] is not an allowed
|
||||
/// flag for the surface texture on all platforms. This means that anytime we want to
|
||||
/// capture the frame, we first render it to this texture, and then we can copy it to
|
||||
/// both the surface texture (via a render pass) and the buffer (via a texture to buffer copy),
|
||||
/// from where we can pull it back
|
||||
/// to the cpu.
|
||||
pub struct CaptureState {
|
||||
padding: BufferPadding,
|
||||
pub texture: wgpu::Texture,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
bind_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
pub type CaptureReceiver = mpsc::Receiver<(ViewportId, Vec<UserData>, ColorImage)>;
|
||||
pub type CaptureSender = mpsc::Sender<(ViewportId, Vec<UserData>, ColorImage)>;
|
||||
pub use mpsc::channel as capture_channel;
|
||||
|
||||
impl CaptureState {
|
||||
pub fn new(device: &wgpu::Device, surface_texture: &wgpu::Texture) -> Self {
|
||||
let shader = device.create_shader_module(wgpu::include_wgsl!("texture_copy.wgsl"));
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("texture_copy"),
|
||||
layout: None,
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(surface_texture.format().into())],
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||
..Default::default()
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let bind_group_layout = pipeline.get_bind_group_layout(0);
|
||||
|
||||
let (texture, padding, bind_group) =
|
||||
Self::create_texture(device, surface_texture, &bind_group_layout);
|
||||
|
||||
Self {
|
||||
padding,
|
||||
texture,
|
||||
pipeline,
|
||||
bind_group,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_texture(
|
||||
device: &wgpu::Device,
|
||||
surface_texture: &wgpu::Texture,
|
||||
layout: &BindGroupLayout,
|
||||
) -> (wgpu::Texture, BufferPadding, wgpu::BindGroup) {
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_screen_capture_texture"),
|
||||
size: surface_texture.size(),
|
||||
mip_level_count: surface_texture.mip_level_count(),
|
||||
sample_count: surface_texture.sample_count(),
|
||||
dimension: surface_texture.dimension(),
|
||||
format: surface_texture.format(),
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING
|
||||
| wgpu::TextureUsages::COPY_SRC,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let padding = BufferPadding::new(surface_texture.width());
|
||||
|
||||
let view = texture.create_view(&Default::default());
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&view),
|
||||
}],
|
||||
label: None,
|
||||
});
|
||||
|
||||
(texture, padding, bind_group)
|
||||
}
|
||||
|
||||
/// Updates the [`CaptureState`] if the size of the surface texture has changed
|
||||
pub fn update(&mut self, device: &wgpu::Device, texture: &wgpu::Texture) {
|
||||
if self.texture.size() != texture.size() {
|
||||
let (new_texture, padding, bind_group) =
|
||||
Self::create_texture(device, texture, &self.pipeline.get_bind_group_layout(0));
|
||||
self.texture = new_texture;
|
||||
self.padding = padding;
|
||||
self.bind_group = bind_group;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles copying from the [`CaptureState`] texture to the surface texture and the buffer.
|
||||
/// Pass the returned buffer to [`CaptureState::read_screen_rgba`] to read the data back to the cpu.
|
||||
pub fn copy_textures(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
output_frame: &wgpu::SurfaceTexture,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
) -> wgpu::Buffer {
|
||||
debug_assert_eq!(
|
||||
self.texture.size(),
|
||||
output_frame.texture.size(),
|
||||
"Texture sizes must match, `CaptureState::update` was probably not called"
|
||||
);
|
||||
|
||||
// It would be more efficient to reuse the Buffer, e.g. via some kind of ring buffer, but
|
||||
// for most screenshot use cases this should be fine. When taking many screenshots (e.g. for a video)
|
||||
// it might make sense to revisit this and implement a more efficient solution.
|
||||
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("egui_screen_capture_buffer"),
|
||||
size: (self.padding.padded_bytes_per_row * self.texture.height()) as u64,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
let padding = self.padding;
|
||||
let tex = &mut self.texture;
|
||||
|
||||
let tex_extent = tex.size();
|
||||
|
||||
encoder.copy_texture_to_buffer(
|
||||
tex.as_image_copy(),
|
||||
wgpu::TexelCopyBufferInfo {
|
||||
buffer: &buffer,
|
||||
layout: wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(padding.padded_bytes_per_row),
|
||||
rows_per_image: None,
|
||||
},
|
||||
},
|
||||
tex_extent,
|
||||
);
|
||||
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("texture_copy"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &output_frame.texture.create_view(&Default::default()),
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Handles copying from the [`CaptureState`] texture to the surface texture and the cpu
|
||||
/// This function is non-blocking and will send the data to the given sender when it's ready.
|
||||
/// Pass in the buffer returned from [`CaptureState::copy_textures`].
|
||||
/// Make sure to call this after the encoder has been submitted.
|
||||
pub fn read_screen_rgba(
|
||||
&self,
|
||||
ctx: egui::Context,
|
||||
buffer: wgpu::Buffer,
|
||||
data: Vec<UserData>,
|
||||
tx: CaptureSender,
|
||||
viewport_id: ViewportId,
|
||||
) {
|
||||
#[allow(clippy::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
||||
let buffer = Arc::new(buffer);
|
||||
let buffer_clone = Arc::clone(&buffer);
|
||||
let buffer_slice = buffer_clone.slice(..);
|
||||
let format = self.texture.format();
|
||||
let tex_extent = self.texture.size();
|
||||
let padding = self.padding;
|
||||
let to_rgba = match format {
|
||||
wgpu::TextureFormat::Rgba8Unorm => [0, 1, 2, 3],
|
||||
wgpu::TextureFormat::Bgra8Unorm => [2, 1, 0, 3],
|
||||
_ => {
|
||||
log::error!(
|
||||
"Screen can't be captured unless the surface format is Rgba8Unorm or Bgra8Unorm. Current surface format is {format:?}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
|
||||
if let Err(err) = result {
|
||||
log::error!("Failed to map buffer for reading: {err}");
|
||||
return;
|
||||
}
|
||||
let buffer_slice = buffer.slice(..);
|
||||
|
||||
let mut pixels = Vec::with_capacity((tex_extent.width * tex_extent.height) as usize);
|
||||
let mapped_range = match buffer_slice.get_mapped_range() {
|
||||
Ok(range) => range,
|
||||
Err(err) => {
|
||||
log::error!("Failed to get mapped range for reading: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
for padded_row in mapped_range.chunks(padding.padded_bytes_per_row as usize) {
|
||||
let row = &padded_row[..padding.unpadded_bytes_per_row as usize];
|
||||
for color in row.chunks(4) {
|
||||
pixels.push(epaint::Color32::from_rgba_premultiplied(
|
||||
color[to_rgba[0]],
|
||||
color[to_rgba[1]],
|
||||
color[to_rgba[2]],
|
||||
color[to_rgba[3]],
|
||||
));
|
||||
}
|
||||
}
|
||||
drop(mapped_range);
|
||||
buffer.unmap();
|
||||
|
||||
tx.send((
|
||||
viewport_id,
|
||||
data,
|
||||
ColorImage::new(
|
||||
[tex_extent.width as usize, tex_extent.height as usize],
|
||||
pixels,
|
||||
),
|
||||
))
|
||||
.ok();
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct BufferPadding {
|
||||
unpadded_bytes_per_row: u32,
|
||||
padded_bytes_per_row: u32,
|
||||
}
|
||||
|
||||
impl BufferPadding {
|
||||
fn new(width: u32) -> Self {
|
||||
let bytes_per_pixel = core::mem::size_of::<u32>() as u32;
|
||||
let unpadded_bytes_per_row = width * bytes_per_pixel;
|
||||
let padded_bytes_per_row =
|
||||
wgpu::util::align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
|
||||
Self {
|
||||
unpadded_bytes_per_row,
|
||||
padded_bytes_per_row,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+162
@@ -0,0 +1,162 @@
|
||||
// Vertex shader bindings
|
||||
|
||||
struct VertexOutput {
|
||||
@location(0) tex_coord: vec2<f32>,
|
||||
@location(1) color: vec4<f32>, // gamma 0-1
|
||||
@builtin(position) position: vec4<f32>,
|
||||
};
|
||||
|
||||
struct Locals {
|
||||
screen_size: vec2<f32>,
|
||||
|
||||
/// 1 if dithering is enabled, 0 otherwise
|
||||
dithering: u32,
|
||||
|
||||
/// 1 to do manual filtering for more predictable kittest snapshot images.
|
||||
/// See also https://github.com/emilk/egui/issues/5295
|
||||
predictable_texture_filtering: u32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> r_locals: Locals;
|
||||
|
||||
|
||||
// -----------------------------------------------
|
||||
// Adapted from
|
||||
// https://www.shadertoy.com/view/llVGzG
|
||||
// Originally presented in:
|
||||
// Jimenez 2014, "Next Generation Post-Processing in Call of Duty"
|
||||
//
|
||||
// A good overview can be found in
|
||||
// https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence/
|
||||
// via https://github.com/rerun-io/rerun/
|
||||
fn interleaved_gradient_noise(n: vec2<f32>) -> f32 {
|
||||
let f = 0.06711056 * n.x + 0.00583715 * n.y;
|
||||
return fract(52.9829189 * fract(f));
|
||||
}
|
||||
|
||||
fn dither_interleaved(rgb: vec3<f32>, levels: f32, frag_coord: vec4<f32>) -> vec3<f32> {
|
||||
var noise = interleaved_gradient_noise(frag_coord.xy);
|
||||
// scale down the noise slightly to ensure flat colors aren't getting dithered
|
||||
noise = (noise - 0.5) * 0.95;
|
||||
return rgb + noise / (levels - 1.0);
|
||||
}
|
||||
|
||||
// 0-1 linear from 0-1 sRGB gamma
|
||||
fn linear_from_gamma_rgb(srgb: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = srgb < vec3<f32>(0.04045);
|
||||
let lower = srgb / vec3<f32>(12.92);
|
||||
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
// 0-1 sRGB gamma from 0-1 linear
|
||||
fn gamma_from_linear_rgb(rgb: vec3<f32>) -> vec3<f32> {
|
||||
let cutoff = rgb < vec3<f32>(0.0031308);
|
||||
let lower = rgb * vec3<f32>(12.92);
|
||||
let higher = vec3<f32>(1.055) * pow(rgb, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
||||
return select(higher, lower, cutoff);
|
||||
}
|
||||
|
||||
// 0-1 sRGBA gamma from 0-1 linear
|
||||
fn gamma_from_linear_rgba(linear_rgba: vec4<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(gamma_from_linear_rgb(linear_rgba.rgb), linear_rgba.a);
|
||||
}
|
||||
|
||||
// [u8; 4] SRGB as u32 -> [r, g, b, a] in 0.-1
|
||||
fn unpack_color(color: u32) -> vec4<f32> {
|
||||
return vec4<f32>(
|
||||
f32(color & 255u),
|
||||
f32((color >> 8u) & 255u),
|
||||
f32((color >> 16u) & 255u),
|
||||
f32((color >> 24u) & 255u),
|
||||
) / 255.0;
|
||||
}
|
||||
|
||||
fn position_from_screen(screen_pos: vec2<f32>) -> vec4<f32> {
|
||||
return vec4<f32>(
|
||||
2.0 * screen_pos.x / r_locals.screen_size.x - 1.0,
|
||||
1.0 - 2.0 * screen_pos.y / r_locals.screen_size.y,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(
|
||||
@location(0) a_pos: vec2<f32>,
|
||||
@location(1) a_tex_coord: vec2<f32>,
|
||||
@location(2) a_color: u32,
|
||||
) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
out.tex_coord = a_tex_coord;
|
||||
out.color = unpack_color(a_color);
|
||||
out.position = position_from_screen(a_pos);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Fragment shader bindings
|
||||
|
||||
@group(1) @binding(0) var r_tex_color: texture_2d<f32>;
|
||||
@group(1) @binding(1) var r_tex_sampler: sampler;
|
||||
|
||||
fn sample_texture(in: VertexOutput) -> vec4<f32> {
|
||||
if r_locals.predictable_texture_filtering == 0 {
|
||||
// Hardware filtering: fast, but varies across GPUs and drivers.
|
||||
return textureSample(r_tex_color, r_tex_sampler, in.tex_coord);
|
||||
} else {
|
||||
// Manual bilinear filtering with four taps at pixel centers using textureLoad
|
||||
let texture_size = vec2<i32>(textureDimensions(r_tex_color, 0));
|
||||
let texture_size_f = vec2<f32>(texture_size);
|
||||
let pixel_coord = in.tex_coord * texture_size_f - 0.5;
|
||||
let pixel_fract = fract(pixel_coord);
|
||||
let pixel_floor = vec2<i32>(floor(pixel_coord));
|
||||
|
||||
// Manual texture clamping
|
||||
let max_coord = texture_size - vec2<i32>(1, 1);
|
||||
let p00 = clamp(pixel_floor + vec2<i32>(0, 0), vec2<i32>(0, 0), max_coord);
|
||||
let p10 = clamp(pixel_floor + vec2<i32>(1, 0), vec2<i32>(0, 0), max_coord);
|
||||
let p01 = clamp(pixel_floor + vec2<i32>(0, 1), vec2<i32>(0, 0), max_coord);
|
||||
let p11 = clamp(pixel_floor + vec2<i32>(1, 1), vec2<i32>(0, 0), max_coord);
|
||||
|
||||
// Load at pixel centers
|
||||
let tl = textureLoad(r_tex_color, p00, 0);
|
||||
let tr = textureLoad(r_tex_color, p10, 0);
|
||||
let bl = textureLoad(r_tex_color, p01, 0);
|
||||
let br = textureLoad(r_tex_color, p11, 0);
|
||||
|
||||
// Manual bilinear interpolation
|
||||
let top = mix(tl, tr, pixel_fract.x);
|
||||
let bottom = mix(bl, br, pixel_fract.x);
|
||||
return mix(top, bottom, pixel_fract.y);
|
||||
}
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main_linear_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We expect "normal" textures that are NOT sRGB-aware.
|
||||
let tex_gamma = sample_texture(in);
|
||||
var out_color_gamma = in.color * tex_gamma;
|
||||
// Dither the float color down to eight bits to reduce banding.
|
||||
// This step is optional for egui backends.
|
||||
// Note that dithering is performed on the gamma encoded values,
|
||||
// because this function is used together with a srgb converting target.
|
||||
if r_locals.dithering == 1 {
|
||||
let out_color_gamma_rgb = dither_interleaved(out_color_gamma.rgb, 256.0, in.position);
|
||||
out_color_gamma = vec4<f32>(out_color_gamma_rgb, out_color_gamma.a);
|
||||
}
|
||||
let out_color_linear = linear_from_gamma_rgb(out_color_gamma.rgb);
|
||||
return vec4<f32>(out_color_linear, out_color_gamma.a);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main_gamma_framebuffer(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
// We expect "normal" textures that are NOT sRGB-aware.
|
||||
let tex_gamma = sample_texture(in);
|
||||
var out_color_gamma = in.color * tex_gamma;
|
||||
// Dither the float color down to eight bits to reduce banding.
|
||||
// This step is optional for egui backends.
|
||||
if r_locals.dithering == 1 {
|
||||
let out_color_gamma_rgb = dither_interleaved(out_color_gamma.rgb, 256.0, in.position);
|
||||
out_color_gamma = vec4<f32>(out_color_gamma_rgb, out_color_gamma.a);
|
||||
}
|
||||
return out_color_gamma;
|
||||
}
|
||||
Vendored
+585
@@ -0,0 +1,585 @@
|
||||
//! This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [wgpu](https://crates.io/crates/wgpu).
|
||||
//!
|
||||
//! If you're targeting WebGL you also need to turn on the
|
||||
//! `webgl` feature of the `wgpu` crate:
|
||||
//!
|
||||
//! ```toml
|
||||
//! # Enable both WebGL and WebGPU backends on web.
|
||||
//! wgpu = { version = "*", features = ["webgpu", "webgl"] }
|
||||
//! ```
|
||||
//!
|
||||
//! 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
|
||||
#![doc = document_features::document_features!()]
|
||||
//!
|
||||
|
||||
pub use wgpu;
|
||||
|
||||
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
|
||||
mod renderer;
|
||||
|
||||
mod setup;
|
||||
|
||||
pub use renderer::*;
|
||||
pub use setup::{
|
||||
EguiDisplayHandle, NativeAdapterSelectorMethod, WgpuSetup, WgpuSetupCreateNew,
|
||||
WgpuSetupExisting,
|
||||
};
|
||||
|
||||
/// Helpers for capturing screenshots of the UI.
|
||||
#[cfg(feature = "capture")]
|
||||
pub mod capture;
|
||||
|
||||
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
|
||||
#[cfg(feature = "winit")]
|
||||
pub mod winit;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use epaint::mutex::RwLock;
|
||||
|
||||
/// An error produced by egui-wgpu.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum WgpuError {
|
||||
#[error(transparent)]
|
||||
RequestAdapterError(#[from] wgpu::RequestAdapterError),
|
||||
|
||||
#[error("Adapter selection failed: {0}")]
|
||||
CustomNativeAdapterSelectionError(String),
|
||||
|
||||
#[error("There was no valid format for the surface at all.")]
|
||||
NoSurfaceFormatsAvailable,
|
||||
|
||||
#[error(transparent)]
|
||||
RequestDeviceError(#[from] wgpu::RequestDeviceError),
|
||||
|
||||
#[error(transparent)]
|
||||
CreateSurfaceError(#[from] wgpu::CreateSurfaceError),
|
||||
|
||||
#[cfg(feature = "winit")]
|
||||
#[error(transparent)]
|
||||
HandleError(#[from] ::winit::raw_window_handle::HandleError),
|
||||
}
|
||||
|
||||
/// Runtime-mutable subset of [`WgpuConfiguration`].
|
||||
///
|
||||
/// Edit any field to have the surface reconfigured on the next paint.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct SurfaceConfig {
|
||||
/// Present mode used for the primary surface.
|
||||
pub present_mode: wgpu::PresentMode,
|
||||
|
||||
/// Desired maximum number of frames that the presentation engine should queue in advance.
|
||||
///
|
||||
/// Use `1` for low-latency, and `2` for high-throughput.
|
||||
///
|
||||
/// See [`wgpu::SurfaceConfiguration::desired_maximum_frame_latency`] for details.
|
||||
///
|
||||
/// `None` => Let `wgpu` pick a default (currently `2`).
|
||||
pub desired_maximum_frame_latency: Option<u32>,
|
||||
}
|
||||
|
||||
impl SurfaceConfig {
|
||||
/// Good default for GUIs with very little (or no) extra GPU work.
|
||||
pub const LOW_LATENCY: Self = Self {
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
|
||||
desired_maximum_frame_latency: if cfg!(target_os = "ios") {
|
||||
None // The default is good on iOS, while `Some(1)` cuts FPS in half
|
||||
} else {
|
||||
Some(1)
|
||||
},
|
||||
};
|
||||
|
||||
/// Good default for GUIs with a lot of extra GPU work,
|
||||
/// or that want to prioritize smoothness over latency.
|
||||
pub const HIGH_THROUGHPUT: Self = Self {
|
||||
present_mode: wgpu::PresentMode::AutoVsync,
|
||||
desired_maximum_frame_latency: Some(2), // High-throughput.
|
||||
};
|
||||
}
|
||||
|
||||
/// Access to the render state for egui.
|
||||
#[derive(Clone)]
|
||||
pub struct RenderState {
|
||||
/// Wgpu adapter used for rendering.
|
||||
pub adapter: wgpu::Adapter,
|
||||
|
||||
/// All the available adapters.
|
||||
///
|
||||
/// This is not available on web.
|
||||
/// On web, we always select WebGPU is available, then fall back to WebGL if not.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub available_adapters: Vec<wgpu::Adapter>,
|
||||
|
||||
/// Wgpu instance used for creating surfaces and adapters.
|
||||
pub instance: wgpu::Instance,
|
||||
|
||||
/// Wgpu device used for rendering, created from the adapter.
|
||||
pub device: wgpu::Device,
|
||||
|
||||
/// Wgpu queue used for rendering, created from the adapter.
|
||||
pub queue: wgpu::Queue,
|
||||
|
||||
/// The target texture format used for presenting to the window.
|
||||
pub target_format: wgpu::TextureFormat,
|
||||
|
||||
/// Effective sample count after backend compatibility handling.
|
||||
/// Custom paint callbacks must use this, rather than the requested setting.
|
||||
pub msaa_samples: u32,
|
||||
|
||||
/// Egui renderer responsible for drawing the UI.
|
||||
pub renderer: Arc<RwLock<Renderer>>,
|
||||
|
||||
/// Runtime-mutable subset of the wgpu configuration.
|
||||
///
|
||||
/// Update this to have the surface reconfigured on the next paint.
|
||||
pub surface_config: SurfaceConfig,
|
||||
}
|
||||
|
||||
async fn request_adapter(
|
||||
instance: &wgpu::Instance,
|
||||
power_preference: wgpu::PowerPreference,
|
||||
compatible_surface: Option<&wgpu::Surface<'_>>,
|
||||
available_adapters: &[wgpu::Adapter],
|
||||
) -> Result<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,
|
||||
apply_limit_buckets: false,
|
||||
})
|
||||
.await
|
||||
.inspect_err(|_err| {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
// Nothing to add here
|
||||
} else if available_adapters.is_empty() {
|
||||
if std::env::var("DYLD_LIBRARY_PATH").is_ok() {
|
||||
// DYLD_LIBRARY_PATH can sometimes lead to loading dylibs that cause
|
||||
// us to find zero adapters. Very strange.
|
||||
// I don't want to debug this again.
|
||||
// See https://github.com/rerun-io/rerun/issues/11351 for more
|
||||
log::warn!(
|
||||
"No wgpu adapter found. This could be because DYLD_LIBRARY_PATH causes dylibs to be loaded that interfere with Metal device creation. Try restarting with DYLD_LIBRARY_PATH=''"
|
||||
);
|
||||
} else {
|
||||
log::info!("No wgpu adapter 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)
|
||||
);
|
||||
}
|
||||
})?;
|
||||
|
||||
if 1 < available_adapters.len() {
|
||||
log::info!(
|
||||
"There are {} available wgpu adapters: {}",
|
||||
available_adapters.len(),
|
||||
describe_adapters(available_adapters)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(adapter)
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
/// Creates a new [`RenderState`], containing everything needed for drawing egui with wgpu.
|
||||
///
|
||||
/// # Errors
|
||||
/// Wgpu initialization may fail due to incompatible hardware or driver for a given config.
|
||||
pub async fn create(
|
||||
config: &WgpuConfiguration,
|
||||
instance: &wgpu::Instance,
|
||||
compatible_surface: Option<&wgpu::Surface<'static>>,
|
||||
options: RendererOptions,
|
||||
) -> Result<Self, WgpuError> {
|
||||
profiling::scope!("RenderState::create"); // async yield give bad names using `profile_function`
|
||||
|
||||
// This is always an empty list on web.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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).await
|
||||
};
|
||||
|
||||
let (instance, 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,
|
||||
}) => {
|
||||
let adapter = {
|
||||
#[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::CustomNativeAdapterSelectionError)
|
||||
} else {
|
||||
request_adapter(
|
||||
instance,
|
||||
power_preference,
|
||||
compatible_surface,
|
||||
&available_adapters,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}?;
|
||||
|
||||
let (device, queue) = {
|
||||
profiling::scope!("request_device");
|
||||
adapter
|
||||
.request_device(&(*device_descriptor)(&adapter))
|
||||
.await?
|
||||
};
|
||||
|
||||
(instance.clone(), adapter, device, queue)
|
||||
}
|
||||
WgpuSetup::Existing(WgpuSetupExisting {
|
||||
instance,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
}) => (instance, adapter, device, queue),
|
||||
};
|
||||
|
||||
log_adapter_info(&adapter.get_info());
|
||||
|
||||
let surface_formats = {
|
||||
profiling::scope!("get_capabilities");
|
||||
compatible_surface.map_or_else(
|
||||
|| vec![wgpu::TextureFormat::Rgba8Unorm],
|
||||
|s| s.get_capabilities(&adapter).formats,
|
||||
)
|
||||
};
|
||||
let target_format = crate::preferred_framebuffer_format(&surface_formats)?;
|
||||
|
||||
// SolidCraft: the OpenGL resolve path can present an entirely black frame with MSAA
|
||||
// (notably Mesa/llvmpipe). Decide after selecting the real surface adapter,
|
||||
// before creating either pipelines or attachments; no second GPU probe.
|
||||
let options = RendererOptions {
|
||||
msaa_samples: effective_msaa_samples(adapter.get_info().backend, options.msaa_samples),
|
||||
..options
|
||||
};
|
||||
let renderer = Renderer::new(&device, target_format, options);
|
||||
|
||||
// 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::allow_attributes, clippy::arc_with_non_send_sync)] // For wasm
|
||||
Ok(Self {
|
||||
instance,
|
||||
adapter,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
available_adapters,
|
||||
device,
|
||||
queue,
|
||||
target_format,
|
||||
msaa_samples: options.msaa_samples,
|
||||
renderer: Arc::new(RwLock::new(renderer)),
|
||||
surface_config: config.surface,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn describe_adapters(adapters: &[wgpu::Adapter]) -> String {
|
||||
if adapters.is_empty() {
|
||||
"(none)".to_owned()
|
||||
} else if adapters.len() == 1 {
|
||||
adapter_info_summary(&adapters[0].get_info())
|
||||
} else {
|
||||
adapters
|
||||
.iter()
|
||||
.map(|a| format!("{{{}}}", adapter_info_summary(&a.get_info())))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies which action should be taken as consequence of a surface error.
|
||||
pub enum SurfaceErrorAction {
|
||||
/// Do nothing and skip the current frame.
|
||||
SkipFrame,
|
||||
|
||||
/// Reconfigure the existing surface, then skip the current frame.
|
||||
///
|
||||
/// Calls [`wgpu::Surface::configure`] on the current surface object.
|
||||
/// Use for [`wgpu::CurrentSurfaceTexture::Outdated`].
|
||||
Reconfigure,
|
||||
|
||||
/// Drop the surface, create a new one via [`wgpu::Instance::create_surface`], configure it,
|
||||
/// then skip the current frame.
|
||||
///
|
||||
/// Use for [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the same surface
|
||||
/// object cannot recover.
|
||||
RecreateSurface,
|
||||
}
|
||||
|
||||
/// Configuration for using wgpu with eframe or the egui-wgpu winit feature.
|
||||
#[derive(Clone)]
|
||||
pub struct WgpuConfiguration {
|
||||
/// Runtime-mutable configuration for the surface (present mode, frame latency).
|
||||
///
|
||||
/// These are the fields exposed via [`RenderState::surface_config`] for live
|
||||
/// reconfiguration at runtime.
|
||||
pub surface: SurfaceConfig,
|
||||
|
||||
/// How to create the wgpu adapter & device
|
||||
pub wgpu_setup: WgpuSetup,
|
||||
|
||||
/// Callback for surface status changes.
|
||||
///
|
||||
/// Called with the [`wgpu::CurrentSurfaceTexture`] result whenever acquiring a frame
|
||||
/// does not return [`wgpu::CurrentSurfaceTexture::Success`]. For
|
||||
/// [`wgpu::CurrentSurfaceTexture::Suboptimal`], egui uses the frame as-is and
|
||||
/// defers surface reconfiguration to the next frame — the callback is not invoked
|
||||
/// in that case either.
|
||||
pub on_surface_status:
|
||||
Arc<dyn Fn(&wgpu::CurrentSurfaceTexture) -> SurfaceErrorAction + Send + Sync>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wgpu_config_impl_send_sync() {
|
||||
fn assert_send_sync<T: Send + Sync>() {}
|
||||
assert_send_sync::<WgpuConfiguration>();
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for WgpuConfiguration {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self {
|
||||
surface,
|
||||
wgpu_setup,
|
||||
on_surface_status: _,
|
||||
} = self;
|
||||
f.debug_struct("WgpuConfiguration")
|
||||
.field("surface", &surface)
|
||||
.field("wgpu_setup", &wgpu_setup)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl WgpuConfiguration {
|
||||
#[inline]
|
||||
pub fn with_surface_config(mut self, surface_config: SurfaceConfig) -> Self {
|
||||
self.surface = surface_config;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WgpuConfiguration {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
surface: SurfaceConfig::HIGH_THROUGHPUT,
|
||||
|
||||
// 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_status: Arc::new(|status| match status {
|
||||
wgpu::CurrentSurfaceTexture::Outdated => {
|
||||
// The compositor changed the surface (resize, scale, output, …). wgpu
|
||||
// requires us to reconfigure before the next acquire. Skipping would mean
|
||||
// we are stuck in `Outdated` forever.
|
||||
log::trace!("Dropped frame with error: {status:?}");
|
||||
SurfaceErrorAction::Reconfigure
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Lost => {
|
||||
// The underlying surface is gone and we need a fresh one from the `wgpu::Instance`.
|
||||
log::debug!("Dropped frame with error: {status:?}");
|
||||
SurfaceErrorAction::RecreateSurface
|
||||
}
|
||||
wgpu::CurrentSurfaceTexture::Occluded => {
|
||||
// App is hidden (minimized / behind another window). Skip silently.
|
||||
log::trace!("Skipping frame due to occlusion.");
|
||||
SurfaceErrorAction::SkipFrame
|
||||
}
|
||||
_ => {
|
||||
log::warn!("Dropped frame with error: {status:?}");
|
||||
SurfaceErrorAction::SkipFrame
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the framebuffer format that egui prefers
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`WgpuError::NoSurfaceFormatsAvailable`] if the given list of formats is empty.
|
||||
pub fn preferred_framebuffer_format(
|
||||
formats: &[wgpu::TextureFormat],
|
||||
) -> Result<wgpu::TextureFormat, WgpuError> {
|
||||
for &format in formats {
|
||||
if matches!(
|
||||
format,
|
||||
wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Bgra8Unorm
|
||||
) {
|
||||
return Ok(format);
|
||||
}
|
||||
}
|
||||
|
||||
formats
|
||||
.first()
|
||||
.copied()
|
||||
.ok_or(WgpuError::NoSurfaceFormatsAvailable)
|
||||
}
|
||||
|
||||
/// Take's epi's depth/stencil bits and returns the corresponding wgpu format.
|
||||
pub fn depth_format_from_bits(depth_buffer: u8, stencil_buffer: u8) -> Option<wgpu::TextureFormat> {
|
||||
match (depth_buffer, stencil_buffer) {
|
||||
(0, 8) => Some(wgpu::TextureFormat::Stencil8),
|
||||
(16, 0) => Some(wgpu::TextureFormat::Depth16Unorm),
|
||||
(24, 0) => Some(wgpu::TextureFormat::Depth24Plus),
|
||||
(24, 8) => Some(wgpu::TextureFormat::Depth24PlusStencil8),
|
||||
(32, 0) => Some(wgpu::TextureFormat::Depth32Float),
|
||||
(32, 8) => Some(wgpu::TextureFormat::Depth32FloatStencil8),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn log_adapter_info(info: &wgpu::AdapterInfo) {
|
||||
let summary = adapter_info_summary(info);
|
||||
|
||||
let is_test = cfg!(test); // Software rasterizers are expected (and preferred) during testing!
|
||||
|
||||
if info.device_type == wgpu::DeviceType::Cpu && !is_test {
|
||||
log::warn!("Software rasterizer detected - loss of performance expected. {summary}");
|
||||
} else {
|
||||
log::debug!("wgpu adapter: {summary}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A human-readable summary about an adapter
|
||||
pub fn adapter_info_summary(info: &wgpu::AdapterInfo) -> String {
|
||||
let wgpu::AdapterInfo {
|
||||
name,
|
||||
vendor,
|
||||
device,
|
||||
device_type,
|
||||
driver,
|
||||
driver_info,
|
||||
backend,
|
||||
device_pci_bus_id,
|
||||
subgroup_min_size,
|
||||
subgroup_max_size,
|
||||
transient_saves_memory,
|
||||
limit_bucket,
|
||||
} = &info;
|
||||
|
||||
// Example values:
|
||||
// > name: "llvmpipe (LLVM 16.0.6, 256 bits)", device_type: Cpu, backend: Vulkan, driver: "llvmpipe", driver_info: "Mesa 23.1.6-arch1.4 (LLVM 16.0.6)"
|
||||
// > name: "Apple M1 Pro", device_type: IntegratedGpu, backend: Metal, driver: "", driver_info: ""
|
||||
// > name: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)", device_type: IntegratedGpu, backend: Gl, driver: "", driver_info: ""
|
||||
|
||||
use core::fmt::Write as _;
|
||||
|
||||
let mut summary = format!("backend: {backend:?}, device_type: {device_type:?}");
|
||||
|
||||
if !name.is_empty() {
|
||||
write!(summary, ", name: {name:?}").ok();
|
||||
}
|
||||
if !driver.is_empty() {
|
||||
write!(summary, ", driver: {driver:?}").ok();
|
||||
}
|
||||
if !driver_info.is_empty() {
|
||||
write!(summary, ", driver_info: {driver_info:?}").ok();
|
||||
}
|
||||
if *vendor != 0 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
write!(
|
||||
summary,
|
||||
", vendor: {} (0x{vendor:04X})",
|
||||
parse_vendor_id(*vendor)
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
write!(summary, ", vendor: 0x{vendor:04X}").ok();
|
||||
}
|
||||
}
|
||||
if *device != 0 {
|
||||
write!(summary, ", device: 0x{device:02X}").ok();
|
||||
}
|
||||
if !device_pci_bus_id.is_empty() {
|
||||
write!(summary, ", pci_bus_id: {device_pci_bus_id:?}").ok();
|
||||
}
|
||||
if *subgroup_min_size != 0 || *subgroup_max_size != 0 {
|
||||
write!(
|
||||
summary,
|
||||
", subgroup_size: {subgroup_min_size}..={subgroup_max_size}"
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
write!(
|
||||
summary,
|
||||
", transient_saves_memory: {transient_saves_memory:?}"
|
||||
)
|
||||
.ok();
|
||||
write!(summary, ", limit_bucket: {limit_bucket:?}").ok();
|
||||
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
/// Selected-adapter policy shared by egui pipelines, attachments and host callbacks.
|
||||
pub fn effective_msaa_samples(backend: wgpu::Backend, requested: u32) -> u32 {
|
||||
if backend == wgpu::Backend::Gl { 1 } else { requested.max(1) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod msaa_tests {
|
||||
#[test]
|
||||
fn gl_saved_multisampling_is_clamped_and_native_backends_keep_it() {
|
||||
for requested in [0, 1, 2, 4, 8] {
|
||||
assert_eq!(super::effective_msaa_samples(wgpu::Backend::Gl, requested), 1);
|
||||
for backend in [wgpu::Backend::Vulkan, wgpu::Backend::Dx12, wgpu::Backend::Metal, wgpu::Backend::BrowserWebGpu] {
|
||||
assert_eq!(super::effective_msaa_samples(backend, requested), requested.max(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1178
File diff suppressed because it is too large.
Load diff
Vendored
+331
@@ -0,0 +1,331 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A cloneable display handle for use with [`wgpu::InstanceDescriptor`].
|
||||
///
|
||||
/// [`wgpu::InstanceDescriptor`] stores its display handle as a non-cloneable
|
||||
/// `Box<dyn WgpuHasDisplayHandle>`. This trait wraps it so it can be cloned
|
||||
/// alongside the rest of the egui wgpu configuration.
|
||||
///
|
||||
/// Automatically implemented for all types that satisfy the bounds
|
||||
/// (including [`winit::event_loop::OwnedDisplayHandle`]).
|
||||
pub trait EguiDisplayHandle:
|
||||
wgpu::rwh::HasDisplayHandle + core::fmt::Debug + Send + Sync + 'static
|
||||
{
|
||||
/// Clone into a `Box<dyn WgpuHasDisplayHandle>` for [`wgpu::InstanceDescriptor::display`].
|
||||
fn clone_for_wgpu(&self) -> Box<dyn wgpu::wgt::WgpuHasDisplayHandle>;
|
||||
|
||||
/// Clone into a new `Box<dyn EguiDisplayHandle>`.
|
||||
fn clone_display_handle(&self) -> Box<dyn EguiDisplayHandle>;
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn EguiDisplayHandle> {
|
||||
fn clone(&self) -> Self {
|
||||
// We need to deref here, otherwise this causes infinite recursion stack overflow.
|
||||
(**self).clone_display_handle()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EguiDisplayHandle for T
|
||||
where
|
||||
T: wgpu::rwh::HasDisplayHandle + Clone + core::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)]
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
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 WgpuSetup {
|
||||
/// Creates a new [`WgpuSetup::CreateNew`] with the given display handle.
|
||||
///
|
||||
/// See [`WgpuSetupCreateNew::from_display_handle`] for details.
|
||||
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.
|
||||
///
|
||||
/// See [`WgpuSetupCreateNew::without_display_handle`] for details.
|
||||
pub fn without_display_handle() -> Self {
|
||||
Self::CreateNew(WgpuSetupCreateNew::without_display_handle())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for WgpuSetup {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::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) -> wgpu::Instance {
|
||||
match self {
|
||||
Self::CreateNew(create_new) => {
|
||||
#[allow(clippy::allow_attributes, unused_mut)]
|
||||
let mut backends = create_new.instance_descriptor.backends;
|
||||
|
||||
// Don't try WebGPU if we're not in a secure context.
|
||||
// Emscripten is excluded: wgpu gates both its `webgpu` backend and
|
||||
// its `web_sys` re-export on `not(Emscripten)` (see the cfg aliases
|
||||
// in `wgpu/build.rs`).
|
||||
#[cfg(all(target_arch = "wasm32", not(target_os = "emscripten")))]
|
||||
if backends.contains(wgpu::Backends::BROWSER_WEBGPU) {
|
||||
let is_secure_context =
|
||||
wgpu::web_sys::window().is_some_and(|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:?}");
|
||||
let desc = &create_new.instance_descriptor;
|
||||
let descriptor = wgpu::InstanceDescriptor {
|
||||
backends: desc.backends,
|
||||
flags: desc.flags,
|
||||
backend_options: desc.backend_options.clone(),
|
||||
memory_budget_thresholds: desc.memory_budget_thresholds,
|
||||
display: create_new
|
||||
.display_handle
|
||||
.as_ref()
|
||||
.map(|handle| handle.clone_for_wgpu()),
|
||||
};
|
||||
wgpu::util::new_instance_with_webgpu_detection(descriptor).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.
|
||||
pub type NativeAdapterSelectorMethod = Arc<
|
||||
dyn Fn(&[wgpu::Adapter], Option<&wgpu::Surface<'_>>) -> Result<wgpu::Adapter, String>
|
||||
+ Send
|
||||
+ Sync,
|
||||
>;
|
||||
|
||||
/// Configuration for creating a new wgpu setup.
|
||||
///
|
||||
/// Used for [`WgpuSetup::CreateNew`].
|
||||
///
|
||||
/// Prefer [`Self::from_display_handle`] when you have a display handle available.
|
||||
/// Most platforms work without one, but some (e.g. Wayland with GLES, or WebGL)
|
||||
/// require it, so providing one ensures maximum compatibility.
|
||||
/// With winit, pass [`EventLoop::owned_display_handle`](winit::event_loop::EventLoop::owned_display_handle).
|
||||
///
|
||||
/// Note: The display handle is stored in [`Self::display_handle`] rather than in
|
||||
/// [`Self::instance_descriptor`] so the config can be cloned
|
||||
/// ([`wgpu::InstanceDescriptor`] is not `Clone`). It is injected at instance creation time.
|
||||
pub struct WgpuSetupCreateNew {
|
||||
/// Descriptor for the wgpu instance.
|
||||
///
|
||||
/// Leave [`wgpu::InstanceDescriptor::display`] as `None` — use [`Self::display_handle`]
|
||||
/// instead (injected at instance creation time).
|
||||
///
|
||||
/// The most important field is [`wgpu::InstanceDescriptor::backends`], which controls
|
||||
/// which backends are supported (wgpu will pick one of these). For example, set it to
|
||||
/// [`wgpu::Backends::GL`] to use only WebGL. By default on web, WebGPU is preferred
|
||||
/// with WebGL as a fallback (requires the `webgl` feature of crate `wgpu`).
|
||||
pub instance_descriptor: wgpu::InstanceDescriptor,
|
||||
|
||||
/// Display handle passed to wgpu at instance creation time.
|
||||
///
|
||||
/// Required on some platforms (e.g. Wayland with GLES, WebGL); optional elsewhere.
|
||||
/// With winit, use [`winit::event_loop::OwnedDisplayHandle`].
|
||||
///
|
||||
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
|
||||
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,
|
||||
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
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 {
|
||||
display_handle: Some(Box::new(display_handle)),
|
||||
..Self::without_display_handle()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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).
|
||||
///
|
||||
/// `eframe` 's winit & web integrations will attempt to fill the display handle automatically if it is left empty.
|
||||
pub fn without_display_handle() -> 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::Backends::from_env()
|
||||
.unwrap_or(wgpu::Backends::PRIMARY | wgpu::Backends::GL),
|
||||
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),
|
||||
|
||||
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_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
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 core::fmt::Debug for WgpuSetupCreateNew {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
let Self {
|
||||
instance_descriptor,
|
||||
display_handle,
|
||||
power_preference,
|
||||
native_adapter_selector,
|
||||
device_descriptor: _,
|
||||
} = self;
|
||||
f.debug_struct("WgpuSetupCreateNew")
|
||||
.field("instance_descriptor", instance_descriptor)
|
||||
.field("display_handle", display_handle)
|
||||
.field("power_preference", power_preference)
|
||||
.field(
|
||||
"native_adapter_selector",
|
||||
&native_adapter_selector.is_some(),
|
||||
)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for using an existing wgpu setup.
|
||||
///
|
||||
/// Used for [`WgpuSetup::Existing`].
|
||||
#[derive(Clone)]
|
||||
pub struct WgpuSetupExisting {
|
||||
pub instance: wgpu::Instance,
|
||||
pub adapter: wgpu::Adapter,
|
||||
pub device: wgpu::Device,
|
||||
pub queue: wgpu::Queue,
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
};
|
||||
|
||||
var<private> positions: array<vec2f, 3> = array<vec2f, 3>(
|
||||
vec2f(-1.0, -3.0),
|
||||
vec2f(-1.0, 1.0),
|
||||
vec2f(3.0, 1.0)
|
||||
);
|
||||
|
||||
// meant to be called with 3 vertex indices: 0, 1, 2
|
||||
// draws one large triangle over the clip space like this:
|
||||
// (the asterisks represent the clip space bounds)
|
||||
//-1,1 1,1
|
||||
// ---------------------------------
|
||||
// | * .
|
||||
// | * .
|
||||
// | * .
|
||||
// | * .
|
||||
// | * .
|
||||
// | * .
|
||||
// |***************
|
||||
// | . 1,-1
|
||||
// | .
|
||||
// | .
|
||||
// | .
|
||||
// | .
|
||||
// |.
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||
var result: VertexOutput;
|
||||
result.position = vec4f(positions[vertex_index], 0.0, 1.0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@group(0)
|
||||
@binding(0)
|
||||
var r_color: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
fn fs_main(vertex: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return textureLoad(r_color, vec2i(vertex.position.xy), 0);
|
||||
}
|
||||
Vendored
+802
@@ -0,0 +1,802 @@
|
||||
#![expect(clippy::missing_errors_doc)]
|
||||
#![expect(clippy::undocumented_unsafe_blocks)]
|
||||
#![expect(clippy::unwrap_used)] // TODO(emilk): avoid unwraps
|
||||
#![expect(unsafe_code)]
|
||||
|
||||
use crate::{RenderState, SurfaceConfig, SurfaceErrorAction, WgpuConfiguration, renderer};
|
||||
use crate::{
|
||||
RendererOptions,
|
||||
capture::{CaptureReceiver, CaptureSender, CaptureState, capture_channel},
|
||||
};
|
||||
use core::num::NonZeroU32;
|
||||
use egui::{Context, Event, UserData, ViewportId, ViewportIdMap, ViewportIdSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct SurfaceState {
|
||||
surface: wgpu::Surface<'static>,
|
||||
alpha_mode: wgpu::CompositeAlphaMode,
|
||||
width: u32,
|
||||
height: u32,
|
||||
resizing: bool,
|
||||
needs_reconfigure: bool,
|
||||
needs_recreate: bool,
|
||||
}
|
||||
|
||||
/// Everything you need to paint egui with [`wgpu`] on [`winit`].
|
||||
///
|
||||
/// Alternatively you can use [`crate::Renderer`] directly.
|
||||
///
|
||||
/// NOTE: all egui viewports share the same painter.
|
||||
pub struct Painter {
|
||||
context: Context,
|
||||
config: WgpuConfiguration,
|
||||
options: RendererOptions,
|
||||
support_transparent_backbuffer: bool,
|
||||
screen_capture_state: Option<CaptureState>,
|
||||
|
||||
instance: wgpu::Instance,
|
||||
render_state: Option<RenderState>,
|
||||
|
||||
// Per viewport/window:
|
||||
depth_texture_view: ViewportIdMap<wgpu::TextureView>,
|
||||
msaa_texture_view: ViewportIdMap<wgpu::TextureView>,
|
||||
surfaces: ViewportIdMap<SurfaceState>,
|
||||
capture_tx: CaptureSender,
|
||||
capture_rx: CaptureReceiver,
|
||||
}
|
||||
|
||||
impl Painter {
|
||||
/// Manages [`wgpu`] state, including surface state, required to render egui.
|
||||
///
|
||||
/// Only the [`wgpu::Instance`] is initialized here. Device selection and the initialization
|
||||
/// of render + surface state is deferred until the painter is given its first window target
|
||||
/// via [`set_window()`](Self::set_window). (Ensuring that a device that's compatible with the
|
||||
/// native window is chosen)
|
||||
///
|
||||
/// Before calling [`paint_and_update_textures()`](Self::paint_and_update_textures) a
|
||||
/// [`wgpu::Surface`] must be initialized (and corresponding render state) by calling
|
||||
/// [`set_window()`](Self::set_window) once you have
|
||||
/// a [`winit::window::Window`] with a valid `.raw_window_handle()`
|
||||
/// associated.
|
||||
pub async fn new(
|
||||
context: Context,
|
||||
config: WgpuConfiguration,
|
||||
support_transparent_backbuffer: bool,
|
||||
options: RendererOptions,
|
||||
) -> Self {
|
||||
let (capture_tx, capture_rx) = capture_channel();
|
||||
let instance = config.wgpu_setup.new_instance().await;
|
||||
|
||||
Self {
|
||||
context,
|
||||
config,
|
||||
options,
|
||||
support_transparent_backbuffer,
|
||||
screen_capture_state: None,
|
||||
|
||||
instance,
|
||||
render_state: None,
|
||||
|
||||
depth_texture_view: Default::default(),
|
||||
surfaces: Default::default(),
|
||||
msaa_texture_view: Default::default(),
|
||||
|
||||
capture_tx,
|
||||
capture_rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the [`RenderState`].
|
||||
///
|
||||
/// Will return [`None`] if the render state has not been initialized yet.
|
||||
pub fn render_state(&self) -> Option<RenderState> {
|
||||
self.render_state.clone()
|
||||
}
|
||||
|
||||
fn configure_surface(
|
||||
surface_state: &SurfaceState,
|
||||
render_state: &RenderState,
|
||||
config: &SurfaceConfig,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let SurfaceConfig {
|
||||
present_mode,
|
||||
desired_maximum_frame_latency,
|
||||
} = *config;
|
||||
|
||||
// Transaction presentation can hold a drawable during AppKit live resize. Keep the
|
||||
// configured low-latency path normally, but use three Metal drawables while resizing.
|
||||
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
|
||||
let desired_maximum_frame_latency = if surface_state.resizing {
|
||||
Some(desired_maximum_frame_latency.unwrap_or(2).max(2))
|
||||
} else {
|
||||
desired_maximum_frame_latency
|
||||
};
|
||||
|
||||
let width = surface_state.width;
|
||||
let height = surface_state.height;
|
||||
|
||||
let mut surf_config = wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
format: render_state.target_format,
|
||||
present_mode,
|
||||
alpha_mode: surface_state.alpha_mode,
|
||||
view_formats: vec![render_state.target_format],
|
||||
..surface_state
|
||||
.surface
|
||||
.get_default_config(&render_state.adapter, width, height)
|
||||
.expect("The surface isn't supported by this adapter")
|
||||
};
|
||||
|
||||
if let Some(desired_maximum_frame_latency) = desired_maximum_frame_latency {
|
||||
surf_config.desired_maximum_frame_latency = desired_maximum_frame_latency;
|
||||
}
|
||||
|
||||
surface_state
|
||||
.surface
|
||||
.configure(&render_state.device, &surf_config);
|
||||
}
|
||||
|
||||
/// Drop the existing [`wgpu::Surface`] for `viewport_id` and create a fresh one for the
|
||||
/// given window via [`wgpu::Instance::create_surface`], then configure it.
|
||||
///
|
||||
/// Used to recover from [`wgpu::CurrentSurfaceTexture::Lost`], where reconfiguring the
|
||||
/// existing surface object cannot recover.
|
||||
fn recreate_surface(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
window: &Arc<winit::window::Window>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
profiling::function_scope!();
|
||||
|
||||
let Some(old_state) = self.surfaces.remove(&viewport_id) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let surface = self.instance.create_surface(Arc::clone(window))?;
|
||||
self.install_surface(
|
||||
surface,
|
||||
viewport_id,
|
||||
old_state.width,
|
||||
old_state.height,
|
||||
old_state.resizing,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`]
|
||||
///
|
||||
/// This creates a [`wgpu::Surface`] for the given Window (as well as initializing render
|
||||
/// state if needed) that is used for egui rendering.
|
||||
///
|
||||
/// This must be called before trying to render via
|
||||
/// [`paint_and_update_textures`](Self::paint_and_update_textures)
|
||||
///
|
||||
/// # Portability
|
||||
///
|
||||
/// _In particular it's important to note that on Android a it's only possible to create
|
||||
/// a window surface between `Resumed` and `Paused` lifecycle events, and Winit will panic on
|
||||
/// attempts to query the raw window handle while paused._
|
||||
///
|
||||
/// On Android [`set_window`](Self::set_window) should be called with `Some(window)` for each
|
||||
/// `Resumed` event and `None` for each `Paused` event. Currently, on all other platforms
|
||||
/// [`set_window`](Self::set_window) may be called with `Some(window)` as soon as you have a
|
||||
/// valid [`winit::window::Window`].
|
||||
///
|
||||
/// # Errors
|
||||
/// If the provided wgpu configuration does not match an available device.
|
||||
pub async fn set_window(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
window: Option<Arc<winit::window::Window>>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
profiling::scope!("Painter::set_window"); // profile_function gives bad names for async functions
|
||||
|
||||
if let Some(window) = window {
|
||||
let size = window.inner_size();
|
||||
if !self.surfaces.contains_key(&viewport_id) {
|
||||
let surface = self.instance.create_surface(window)?;
|
||||
self.add_surface(surface, viewport_id, size).await?;
|
||||
}
|
||||
} else {
|
||||
log::warn!("No window - clearing all surfaces");
|
||||
self.surfaces.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates (or clears) the [`winit::window::Window`] associated with the [`Painter`] without taking ownership of the window.
|
||||
///
|
||||
/// Like [`set_window`](Self::set_window) except:
|
||||
///
|
||||
/// # Safety
|
||||
/// The user is responsible for ensuring that the window is alive for as long as it is set.
|
||||
pub async unsafe fn set_window_unsafe(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
window: Option<&winit::window::Window>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
profiling::scope!("Painter::set_window_unsafe"); // profile_function gives bad names for async functions
|
||||
|
||||
if let Some(window) = window {
|
||||
let size = window.inner_size();
|
||||
if !self.surfaces.contains_key(&viewport_id) {
|
||||
let surface = unsafe {
|
||||
self.instance
|
||||
.create_surface_unsafe(wgpu::SurfaceTargetUnsafe::from_window(&window)?)?
|
||||
};
|
||||
self.add_surface(surface, viewport_id, size).await?;
|
||||
}
|
||||
} else {
|
||||
log::warn!("No window - clearing all surfaces");
|
||||
self.surfaces.clear();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_surface(
|
||||
&mut self,
|
||||
surface: wgpu::Surface<'static>,
|
||||
viewport_id: ViewportId,
|
||||
size: winit::dpi::PhysicalSize<u32>,
|
||||
) -> Result<(), crate::WgpuError> {
|
||||
if self.render_state.is_none() {
|
||||
let render_state =
|
||||
RenderState::create(&self.config, &self.instance, Some(&surface), self.options)
|
||||
.await?;
|
||||
// SolidCraft: attachment sample counts must match the backend-adjusted pipelines.
|
||||
self.options.msaa_samples = render_state.msaa_samples;
|
||||
self.render_state = Some(render_state);
|
||||
}
|
||||
self.install_surface(surface, viewport_id, size.width, size.height, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Inserts a freshly created surface into [`Self::surfaces`] and configures it.
|
||||
///
|
||||
/// Render state must already be initialised before calling this.
|
||||
// NOTE: The same assumption is already required by `resize_and_generate_depth_texture_view_and_msaa_view`.
|
||||
fn install_surface(
|
||||
&mut self,
|
||||
surface: wgpu::Surface<'static>,
|
||||
viewport_id: ViewportId,
|
||||
width: u32,
|
||||
height: u32,
|
||||
resizing: bool,
|
||||
) {
|
||||
let alpha_mode = {
|
||||
// Panic: We use the same failure mode as `resize_and_generate_depth_texture_view_and_msaa_view`
|
||||
let render_state = self
|
||||
.render_state
|
||||
.as_ref()
|
||||
.expect("install_surface called before render_state initialization");
|
||||
if self.support_transparent_backbuffer {
|
||||
let supported_alpha_modes =
|
||||
surface.get_capabilities(&render_state.adapter).alpha_modes;
|
||||
// Prefer pre multiplied over post multiplied!
|
||||
if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PreMultiplied) {
|
||||
wgpu::CompositeAlphaMode::PreMultiplied
|
||||
} else if supported_alpha_modes.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
|
||||
{
|
||||
wgpu::CompositeAlphaMode::PostMultiplied
|
||||
} else {
|
||||
log::warn!(
|
||||
"Transparent window was requested, but the active wgpu surface does not support a `CompositeAlphaMode` with transparency."
|
||||
);
|
||||
wgpu::CompositeAlphaMode::Auto
|
||||
}
|
||||
} else {
|
||||
wgpu::CompositeAlphaMode::Auto
|
||||
}
|
||||
};
|
||||
self.surfaces.insert(
|
||||
viewport_id,
|
||||
SurfaceState {
|
||||
surface,
|
||||
width,
|
||||
height,
|
||||
alpha_mode,
|
||||
resizing,
|
||||
needs_reconfigure: false,
|
||||
needs_recreate: false,
|
||||
},
|
||||
);
|
||||
let Some(width) = NonZeroU32::new(width) else {
|
||||
log::debug!("The window width was zero; skipping generate textures");
|
||||
return;
|
||||
};
|
||||
let Some(height) = NonZeroU32::new(height) else {
|
||||
log::debug!("The window height was zero; skipping generate textures");
|
||||
return;
|
||||
};
|
||||
self.resize_and_generate_depth_texture_view_and_msaa_view(viewport_id, width, height);
|
||||
}
|
||||
|
||||
/// Returns the maximum texture dimension supported if known
|
||||
///
|
||||
/// This API will only return a known dimension after `set_window()` has been called
|
||||
/// at least once, since the underlying device and render state are initialized lazily
|
||||
/// once we have a window (that may determine the choice of adapter/device).
|
||||
pub fn max_texture_side(&self) -> Option<usize> {
|
||||
self.render_state
|
||||
.as_ref()
|
||||
.map(|rs| rs.device.limits().max_texture_dimension_2d as usize)
|
||||
}
|
||||
|
||||
fn resize_and_generate_depth_texture_view_and_msaa_view(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
width_in_pixels: NonZeroU32,
|
||||
height_in_pixels: NonZeroU32,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let width = width_in_pixels.get();
|
||||
let height = height_in_pixels.get();
|
||||
|
||||
let render_state = self.render_state.as_ref().unwrap();
|
||||
let surface_state = self.surfaces.get_mut(&viewport_id).unwrap();
|
||||
|
||||
surface_state.width = width;
|
||||
surface_state.height = height;
|
||||
|
||||
Self::configure_surface(surface_state, render_state, &self.config.surface);
|
||||
|
||||
if let Some(depth_format) = self.options.depth_stencil_format {
|
||||
self.depth_texture_view.insert(
|
||||
viewport_id,
|
||||
render_state
|
||||
.device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_depth_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: self.options.msaa_samples.max(1),
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: depth_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
||||
| wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[depth_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(render_state) = (self.options.msaa_samples > 1)
|
||||
.then_some(self.render_state.as_ref())
|
||||
.flatten()
|
||||
{
|
||||
let texture_format = render_state.target_format;
|
||||
self.msaa_texture_view.insert(
|
||||
viewport_id,
|
||||
render_state
|
||||
.device
|
||||
.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("egui_msaa_texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: self.options.msaa_samples.max(1),
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: texture_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[texture_format],
|
||||
})
|
||||
.create_view(&wgpu::TextureViewDescriptor::default()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles changes of the resizing state.
|
||||
///
|
||||
/// Should be called prior to the first [`Painter::on_window_resized`] call and after the last in
|
||||
/// the chain. Used to apply platform-specific logic, e.g. OSX Metal window resize jitter fix.
|
||||
pub fn on_window_resize_state_change(&mut self, viewport_id: ViewportId, resizing: bool) {
|
||||
profiling::function_scope!();
|
||||
|
||||
let Some(state) = self.surfaces.get_mut(&viewport_id) else {
|
||||
return;
|
||||
};
|
||||
if state.resizing == resizing {
|
||||
if resizing {
|
||||
log::debug!(
|
||||
"Painter::on_window_resize_state_change() redundant call while resizing"
|
||||
);
|
||||
} else {
|
||||
log::debug!(
|
||||
"Painter::on_window_resize_state_change() redundant call after resizing"
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Set before reconfiguring so macOS live resize uses the temporary latency bump above.
|
||||
state.resizing = resizing;
|
||||
|
||||
// Resizing is a bit tricky on macOS.
|
||||
// It requires enabling ["present_with_transaction"](https://developer.apple.com/documentation/quartzcore/cametallayer/presentswithtransaction)
|
||||
// flag to avoid jittering during the resize. Even though resize jittering on macOS
|
||||
// is common across rendering backends, the solution for wgpu/metal is known.
|
||||
//
|
||||
// See https://github.com/emilk/egui/issues/903
|
||||
#[cfg(all(target_os = "macos", feature = "macos-window-resize-jitter-fix"))]
|
||||
{
|
||||
// SAFETY: `as_hal::<Metal>()` returns `None` unless this surface is backed by wgpu's
|
||||
// Metal backend.
|
||||
unsafe {
|
||||
if let (Some(render_state), Some(hal_surface)) = (
|
||||
self.render_state.as_ref(),
|
||||
state.surface.as_hal::<wgpu::hal::api::Metal>(),
|
||||
) {
|
||||
hal_surface
|
||||
.render_layer()
|
||||
.lock()
|
||||
.setPresentsWithTransaction(resizing);
|
||||
|
||||
Self::configure_surface(state, render_state, &self.config.surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_window_resized(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
width_in_pixels: NonZeroU32,
|
||||
height_in_pixels: NonZeroU32,
|
||||
) {
|
||||
profiling::function_scope!();
|
||||
|
||||
if self.surfaces.contains_key(&viewport_id) {
|
||||
self.resize_and_generate_depth_texture_view_and_msaa_view(
|
||||
viewport_id,
|
||||
width_in_pixels,
|
||||
height_in_pixels,
|
||||
);
|
||||
} else {
|
||||
log::warn!(
|
||||
"Ignoring window resize notification with no surface created via Painter::set_window()"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns two things:
|
||||
///
|
||||
/// The approximate number of seconds spent on vsync-waiting (if any),
|
||||
/// and the captures captured screenshot if it was requested.
|
||||
///
|
||||
/// If `capture_data` isn't empty, a screenshot will be captured.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub fn paint_and_update_textures(
|
||||
&mut self,
|
||||
viewport_id: ViewportId,
|
||||
pixels_per_point: f32,
|
||||
clear_color: [f32; 4],
|
||||
clipped_primitives: &[epaint::ClippedPrimitive],
|
||||
textures_delta: &mut epaint::textures::TexturesDelta,
|
||||
capture_data: Vec<UserData>,
|
||||
window: &Arc<winit::window::Window>,
|
||||
) -> f32 {
|
||||
profiling::function_scope!();
|
||||
|
||||
/// Guard to ensure that commands are always submitted to the renderer queue
|
||||
/// so that calls to [`write_buffer()`](https://docs.rs/wgpu/latest/wgpu/struct.Queue.html#method.write_buffer)
|
||||
/// are completed even if we take a codepath which doesn't submit commands and avoids
|
||||
/// internal buffers growing indefinitely.
|
||||
///
|
||||
/// This may happen, for example, if no output frame is resolved.
|
||||
/// See <https://github.com/emilk/egui/pull/7928> for full context.
|
||||
struct RendererQueueGuard<'q> {
|
||||
queue: &'q wgpu::Queue,
|
||||
commands_submitted: bool,
|
||||
}
|
||||
|
||||
impl Drop for RendererQueueGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
// Only submit an empty command buffer array if no commands were
|
||||
// explicitly submitted.
|
||||
if !self.commands_submitted {
|
||||
self.queue.submit([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let capture = !capture_data.is_empty();
|
||||
let mut vsync_sec = 0.0;
|
||||
|
||||
// If the previous frame produced `CurrentSurfaceTexture::Lost`, the action match
|
||||
// below set `needs_recreate`. Recreate the surface now, before re-borrowing
|
||||
// `self.render_state` / `self.surfaces` for the rest of the paint.
|
||||
if self
|
||||
.surfaces
|
||||
.get(&viewport_id)
|
||||
.is_some_and(|s| s.needs_recreate)
|
||||
&& let Err(err) = self.recreate_surface(viewport_id, window)
|
||||
{
|
||||
log::error!("Failed to recreate surface for {viewport_id:?}: {err}");
|
||||
return vsync_sec;
|
||||
}
|
||||
|
||||
// Apply any runtime changes requested via `RenderState::surface_config`.
|
||||
// We diff against the already-applied values in `self.config.surface`
|
||||
// and, if anything differs, mark every surface as needing reconfiguration so
|
||||
// the existing `needs_reconfigure` pathway below picks them up.
|
||||
if let Some(render_state) = self.render_state.as_ref()
|
||||
&& render_state.surface_config != self.config.surface
|
||||
{
|
||||
self.config.surface = render_state.surface_config;
|
||||
#[expect(clippy::iter_over_hash_type)]
|
||||
for surface in self.surfaces.values_mut() {
|
||||
surface.needs_reconfigure = true;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(render_state) = self.render_state.as_mut() else {
|
||||
return vsync_sec;
|
||||
};
|
||||
|
||||
let mut render_queue_guard = RendererQueueGuard {
|
||||
queue: &render_state.queue,
|
||||
commands_submitted: false,
|
||||
};
|
||||
|
||||
let Some(surface_state) = self.surfaces.get_mut(&viewport_id) else {
|
||||
return vsync_sec;
|
||||
};
|
||||
|
||||
let mut encoder =
|
||||
render_state
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("encoder"),
|
||||
});
|
||||
|
||||
// Upload all resources for the GPU.
|
||||
let screen_descriptor = renderer::ScreenDescriptor {
|
||||
size_in_pixels: [surface_state.width, surface_state.height],
|
||||
pixels_per_point,
|
||||
};
|
||||
|
||||
let user_cmd_bufs = {
|
||||
let mut renderer = render_state.renderer.write();
|
||||
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||
for (id, image_deltas) in textures_delta.set.drain() {
|
||||
for image_delta in image_deltas {
|
||||
renderer.update_texture(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
id,
|
||||
&image_delta,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.update_buffers(
|
||||
&render_state.device,
|
||||
&render_state.queue,
|
||||
&mut encoder,
|
||||
clipped_primitives,
|
||||
&screen_descriptor,
|
||||
)
|
||||
};
|
||||
|
||||
if surface_state.needs_reconfigure {
|
||||
Self::configure_surface(surface_state, render_state, &self.config.surface);
|
||||
surface_state.needs_reconfigure = false;
|
||||
}
|
||||
|
||||
let output_frame = {
|
||||
profiling::scope!("get_current_texture");
|
||||
// This is what vsync-waiting happens on my Mac.
|
||||
let start = web_time::Instant::now();
|
||||
let output_frame = surface_state.surface.get_current_texture();
|
||||
vsync_sec += start.elapsed().as_secs_f32();
|
||||
output_frame
|
||||
};
|
||||
|
||||
let output_frame = match output_frame {
|
||||
wgpu::CurrentSurfaceTexture::Success(frame) => frame,
|
||||
wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
|
||||
surface_state.needs_reconfigure = true;
|
||||
frame
|
||||
}
|
||||
other => {
|
||||
match (*self.config.on_surface_status)(&other) {
|
||||
SurfaceErrorAction::Reconfigure => {
|
||||
Self::configure_surface(surface_state, render_state, &self.config.surface);
|
||||
self.context.request_repaint_of(viewport_id);
|
||||
}
|
||||
SurfaceErrorAction::RecreateSurface => {
|
||||
// Because of ownership, I could not find an easy way to do a full recovery here,
|
||||
// as that would involve dropping the old surface and creating a new one.
|
||||
// For now, we defer the recreation to the beginning of the next frame (which
|
||||
// we ensure to arrive via `request_repaint_of`). A cleaner solution would be
|
||||
// to untangle the ownership of `RenderState`.
|
||||
surface_state.needs_recreate = true;
|
||||
self.context.request_repaint_of(viewport_id);
|
||||
}
|
||||
SurfaceErrorAction::SkipFrame => {}
|
||||
}
|
||||
return vsync_sec;
|
||||
}
|
||||
};
|
||||
|
||||
let mut capture_buffer = None;
|
||||
{
|
||||
let renderer = render_state.renderer.read();
|
||||
|
||||
let target_texture = if capture {
|
||||
let capture_state = self.screen_capture_state.get_or_insert_with(|| {
|
||||
CaptureState::new(&render_state.device, &output_frame.texture)
|
||||
});
|
||||
capture_state.update(&render_state.device, &output_frame.texture);
|
||||
|
||||
&capture_state.texture
|
||||
} else {
|
||||
&output_frame.texture
|
||||
};
|
||||
let target_view = target_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
|
||||
let (view, resolve_target) = (self.options.msaa_samples > 1)
|
||||
.then_some(self.msaa_texture_view.get(&viewport_id))
|
||||
.flatten()
|
||||
.map_or((&target_view, None), |texture_view| {
|
||||
(texture_view, Some(&target_view))
|
||||
});
|
||||
|
||||
let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("egui_render"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
resolve_target,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: clear_color[0] as f64,
|
||||
g: clear_color[1] as f64,
|
||||
b: clear_color[2] as f64,
|
||||
a: clear_color[3] as f64,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: self.depth_texture_view.get(&viewport_id).map(|view| {
|
||||
wgpu::RenderPassDepthStencilAttachment {
|
||||
view,
|
||||
depth_ops: self
|
||||
.options
|
||||
.depth_stencil_format
|
||||
.is_some_and(|depth_stencil_format| {
|
||||
depth_stencil_format.has_depth_aspect()
|
||||
})
|
||||
.then_some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
// It is very unlikely that the depth buffer is needed after egui finished rendering
|
||||
// so no need to store it. (this can improve performance on tiling GPUs like mobile chips or Apple Silicon)
|
||||
store: wgpu::StoreOp::Discard,
|
||||
}),
|
||||
stencil_ops: self
|
||||
.options
|
||||
.depth_stencil_format
|
||||
.is_some_and(|depth_stencil_format| {
|
||||
depth_stencil_format.has_stencil_aspect()
|
||||
})
|
||||
.then_some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(0),
|
||||
store: wgpu::StoreOp::Discard,
|
||||
}),
|
||||
}
|
||||
}),
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
// Forgetting the pass' lifetime means that we are no longer compile-time protected from
|
||||
// runtime errors caused by accessing the parent encoder before the render pass is dropped.
|
||||
// Since we don't pass it on to the renderer, we should be perfectly safe against this mistake here!
|
||||
renderer.render(
|
||||
&mut render_pass.forget_lifetime(),
|
||||
clipped_primitives,
|
||||
&screen_descriptor,
|
||||
);
|
||||
|
||||
if capture && let Some(capture_state) = &mut self.screen_capture_state {
|
||||
capture_buffer = Some(capture_state.copy_textures(
|
||||
&render_state.device,
|
||||
&output_frame,
|
||||
&mut encoder,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let encoded = {
|
||||
profiling::scope!("CommandEncoder::finish");
|
||||
encoder.finish()
|
||||
};
|
||||
|
||||
// Submit the commands: both the main buffer and user-defined ones.
|
||||
{
|
||||
profiling::scope!("Queue::submit");
|
||||
// wgpu doesn't document where vsync can happen. Maybe here?
|
||||
let start = web_time::Instant::now();
|
||||
render_state
|
||||
.queue
|
||||
.submit(core::iter::chain(user_cmd_bufs, [encoded]));
|
||||
vsync_sec += start.elapsed().as_secs_f32();
|
||||
};
|
||||
|
||||
// Ensure that the queue guard does not do unnecessary work when dropped
|
||||
render_queue_guard.commands_submitted = true;
|
||||
|
||||
// Free textures marked for destruction **after** queue submit since they might still be used in the current frame.
|
||||
// Calling `wgpu::Texture::destroy` on a texture that is still in use would invalidate the command buffer(s) it is used in.
|
||||
// However, once we called `wgpu::Queue::submit`, it is up for wgpu to determine how long the underlying gpu resource has to live.
|
||||
{
|
||||
let mut renderer = render_state.renderer.write();
|
||||
#[expect(clippy::iter_over_hash_type)] // Order doesn't matter here
|
||||
for id in textures_delta.free.drain() {
|
||||
renderer.free_texture(&id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(capture_buffer) = capture_buffer
|
||||
&& let Some(screen_capture_state) = &mut self.screen_capture_state
|
||||
{
|
||||
screen_capture_state.read_screen_rgba(
|
||||
self.context.clone(),
|
||||
capture_buffer,
|
||||
capture_data,
|
||||
self.capture_tx.clone(),
|
||||
viewport_id,
|
||||
);
|
||||
}
|
||||
|
||||
window.pre_present_notify();
|
||||
|
||||
{
|
||||
profiling::scope!("present");
|
||||
// wgpu doesn't document where vsync can happen. Maybe here?
|
||||
let start = web_time::Instant::now();
|
||||
render_state.queue.present(output_frame);
|
||||
vsync_sec += start.elapsed().as_secs_f32();
|
||||
}
|
||||
|
||||
vsync_sec
|
||||
}
|
||||
|
||||
/// Call this at the beginning of each frame to receive the requested screenshots.
|
||||
pub fn handle_screenshots(&self, events: &mut Vec<Event>) {
|
||||
for (viewport_id, user_data, screenshot) in self.capture_rx.try_iter() {
|
||||
let screenshot = Arc::new(screenshot);
|
||||
for data in user_data {
|
||||
events.push(Event::Screenshot {
|
||||
viewport_id,
|
||||
user_data: data,
|
||||
image: Arc::clone(&screenshot),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gc_viewports(&mut self, active_viewports: &ViewportIdSet) {
|
||||
self.surfaces.retain(|id, _| active_viewports.contains(id));
|
||||
self.depth_texture_view
|
||||
.retain(|id, _| active_viewports.contains(id));
|
||||
self.msaa_texture_view
|
||||
.retain(|id, _| active_viewports.contains(id));
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_pass_by_ref_mut, clippy::unused_self)]
|
||||
pub fn destroy(&mut self) {
|
||||
// TODO(emilk): something here?
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user