Improved wgpu callbacks (#3253)

* Improved wgpu callbacks

* update documentation on egui_wgpu callbacks

* make shared callback resource map pub

* make it nicer to create epaint::PaintCallback from egui_wgpu callback

* constrain ClippedPrimitive lifetime to outlive wgpu::RenderPass

* Revert callback resources to TypeMap, put finish_prepare on callback trait

* doc string fixes
This commit is contained in:
Andreas Reich 2023-08-15 17:17:39 +02:00 committed by GitHub
parent 3c4223c6b1
commit b896d641c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 196 additions and 163 deletions

View File

@ -36,4 +36,4 @@ opt-level = 2
[workspace.dependencies] [workspace.dependencies]
thiserror = "1.0.37" thiserror = "1.0.37"
wgpu = { version = "0.17.0", features = ["fragile-send-sync-non-atomic-wasm"] } wgpu = "0.17.0"

View File

@ -10,8 +10,8 @@ pub use wgpu;
/// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`]. /// Low-level painting of [`egui`](https://github.com/emilk/egui) on [`wgpu`].
pub mod renderer; pub mod renderer;
pub use renderer::CallbackFn;
pub use renderer::Renderer; pub use renderer::Renderer;
pub use renderer::{Callback, CallbackResources, CallbackTrait};
/// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`]. /// Module for painting [`egui`](https://github.com/emilk/egui) with [`wgpu`] on [`winit`].
#[cfg(feature = "winit")] #[cfg(feature = "winit")]

View File

@ -1,72 +1,60 @@
#![allow(unsafe_code)] #![allow(unsafe_code)]
use std::num::NonZeroU64; use std::{borrow::Cow, num::NonZeroU64, ops::Range};
use std::ops::Range;
use std::{borrow::Cow, collections::HashMap}; use epaint::{ahash::HashMap, emath::NumExt, PaintCallbackInfo, Primitive, Vertex};
use type_map::concurrent::TypeMap;
use wgpu; use wgpu;
use wgpu::util::DeviceExt as _; use wgpu::util::DeviceExt as _;
use epaint::{emath::NumExt, PaintCallbackInfo, Primitive, Vertex}; // Only implements Send + Sync on wasm32 in order to allow storing wgpu resources on the type map.
#[cfg(not(target_arch = "wasm32"))]
pub type CallbackResources = type_map::concurrent::TypeMap;
#[cfg(target_arch = "wasm32")]
pub type CallbackResources = type_map::TypeMap;
/// A callback function that can be used to compose an [`epaint::PaintCallback`] for custom WGPU pub struct Callback(Box<dyn CallbackTrait>);
/// rendering.
impl Callback {
/// Creates a new [`epaint::PaintCallback`] from a callback trait instance.
pub fn new_paint_callback(
rect: epaint::emath::Rect,
callback: impl CallbackTrait + 'static,
) -> epaint::PaintCallback {
epaint::PaintCallback {
rect,
callback: std::sync::Arc::new(Self(Box::new(callback))),
}
}
}
/// A callback trait that can be used to compose an [`epaint::PaintCallback`] via [`Callback`]
/// for custom WGPU rendering.
/// ///
/// The callback is composed of two functions: `prepare` and `paint`: /// Callbacks in [`Renderer`] are done in three steps:
/// - `prepare` is called every frame before `paint`, and can use the passed-in /// * [`CallbackTrait::prepare`]: called for all registered callbacks before the main egui render pass.
/// [`wgpu::Device`] and [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers. /// * [`CallbackTrait::finish_prepare`]: called for all registered callbacks after all callbacks finished calling prepare.
/// - `paint` is called after `prepare` and is given access to the [`wgpu::RenderPass`] so /// * [`CallbackTrait::paint`]: called for all registered callbacks during the main egui render pass.
/// that it can issue draw commands into the same [`wgpu::RenderPass`] that is used for
/// all other egui elements.
/// ///
/// The final argument of both the `prepare` and `paint` callbacks is a the /// Each callback has access to an instance of [`CallbackResources`] that is stored in the [`Renderer`].
/// [`paint_callback_resources`][crate::renderer::Renderer::paint_callback_resources]. /// This can be used to store wgpu resources that need to be accessed during the [`CallbackTrait::paint`] step.
/// `paint_callback_resources` has the same lifetime as the Egui render pass, so it can be used to
/// store buffers, pipelines, and other information that needs to be accessed during the render
/// pass.
/// ///
/// # Example /// The callbacks implementing [`CallbackTrait`] itself must always be Send + Sync, but resources stored in
/// [`Renderer::callback_resources`] are not required to implement Send + Sync when building for wasm.
/// (this is because wgpu stores references to the JS heap in most of its resources which can not be shared with other threads).
/// ///
/// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example. ///
pub struct CallbackFn { /// # Command submission
prepare: Box<PrepareCallback>, ///
paint: Box<PaintCallback>, /// ## Command Encoder
}
type PrepareCallback = dyn Fn(
&wgpu::Device,
&wgpu::Queue,
&mut wgpu::CommandEncoder,
&mut TypeMap,
) -> Vec<wgpu::CommandBuffer>
+ Sync
+ Send;
type PaintCallback =
dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send;
impl Default for CallbackFn {
fn default() -> Self {
CallbackFn {
prepare: Box::new(|_, _, _, _| Vec::new()),
paint: Box::new(|_, _, _| ()),
}
}
}
impl CallbackFn {
pub fn new() -> Self {
Self::default()
}
/// Set the prepare callback.
/// ///
/// The passed-in `CommandEncoder` is egui's and can be used directly to register /// The passed-in `CommandEncoder` is egui's and can be used directly to register
/// wgpu commands for simple use cases. /// wgpu commands for simple use cases.
/// This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui /// This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui
/// rendering itself. /// rendering itself.
/// ///
/// ## Command Buffers
///
/// For more complicated use cases, one can also return a list of arbitrary /// For more complicated use cases, one can also return a list of arbitrary
/// `CommandBuffer`s and have complete control over how they get created and fed. /// `CommandBuffer`s and have complete control over how they get created and fed.
/// In particular, this gives an opportunity to parallelize command registration and /// In particular, this gives an opportunity to parallelize command registration and
@ -75,33 +63,47 @@ impl CallbackFn {
/// When using eframe, the main egui command buffer, as well as all user-defined /// When using eframe, the main egui command buffer, as well as all user-defined
/// command buffers returned by this function, are guaranteed to all be submitted /// command buffers returned by this function, are guaranteed to all be submitted
/// at once in a single call. /// at once in a single call.
pub fn prepare<F>(mut self, prepare: F) -> Self ///
where /// Command Buffers returned by [`CallbackTrait::finish_prepare`] will always be issued *after*
F: Fn( /// those returned by [`CallbackTrait::prepare`].
&wgpu::Device, /// Order within command buffers returned by [`CallbackTrait::prepare`] is dependent
&wgpu::Queue, /// on the order the respective [`epaint::Shape::Callback`]s were submitted in.
&mut wgpu::CommandEncoder, ///
&mut TypeMap, /// # Example
) -> Vec<wgpu::CommandBuffer> ///
+ Sync /// See the [`custom3d_wgpu`](https://github.com/emilk/egui/blob/master/crates/egui_demo_app/src/apps/custom3d_wgpu.rs) demo source for a detailed usage example.
+ Send pub trait CallbackTrait: Send + Sync {
+ 'static, fn prepare(
{ &self,
self.prepare = Box::new(prepare) as _; _device: &wgpu::Device,
self _queue: &wgpu::Queue,
_egui_encoder: &mut wgpu::CommandEncoder,
_callback_resources: &mut CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
Vec::new()
} }
/// Set the paint callback /// Called after all [`CallbackTrait::prepare`] calls are done.
pub fn paint<F>(mut self, paint: F) -> Self fn finish_prepare(
where &self,
F: for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) _device: &wgpu::Device,
+ Sync _queue: &wgpu::Queue,
+ Send _egui_encoder: &mut wgpu::CommandEncoder,
+ 'static, _callback_resources: &mut CallbackResources,
{ ) -> Vec<wgpu::CommandBuffer> {
self.paint = Box::new(paint) as _; Vec::new()
self
} }
/// Called after all [`CallbackTrait::finish_prepare`] calls are done.
///
/// It is given access to the [`wgpu::RenderPass`] so that it can issue draw commands
/// into the same [`wgpu::RenderPass`] that is used for all other egui elements.
fn paint<'a>(
&'a self,
info: PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'a>,
callback_resources: &'a CallbackResources,
);
} }
/// Information about the screen used for rendering. /// Information about the screen used for rendering.
@ -164,9 +166,10 @@ pub struct Renderer {
next_user_texture_id: u64, next_user_texture_id: u64,
samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>, samplers: HashMap<epaint::textures::TextureOptions, wgpu::Sampler>,
/// Storage for use by [`epaint::PaintCallback`]'s that need to store resources such as render /// Storage for resources shared with all invocations of [`CallbackTrait`]'s methods.
/// pipelines that must have the lifetime of the renderpass. ///
pub paint_callback_resources: TypeMap, /// See also [`CallbackTrait`].
pub callback_resources: CallbackResources,
} }
impl Renderer { impl Renderer {
@ -346,10 +349,10 @@ impl Renderer {
}, },
uniform_bind_group, uniform_bind_group,
texture_bind_group_layout, texture_bind_group_layout,
textures: HashMap::new(), textures: HashMap::default(),
next_user_texture_id: 0, next_user_texture_id: 0,
samplers: HashMap::new(), samplers: HashMap::default(),
paint_callback_resources: TypeMap::default(), callback_resources: CallbackResources::default(),
} }
} }
@ -357,7 +360,7 @@ impl Renderer {
pub fn render<'rp>( pub fn render<'rp>(
&'rp self, &'rp self,
render_pass: &mut wgpu::RenderPass<'rp>, render_pass: &mut wgpu::RenderPass<'rp>,
paint_jobs: &[epaint::ClippedPrimitive], paint_jobs: &'rp [epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor, screen_descriptor: &ScreenDescriptor,
) { ) {
crate::profile_function!(); crate::profile_function!();
@ -432,7 +435,7 @@ impl Renderer {
} }
} }
Primitive::Callback(callback) => { Primitive::Callback(callback) => {
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() { let cbfn = if let Some(c) = callback.callback.downcast_ref::<Callback>() {
c c
} else { } else {
// We already warned in the `prepare` callback // We already warned in the `prepare` callback
@ -467,7 +470,7 @@ impl Renderer {
); );
} }
(cbfn.paint)( cbfn.0.paint(
PaintCallbackInfo { PaintCallbackInfo {
viewport: callback.rect, viewport: callback.rect,
clip_rect: *clip_rect, clip_rect: *clip_rect,
@ -475,7 +478,7 @@ impl Renderer {
screen_size_px: size_in_pixels, screen_size_px: size_in_pixels,
}, },
render_pass, render_pass,
&self.paint_callback_resources, &self.callback_resources,
); );
} }
} }
@ -751,7 +754,7 @@ impl Renderer {
/// Uploads the uniform, vertex and index data used by the renderer. /// Uploads the uniform, vertex and index data used by the renderer.
/// Should be called before `render()`. /// Should be called before `render()`.
/// ///
/// Returns all user-defined command buffers gathered from prepare callbacks. /// Returns all user-defined command buffers gathered from [`CallbackTrait::prepare`] & [`CallbackTrait::finish_prepare`] callbacks.
pub fn update_buffers( pub fn update_buffers(
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
@ -778,7 +781,8 @@ impl Renderer {
self.previous_uniform_buffer_content = uniform_buffer_content; self.previous_uniform_buffer_content = uniform_buffer_content;
} }
// Determine how many vertices & indices need to be rendered. // Determine how many vertices & indices need to be rendered, and gather prepare callbacks
let mut callbacks = Vec::new();
let (vertex_count, index_count) = { let (vertex_count, index_count) = {
crate::profile_scope!("count_vertices_indices"); crate::profile_scope!("count_vertices_indices");
paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| { paint_jobs.iter().fold((0, 0), |acc, clipped_primitive| {
@ -786,7 +790,14 @@ impl Renderer {
Primitive::Mesh(mesh) => { Primitive::Mesh(mesh) => {
(acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len()) (acc.0 + mesh.vertices.len(), acc.1 + mesh.indices.len())
} }
Primitive::Callback(_) => acc, Primitive::Callback(callback) => {
if let Some(c) = callback.callback.downcast_ref::<Callback>() {
callbacks.push(c.0.as_ref());
} else {
log::warn!("Unknown paint callback: expected `egui_wgpu::Callback`");
};
acc
}
} }
}) })
}; };
@ -861,34 +872,33 @@ impl Renderer {
} }
} }
let mut user_cmd_bufs = Vec::new();
{ {
crate::profile_scope!("user command buffers"); crate::profile_scope!("prepare callbacks");
let mut user_cmd_bufs = Vec::new(); // collect user command buffers for callback in &callbacks {
for epaint::ClippedPrimitive { primitive, .. } in paint_jobs.iter() { user_cmd_bufs.extend(callback.prepare(
match primitive {
Primitive::Mesh(_) => {}
Primitive::Callback(callback) => {
let cbfn = if let Some(c) = callback.callback.downcast_ref::<CallbackFn>() {
c
} else {
log::warn!("Unknown paint callback: expected `egui_wgpu::CallbackFn`");
continue;
};
crate::profile_scope!("callback");
user_cmd_bufs.extend((cbfn.prepare)(
device, device,
queue, queue,
encoder, encoder,
&mut self.paint_callback_resources, &mut self.callback_resources,
)); ));
} }
} }
{
crate::profile_scope!("finish prepare callbacks");
for callback in &callbacks {
user_cmd_bufs.extend(callback.finish_prepare(
device,
queue,
encoder,
&mut self.callback_resources,
));
} }
}
user_cmd_bufs user_cmd_bufs
} }
} }
}
fn create_sampler( fn create_sampler(
options: epaint::textures::TextureOptions, options: epaint::textures::TextureOptions,
@ -969,6 +979,9 @@ impl ScissorRect {
} }
} }
// Wgpu objects contain references to the JS heap on the web, therefore they are not Send/Sync.
// It follows that egui_wgpu::Renderer can not be Send/Sync either when building with wasm.
#[cfg(not(target_arch = "wasm32"))]
#[test] #[test]
fn renderer_impl_send_sync() { fn renderer_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {} fn assert_send_sync<T: Send + Sync>() {}

View File

@ -1,4 +1,4 @@
use std::{num::NonZeroU64, sync::Arc}; use std::num::NonZeroU64;
use eframe::{ use eframe::{
egui_wgpu::wgpu::util::DeviceExt, egui_wgpu::wgpu::util::DeviceExt,
@ -84,7 +84,7 @@ impl Custom3d {
wgpu_render_state wgpu_render_state
.renderer .renderer
.write() .write()
.paint_callback_resources .callback_resources
.insert(TriangleRenderResources { .insert(TriangleRenderResources {
pipeline, pipeline,
bind_group, bind_group,
@ -119,46 +119,64 @@ impl eframe::App for Custom3d {
} }
} }
// Callbacks in egui_wgpu have 3 stages:
// * prepare (per callback impl)
// * finish_prepare (once)
// * paint (per callback impl)
//
// The prepare callback is called every frame before paint and is given access to the wgpu
// Device and Queue, which can be used, for instance, to update buffers and uniforms before
// rendering.
// If [`egui_wgpu::Renderer`] has [`egui_wgpu::FinishPrepareCallback`] registered,
// it will be called after all `prepare` callbacks have been called.
// You can use this to update any shared resources that need to be updated once per frame
// after all callbacks have been processed.
//
// On both prepare methods you can use the main `CommandEncoder` that is passed-in,
// return an arbitrary number of user-defined `CommandBuffer`s, or both.
// The main command buffer, as well as all user-defined ones, will be submitted together
// to the GPU in a single call.
//
// The paint callback is called after finish prepare and is given access to egui's main render pass,
// which can be used to issue draw commands.
struct CustomTriangleCallback {
angle: f32,
}
impl egui_wgpu::CallbackTrait for CustomTriangleCallback {
fn prepare(
&self,
device: &wgpu::Device,
queue: &wgpu::Queue,
_egui_encoder: &mut wgpu::CommandEncoder,
resources: &mut egui_wgpu::CallbackResources,
) -> Vec<wgpu::CommandBuffer> {
let resources: &TriangleRenderResources = resources.get().unwrap();
resources.prepare(device, queue, self.angle);
Vec::new()
}
fn paint<'a>(
&self,
_info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'a>,
resources: &'a egui_wgpu::CallbackResources,
) {
let resources: &TriangleRenderResources = resources.get().unwrap();
resources.paint(render_pass);
}
}
impl Custom3d { impl Custom3d {
fn custom_painting(&mut self, ui: &mut egui::Ui) { fn custom_painting(&mut self, ui: &mut egui::Ui) {
let (rect, response) = let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
self.angle += response.drag_delta().x * 0.01; self.angle += response.drag_delta().x * 0.01;
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
// Clone locals so we can move them into the paint callback:
let angle = self.angle;
// The callback function for WGPU is in two stages: prepare, and paint.
//
// The prepare callback is called every frame before paint and is given access to the wgpu
// Device and Queue, which can be used, for instance, to update buffers and uniforms before
// rendering.
//
// You can use the main `CommandEncoder` that is passed-in, return an arbitrary number
// of user-defined `CommandBuffer`s, or both.
// The main command buffer, as well as all user-defined ones, will be submitted together
// to the GPU in a single call.
//
// The paint callback is called after prepare and is given access to the render pass, which
// can be used to issue draw commands.
let cb = egui_wgpu::CallbackFn::new()
.prepare(move |device, queue, _encoder, paint_callback_resources| {
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
resources.prepare(device, queue, angle);
Vec::new()
})
.paint(move |_info, render_pass, paint_callback_resources| {
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
resources.paint(render_pass);
});
let callback = egui::PaintCallback {
rect, rect,
callback: Arc::new(cb), CustomTriangleCallback { angle: self.angle },
}; ));
ui.painter().add(callback);
} }
} }

View File

@ -852,7 +852,7 @@ pub struct PaintCallback {
/// ///
/// The concrete value of `callback` depends on the rendering backend used. For instance, the /// The concrete value of `callback` depends on the rendering backend used. For instance, the
/// `glow` backend requires that callback be an `egui_glow::CallbackFn` while the `wgpu` /// `glow` backend requires that callback be an `egui_glow::CallbackFn` while the `wgpu`
/// backend requires a `egui_wgpu::CallbackFn`. /// backend requires a `egui_wgpu::Callback`.
/// ///
/// If the type cannot be downcast to the type expected by the current backend the callback /// If the type cannot be downcast to the type expected by the current backend the callback
/// will not be drawn. /// will not be drawn.
@ -862,7 +862,9 @@ pub struct PaintCallback {
/// ///
/// The rendering backend is also responsible for restoring any state, such as the bound shader /// The rendering backend is also responsible for restoring any state, such as the bound shader
/// program, vertex array, etc. /// program, vertex array, etc.
pub callback: Arc<dyn Any + Sync + Send>, ///
/// Shape has to be clone, therefore this has to be an `Arc` instead of a `Box`.
pub callback: Arc<dyn Any + Send + Sync>,
} }
impl std::fmt::Debug for PaintCallback { impl std::fmt::Debug for PaintCallback {