wgpu: add support for user-level command buffers + viewport clarification (#2230)

* wgpu: add support for user-level command buffers
* updated wgpu demo app
This commit is contained in:
Clement Rey 2022-11-04 09:54:29 +01:00 committed by GitHub
parent 4d1e858a52
commit fcb00723bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 88 additions and 42 deletions

View file

@ -148,7 +148,8 @@ impl WebPainter for WebPainterWgpu {
size_in_pixels, size_in_pixels,
pixels_per_point, pixels_per_point,
}; };
{
let user_cmd_bufs = {
let mut renderer = render_state.renderer.write(); let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set { for (id, image_delta) in &textures_delta.set {
renderer.update_texture( renderer.update_texture(
@ -165,8 +166,8 @@ impl WebPainter for WebPainterWgpu {
&mut encoder, &mut encoder,
clipped_primitives, clipped_primitives,
&screen_descriptor, &screen_descriptor,
); )
} };
{ {
let renderer = render_state.renderer.read(); let renderer = render_state.renderer.read();
@ -201,8 +202,12 @@ impl WebPainter for WebPainterWgpu {
} }
} }
// Submit the commands. // Submit the commands: both the main buffer and user-defined ones.
render_state.queue.submit(std::iter::once(encoder.finish())); render_state.queue.submit(
user_cmd_bufs
.into_iter()
.chain(std::iter::once(encoder.finish())),
);
frame.present(); frame.present();
Ok(()) Ok(())

View file

@ -9,6 +9,7 @@ All notable changes to the `egui-wgpu` integration will be noted in this file.
* Reexported `Renderer`. * Reexported `Renderer`.
* `Renderer` no longer handles pass creation and depth buffer creation ([#2136](https://github.com/emilk/egui/pull/2136)) * `Renderer` no longer handles pass creation and depth buffer creation ([#2136](https://github.com/emilk/egui/pull/2136))
* `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136)) * `PrepareCallback` now passes `wgpu::CommandEncoder` ([#2136](https://github.com/emilk/egui/pull/2136))
* `PrepareCallback` can now returns `wgpu::CommandBuffer` that are bundled into a single `wgpu::Queue::submit` call ([#2230](https://github.com/emilk/egui/pull/2230))
* Only a single vertex & index buffer is now created and resized when necessary (previously, vertex/index buffers were allocated for every mesh) ([#2148](https://github.com/emilk/egui/pull/2148)). * Only a single vertex & index buffer is now created and resized when necessary (previously, vertex/index buffers were allocated for every mesh) ([#2148](https://github.com/emilk/egui/pull/2148)).
* `Renderer::update_texture` no longer creates a new `wgpu::Sampler` with every new texture ([#2198](https://github.com/emilk/egui/pull/2198)) * `Renderer::update_texture` no longer creates a new `wgpu::Sampler` with every new texture ([#2198](https://github.com/emilk/egui/pull/2198))
* `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207)) * `Painter`'s instance/device/adapter/surface creation is now configurable via `WgpuConfiguration` ([#2207](https://github.com/emilk/egui/pull/2207))

View file

@ -14,17 +14,12 @@ use wgpu::util::DeviceExt as _;
/// A callback function that can be used to compose an [`egui::PaintCallback`] for custom WGPU /// A callback function that can be used to compose an [`egui::PaintCallback`] for custom WGPU
/// rendering. /// rendering.
/// ///
/// The callback is composed of two functions: `prepare` and `paint`. /// The callback is composed of two functions: `prepare` and `paint`:
/// /// - `prepare` is called every frame before `paint`, and can use the passed-in
/// `prepare` is called every frame before `paint`, and can use the passed-in [`wgpu::Device`] and /// [`wgpu::Device`] and [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers.
/// [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers. /// - `paint` is called after `prepare` and is given access to the [`wgpu::RenderPass`] so
/// Additionally, a [`wgpu::CommandEncoder`] is provided in order to allow creation of /// that it can issue draw commands into the same [`wgpu::RenderPass`] that is used for
/// custom [`wgpu::RenderPass`]/[`wgpu::ComputePass`] or perform buffer/texture copies /// all other egui elements.
/// which may serve as preparation to the final `paint`.
/// (This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui rendering itself)
///
/// `paint` is called after `prepare` and 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.
/// ///
/// The final argument of both the `prepare` and `paint` callbacks is a the /// The final argument of both the `prepare` and `paint` callbacks is a the
/// [`paint_callback_resources`][crate::renderer::Renderer::paint_callback_resources]. /// [`paint_callback_resources`][crate::renderer::Renderer::paint_callback_resources].
@ -40,8 +35,14 @@ pub struct CallbackFn {
paint: Box<PaintCallback>, paint: Box<PaintCallback>,
} }
type PrepareCallback = type PrepareCallback = dyn Fn(
dyn Fn(&wgpu::Device, &wgpu::Queue, &mut wgpu::CommandEncoder, &mut TypeMap) + Sync + Send; &wgpu::Device,
&wgpu::Queue,
&mut wgpu::CommandEncoder,
&mut TypeMap,
) -> Vec<wgpu::CommandBuffer>
+ Sync
+ Send;
type PaintCallback = type PaintCallback =
dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send; dyn for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + Sync + Send;
@ -49,7 +50,7 @@ type PaintCallback =
impl Default for CallbackFn { impl Default for CallbackFn {
fn default() -> Self { fn default() -> Self {
CallbackFn { CallbackFn {
prepare: Box::new(|_, _, _, _| ()), prepare: Box::new(|_, _, _, _| Vec::new()),
paint: Box::new(|_, _, _| ()), paint: Box::new(|_, _, _| ()),
} }
} }
@ -60,10 +61,29 @@ impl CallbackFn {
Self::default() Self::default()
} }
/// Set the prepare callback /// Set the prepare callback.
///
/// The passed-in `CommandEncoder` is egui's and can be used directly to register
/// wgpu commands for simple use cases.
/// This allows reusing the same [`wgpu::CommandEncoder`] for all callbacks and egui
/// rendering itself.
///
/// 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.
/// In particular, this gives an opportunity to parallelize command registration and
/// prevents a faulty callback from poisoning the main wgpu pipeline.
///
/// 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
/// at once in a single call.
pub fn prepare<F>(mut self, prepare: F) -> Self pub fn prepare<F>(mut self, prepare: F) -> Self
where where
F: Fn(&wgpu::Device, &wgpu::Queue, &mut wgpu::CommandEncoder, &mut TypeMap) F: Fn(
&wgpu::Device,
&wgpu::Queue,
&mut wgpu::CommandEncoder,
&mut TypeMap,
) -> Vec<wgpu::CommandBuffer>
+ Sync + Sync
+ Send + Send
+ 'static, + 'static,
@ -404,23 +424,23 @@ impl Renderer {
needs_reset = true; needs_reset = true;
{ {
// Set the viewport rect // We're setting a default viewport for the render pass as a
// Transform callback rect to physical pixels: // courtesy for the user, so that they don't have to think about
let rect_min_x = pixels_per_point * callback.rect.min.x; // it in the simple case where they just want to fill the whole
let rect_min_y = pixels_per_point * callback.rect.min.y; // paint area.
let rect_max_x = pixels_per_point * callback.rect.max.x; //
let rect_max_y = pixels_per_point * callback.rect.max.y; // The user still has the possibility of setting their own custom
// viewport during the paint callback, effectively overriding this
// one.
let rect_min_x = rect_min_x.round(); let min = (callback.rect.min.to_vec2() * pixels_per_point).round();
let rect_min_y = rect_min_y.round(); let max = (callback.rect.max.to_vec2() * pixels_per_point).round();
let rect_max_x = rect_max_x.round();
let rect_max_y = rect_max_y.round();
render_pass.set_viewport( render_pass.set_viewport(
rect_min_x, min.x,
rect_min_y, min.y,
rect_max_x - rect_min_x, max.x - min.x,
rect_max_y - rect_min_y, max.y - min.y,
0.0, 0.0,
1.0, 1.0,
); );
@ -702,6 +722,8 @@ 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.
pub fn update_buffers( pub fn update_buffers(
&mut self, &mut self,
device: &wgpu::Device, device: &wgpu::Device,
@ -709,7 +731,7 @@ impl Renderer {
encoder: &mut wgpu::CommandEncoder, encoder: &mut wgpu::CommandEncoder,
paint_jobs: &[egui::epaint::ClippedPrimitive], paint_jobs: &[egui::epaint::ClippedPrimitive],
screen_descriptor: &ScreenDescriptor, screen_descriptor: &ScreenDescriptor,
) { ) -> Vec<wgpu::CommandBuffer> {
let screen_size_in_points = screen_descriptor.screen_size_in_points(); let screen_size_in_points = screen_descriptor.screen_size_in_points();
// Update uniform buffer // Update uniform buffer
@ -755,6 +777,7 @@ impl Renderer {
} }
// Upload index & vertex data and call user callbacks // Upload index & vertex data and call user callbacks
let mut user_cmd_bufs = Vec::new(); // collect user command buffers
for egui::ClippedPrimitive { primitive, .. } in paint_jobs.iter() { for egui::ClippedPrimitive { primitive, .. } in paint_jobs.iter() {
match primitive { match primitive {
Primitive::Mesh(mesh) => { Primitive::Mesh(mesh) => {
@ -783,10 +806,17 @@ impl Renderer {
continue; continue;
}; };
(cbfn.prepare)(device, queue, encoder, &mut self.paint_callback_resources); user_cmd_bufs.extend((cbfn.prepare)(
device,
queue,
encoder,
&mut self.paint_callback_resources,
));
} }
} }
} }
user_cmd_bufs
} }
} }

View file

@ -264,7 +264,7 @@ impl Painter {
pixels_per_point, pixels_per_point,
}; };
{ let user_cmd_bufs = {
let mut renderer = render_state.renderer.write(); let mut renderer = render_state.renderer.write();
for (id, image_delta) in &textures_delta.set { for (id, image_delta) in &textures_delta.set {
renderer.update_texture( renderer.update_texture(
@ -281,8 +281,8 @@ impl Painter {
&mut encoder, &mut encoder,
clipped_primitives, clipped_primitives,
&screen_descriptor, &screen_descriptor,
); )
} };
{ {
let renderer = render_state.renderer.read(); let renderer = render_state.renderer.read();
@ -326,8 +326,12 @@ impl Painter {
} }
} }
// Submit the commands. // Submit the commands: both the main buffer and user-defined ones.
render_state.queue.submit(std::iter::once(encoder.finish())); render_state.queue.submit(
user_cmd_bufs
.into_iter()
.chain(std::iter::once(encoder.finish())),
);
// Redraw egui // Redraw egui
output_frame.present(); output_frame.present();

View file

@ -135,12 +135,18 @@ impl Custom3d {
// Device and Queue, which can be used, for instance, to update buffers and uniforms before // Device and Queue, which can be used, for instance, to update buffers and uniforms before
// rendering. // 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 // The paint callback is called after prepare and is given access to the render pass, which
// can be used to issue draw commands. // can be used to issue draw commands.
let cb = egui_wgpu::CallbackFn::new() let cb = egui_wgpu::CallbackFn::new()
.prepare(move |device, queue, _encoder, paint_callback_resources| { .prepare(move |device, queue, _encoder, paint_callback_resources| {
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap(); let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();
resources.prepare(device, queue, angle); resources.prepare(device, queue, angle);
Vec::new()
}) })
.paint(move |_info, render_pass, paint_callback_resources| { .paint(move |_info, render_pass, paint_callback_resources| {
let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap(); let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap();