From 1d9524cc593d0bcf100d1fe9d989db4d3aaebb0d Mon Sep 17 00:00:00 2001 From: Zicklag Date: Sat, 28 May 2022 10:52:36 -0500 Subject: [PATCH] Re-implement PaintCallbacks With Support for WGPU (#1684) * Re-implement PaintCallbacks With Support for WGPU This makes breaking changes to the PaintCallback system, but makes it flexible enough to support both the WGPU and glow backends with custom rendering. Also adds a WGPU equivalent to the glow demo for custom painting. --- CHANGELOG.md | 4 + Cargo.lock | 12 + eframe/src/epi.rs | 10 + eframe/src/lib.rs | 3 + eframe/src/native/epi_integration.rs | 3 + eframe/src/native/run.rs | 8 + eframe/src/web/backend.rs | 4 + egui-wgpu/Cargo.toml | 1 + egui-wgpu/src/lib.rs | 3 + egui-wgpu/src/renderer.rs | 253 +++++++++++++++--- egui-wgpu/src/winit.rs | 63 +++-- egui_demo_app/Cargo.toml | 4 +- .../apps/{custom3d.rs => custom3d_glow.rs} | 18 +- egui_demo_app/src/apps/custom3d_wgpu.rs | 177 ++++++++++++ .../src/apps/custom3d_wgpu_shader.wgsl | 39 +++ egui_demo_app/src/apps/mod.rs | 14 +- egui_demo_app/src/wrap_app.rs | 24 +- egui_glow/src/lib.rs | 2 +- egui_glow/src/painter.rs | 31 ++- epaint/src/shape.rs | 26 +- examples/custom_3d_glow/src/main.rs | 10 +- examples/custom_3d_three-d/src/main.rs | 14 +- 22 files changed, 594 insertions(+), 129 deletions(-) rename egui_demo_app/src/apps/{custom3d.rs => custom3d_glow.rs} (92%) create mode 100644 egui_demo_app/src/apps/custom3d_wgpu.rs create mode 100644 egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl diff --git a/CHANGELOG.md b/CHANGELOG.md index 590ece1a0..43835a029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui-w * Added opt-in feature `deadlock_detection` to detect double-lock of mutexes on the same thread ([#1619](https://github.com/emilk/egui/pull/1619)). * Added `InputState::stable_dt`: a more stable estimate for the delta-time in reactive mode ([#1625](https://github.com/emilk/egui/pull/1625)). * You can now specify a texture filter for your textures ([#1636](https://github.com/emilk/egui/pull/1636)). +* Added support for using `PaintCallback` shapes with the WGPU backend ([#1684](https://github.com/emilk/egui/pull/1684)) + +### Changed +* `PaintCallback` shapes now require the whole callback to be put in an `Arc` with the value being a backend-specific callback type. ([#1684](https://github.com/emilk/egui/pull/1684)) ### Fixed 🐛 * Fixed `ImageButton`'s changing background padding on hover ([#1595](https://github.com/emilk/egui/pull/1595)). diff --git a/Cargo.lock b/Cargo.lock index 8cff8bf68..3d6a8ae5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1168,6 +1168,7 @@ dependencies = [ "egui", "pollster", "tracing", + "type-map", "wgpu", "winit", ] @@ -1192,6 +1193,7 @@ dependencies = [ name = "egui_demo_app" version = "0.18.0" dependencies = [ + "bytemuck", "chrono", "console_error_panic_hook", "eframe", @@ -1201,6 +1203,7 @@ dependencies = [ "ehttp", "image", "poll-promise", + "pollster", "serde", "tracing-subscriber", "tracing-wasm", @@ -3720,6 +3723,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash", +] + [[package]] name = "unicode-bidi" version = "0.3.8" diff --git a/eframe/src/epi.rs b/eframe/src/epi.rs index d04e5089a..cf3973316 100644 --- a/eframe/src/epi.rs +++ b/eframe/src/epi.rs @@ -29,6 +29,11 @@ pub struct CreationContext<'s> { /// you might want to use later from a [`egui::PaintCallback`]. #[cfg(feature = "glow")] pub gl: Option>, + + /// Can be used to manage GPU resources for custom rendering with WGPU using + /// [`egui::PaintCallback`]s. + #[cfg(feature = "wgpu")] + pub render_state: Option, } // ---------------------------------------------------------------------------- @@ -335,6 +340,11 @@ pub struct Frame { #[cfg(feature = "glow")] #[doc(hidden)] pub gl: Option>, + + /// Can be used to manage GPU resources for custom rendering with WGPU using + /// [`egui::PaintCallback`]s. + #[cfg(feature = "wgpu")] + pub render_state: Option, } impl Frame { diff --git a/eframe/src/lib.rs b/eframe/src/lib.rs index 750093ba3..57d571a7c 100644 --- a/eframe/src/lib.rs +++ b/eframe/src/lib.rs @@ -61,6 +61,9 @@ pub use {egui, egui::emath, egui::epaint}; #[cfg(feature = "glow")] pub use {egui_glow, glow}; +#[cfg(feature = "wgpu")] +pub use {egui_wgpu, wgpu}; + mod epi; // Re-export everything in `epi` so `eframe` users don't have to care about what `epi` is: diff --git a/eframe/src/native/epi_integration.rs b/eframe/src/native/epi_integration.rs index 093ce162c..349a34a47 100644 --- a/eframe/src/native/epi_integration.rs +++ b/eframe/src/native/epi_integration.rs @@ -188,6 +188,7 @@ impl EpiIntegration { window: &winit::window::Window, storage: Option>, #[cfg(feature = "glow")] gl: Option>, + #[cfg(feature = "wgpu")] render_state: Option, ) -> Self { let egui_ctx = egui::Context::default(); @@ -207,6 +208,8 @@ impl EpiIntegration { storage, #[cfg(feature = "glow")] gl, + #[cfg(feature = "wgpu")] + render_state, }; if prefer_dark_mode == Some(true) { diff --git a/eframe/src/native/run.rs b/eframe/src/native/run.rs index 332e80750..1cb36c747 100644 --- a/eframe/src/native/run.rs +++ b/eframe/src/native/run.rs @@ -62,6 +62,8 @@ pub fn run_glow( gl_window.window(), storage, Some(gl.clone()), + #[cfg(feature = "wgpu")] + None, ); { @@ -76,6 +78,8 @@ pub fn run_glow( integration_info: integration.frame.info(), storage: integration.frame.storage(), gl: Some(gl.clone()), + #[cfg(feature = "wgpu")] + render_state: None, }); if app.warm_up_enabled() { @@ -230,6 +234,8 @@ pub fn run_wgpu( painter }; + let render_state = painter.get_render_state().expect("Uninitialized"); + let mut integration = epi_integration::EpiIntegration::new( &event_loop, painter.max_texture_side().unwrap_or(2048), @@ -237,6 +243,7 @@ pub fn run_wgpu( storage, #[cfg(feature = "glow")] None, + Some(render_state.clone()), ); { @@ -252,6 +259,7 @@ pub fn run_wgpu( storage: integration.frame.storage(), #[cfg(feature = "glow")] gl: None, + render_state: Some(render_state), }); if app.warm_up_enabled() { diff --git a/eframe/src/web/backend.rs b/eframe/src/web/backend.rs index d357021e5..a636f4055 100644 --- a/eframe/src/web/backend.rs +++ b/eframe/src/web/backend.rs @@ -170,6 +170,8 @@ impl AppRunner { storage: Some(&storage), #[cfg(feature = "glow")] gl: Some(painter.painter.gl().clone()), + #[cfg(feature = "wgpu")] + render_state: None, }); let frame = epi::Frame { @@ -178,6 +180,8 @@ impl AppRunner { storage: Some(Box::new(storage)), #[cfg(feature = "glow")] gl: Some(painter.gl().clone()), + #[cfg(feature = "wgpu")] + render_state: None, }; let needs_repaint: std::sync::Arc = Default::default(); diff --git a/egui-wgpu/Cargo.toml b/egui-wgpu/Cargo.toml index b9258e229..f2e8b02b7 100644 --- a/egui-wgpu/Cargo.toml +++ b/egui-wgpu/Cargo.toml @@ -37,6 +37,7 @@ egui = { version = "0.18.1", path = "../egui", default-features = false, feature bytemuck = "1.7" tracing = "0.1" +type-map = "0.5.0" wgpu = { version = "0.12", features = ["webgl"] } # Optional: diff --git a/egui-wgpu/src/lib.rs b/egui-wgpu/src/lib.rs index 0fe21b84e..89cb09c84 100644 --- a/egui-wgpu/src/lib.rs +++ b/egui-wgpu/src/lib.rs @@ -6,7 +6,10 @@ pub use wgpu; /// Low-level painting of [`egui`] on [`wgpu`]. pub mod renderer; +pub use renderer::CallbackFn; /// Module for painting [`egui`] with [`wgpu`] on [`winit`]. #[cfg(feature = "winit")] pub mod winit; +#[cfg(feature = "winit")] +pub use crate::winit::RenderState; diff --git a/egui-wgpu/src/renderer.rs b/egui-wgpu/src/renderer.rs index 89312c170..1c5c58632 100644 --- a/egui-wgpu/src/renderer.rs +++ b/egui-wgpu/src/renderer.rs @@ -2,10 +2,79 @@ use std::{borrow::Cow, collections::HashMap, num::NonZeroU32}; -use egui::epaint::Primitive; +use egui::{epaint::Primitive, PaintCallbackInfo}; +use type_map::TypeMap; use wgpu; use wgpu::util::DeviceExt as _; +/// A callback function that can be used to compose an [`egui::PaintCallback`] for custom WGPU +/// rendering. +/// +/// The callback is composed of two functions: `prepare` and `paint`. +/// +/// `prepare` is called every frame before `paint`, and can use the passed-in [`wgpu::Device`] and +/// [`wgpu::Buffer`] to allocate or modify GPU resources such as buffers. +/// +/// `paint` is called after `prepare` and is given access to the the [`wgpu::RenderPass`] so that it +/// can issue draw commands. +/// +/// The final argument of both the `prepare` and `paint` callbacks is a the +/// [`paint_callback_resources`][crate::renderer::RenderPass::paint_callback_resources]. +/// `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 +/// +/// See the [custom3d_wgpu] demo source for a detailed usage example. +/// +/// [custom3d_wgpu]: +/// https://github.com/emilk/egui/blob/master/egui_demo_app/src/apps/custom3d_wgpu.rs +pub struct CallbackFn { + prepare: Box, + paint: Box, +} + +type PrepareCallback = dyn Fn(&wgpu::Device, &wgpu::Queue, &mut TypeMap) + 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(|_, _, _| ()), + paint: Box::new(|_, _, _| ()), + } + } +} + +impl CallbackFn { + pub fn new() -> Self { + Self::default() + } + + /// Set the prepare callback + pub fn prepare(mut self, prepare: F) -> Self + where + F: Fn(&wgpu::Device, &wgpu::Queue, &mut TypeMap) + Sync + Send + 'static, + { + self.prepare = Box::new(prepare) as _; + self + } + + /// Set the paint callback + pub fn paint(mut self, paint: F) -> Self + where + F: for<'a, 'b> Fn(PaintCallbackInfo, &'a mut wgpu::RenderPass<'b>, &'b TypeMap) + + Sync + + Send + + 'static, + { + self.paint = Box::new(paint) as _; + self + } +} + /// Enum for selecting the right buffer type. #[derive(Debug)] enum BufferType { @@ -61,6 +130,9 @@ pub struct RenderPass { /// sampler. textures: HashMap, wgpu::BindGroup)>, next_user_texture_id: u64, + /// Storage for use by [`egui::PaintCallback`]'s that need to store resources such as render + /// pipelines that must have the lifetime of the renderpass. + pub paint_callback_resources: type_map::TypeMap, } impl RenderPass { @@ -214,6 +286,7 @@ impl RenderPass { texture_bind_group_layout, textures: HashMap::new(), next_user_texture_id: 0, + paint_callback_resources: TypeMap::default(), } } @@ -258,13 +331,13 @@ impl RenderPass { paint_jobs: &[egui::epaint::ClippedPrimitive], screen_descriptor: &ScreenDescriptor, ) { - rpass.set_pipeline(&self.render_pipeline); - - rpass.set_bind_group(0, &self.uniform_bind_group, &[]); - let pixels_per_point = screen_descriptor.pixels_per_point; let size_in_pixels = screen_descriptor.size_in_pixels; + // Whether or not we need to reset the renderpass state because a paint callback has just + // run. + let mut needs_reset = true; + for ( ( egui::ClippedPrimitive { @@ -279,41 +352,34 @@ impl RenderPass { .zip(&self.vertex_buffers) .zip(&self.index_buffers) { - // Transform clip rect to physical pixels. - let clip_min_x = pixels_per_point * clip_rect.min.x; - let clip_min_y = pixels_per_point * clip_rect.min.y; - let clip_max_x = pixels_per_point * clip_rect.max.x; - let clip_max_y = pixels_per_point * clip_rect.max.y; - - // Make sure clip rect can fit within an `u32`. - let clip_min_x = clip_min_x.clamp(0.0, size_in_pixels[0] as f32); - let clip_min_y = clip_min_y.clamp(0.0, size_in_pixels[1] as f32); - let clip_max_x = clip_max_x.clamp(clip_min_x, size_in_pixels[0] as f32); - let clip_max_y = clip_max_y.clamp(clip_min_y, size_in_pixels[1] as f32); - - let clip_min_x = clip_min_x.round() as u32; - let clip_min_y = clip_min_y.round() as u32; - let clip_max_x = clip_max_x.round() as u32; - let clip_max_y = clip_max_y.round() as u32; - - let width = (clip_max_x - clip_min_x).max(1); - let height = (clip_max_y - clip_min_y).max(1); - - { - // Clip scissor rectangle to target size. - let x = clip_min_x.min(size_in_pixels[0]); - let y = clip_min_y.min(size_in_pixels[1]); - let width = width.min(size_in_pixels[0] - x); - let height = height.min(size_in_pixels[1] - y); - - // Skip rendering with zero-sized clip areas. - if width == 0 || height == 0 { - continue; - } + if needs_reset { + rpass.set_viewport( + 0.0, + 0.0, + size_in_pixels[0] as f32, + size_in_pixels[1] as f32, + 0.0, + 1.0, + ); + rpass.set_pipeline(&self.render_pipeline); + rpass.set_bind_group(0, &self.uniform_bind_group, &[]); + needs_reset = false; + } + + let PixelRect { + x, + y, + width, + height, + } = calculate_pixel_rect(clip_rect, pixels_per_point, size_in_pixels); - rpass.set_scissor_rect(x, y, width, height); + // Skip rendering with zero-sized clip areas. + if width == 0 || height == 0 { + continue; } + rpass.set_scissor_rect(x, y, width, height); + match primitive { Primitive::Mesh(mesh) => { if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) { @@ -328,8 +394,57 @@ impl RenderPass { tracing::warn!("Missing texture: {:?}", mesh.texture_id); } } - Primitive::Callback(_) => { - // already warned about earlier + Primitive::Callback(callback) => { + let cbfn = if let Some(c) = callback.callback.downcast_ref::() { + c + } else { + // We already warned in the `prepare` callback + continue; + }; + + if callback.rect.is_positive() { + needs_reset = true; + + // Set the viewport rect + let PixelRect { + x, + y, + width, + height, + } = calculate_pixel_rect(&callback.rect, pixels_per_point, size_in_pixels); + rpass.set_viewport( + x as f32, + y as f32, + width as f32, + height as f32, + 0.0, + 1.0, + ); + + // Set the scissor rect + let PixelRect { + x, + y, + width, + height, + } = calculate_pixel_rect(clip_rect, pixels_per_point, size_in_pixels); + // Skip rendering with zero-sized clip areas. + if width == 0 || height == 0 { + continue; + } + rpass.set_scissor_rect(x, y, width, height); + + (cbfn.paint)( + PaintCallbackInfo { + viewport: callback.rect, + clip_rect: *clip_rect, + pixels_per_point, + screen_size_px: size_in_pixels, + }, + rpass, + &self.paint_callback_resources, + ); + } } } } @@ -448,7 +563,6 @@ impl RenderPass { }; } - /// Should be called before `execute()`. pub fn free_texture(&mut self, id: &egui::TextureId) { self.textures.remove(id); } @@ -587,8 +701,15 @@ impl RenderPass { }); } } - Primitive::Callback(_) => { - tracing::warn!("Painting callbacks not supported by egui-wgpu (yet)"); + Primitive::Callback(callback) => { + let cbfn = if let Some(c) = callback.callback.downcast_ref::() { + c + } else { + tracing::warn!("Unknown paint callback: expected `egui_gpu::CallbackFn`"); + continue; + }; + + (cbfn.prepare)(device, queue, &mut self.paint_callback_resources); } } } @@ -633,3 +754,51 @@ impl RenderPass { } } } + +/// A Rect in physical pixel space, used for setting viewport and cliipping rectangles. +struct PixelRect { + x: u32, + y: u32, + width: u32, + height: u32, +} + +/// Convert the Egui clip rect to a physical pixel rect we can use for the GPU viewport/scissor +fn calculate_pixel_rect( + clip_rect: &egui::Rect, + pixels_per_point: f32, + target_size: [u32; 2], +) -> PixelRect { + // Transform clip rect to physical pixels. + let clip_min_x = pixels_per_point * clip_rect.min.x; + let clip_min_y = pixels_per_point * clip_rect.min.y; + let clip_max_x = pixels_per_point * clip_rect.max.x; + let clip_max_y = pixels_per_point * clip_rect.max.y; + + // Make sure clip rect can fit within an `u32`. + let clip_min_x = clip_min_x.clamp(0.0, target_size[0] as f32); + let clip_min_y = clip_min_y.clamp(0.0, target_size[1] as f32); + let clip_max_x = clip_max_x.clamp(clip_min_x, target_size[0] as f32); + let clip_max_y = clip_max_y.clamp(clip_min_y, target_size[1] as f32); + + let clip_min_x = clip_min_x.round() as u32; + let clip_min_y = clip_min_y.round() as u32; + let clip_max_x = clip_max_x.round() as u32; + let clip_max_y = clip_max_y.round() as u32; + + let width = (clip_max_x - clip_min_x).max(1); + let height = (clip_max_y - clip_min_y).max(1); + + // Clip scissor rectangle to target size. + let x = clip_min_x.min(target_size[0]); + let y = clip_min_y.min(target_size[1]); + let width = width.min(target_size[0] - x); + let height = height.min(target_size[1] - y); + + PixelRect { + x, + y, + width, + height, + } +} diff --git a/egui-wgpu/src/winit.rs b/egui-wgpu/src/winit.rs index 96ddec735..968fa5e3a 100644 --- a/egui-wgpu/src/winit.rs +++ b/egui-wgpu/src/winit.rs @@ -1,13 +1,19 @@ +use std::sync::Arc; + +use egui::mutex::RwLock; use tracing::error; use wgpu::{Adapter, Instance, Surface, TextureFormat}; use crate::renderer; -struct RenderState { - device: wgpu::Device, - queue: wgpu::Queue, - target_format: TextureFormat, - egui_rpass: renderer::RenderPass, +/// Access to the render state for egui, which can be useful in combination with +/// [`egui::PaintCallback`]s for custom rendering using WGPU. +#[derive(Clone)] +pub struct RenderState { + pub device: Arc, + pub queue: Arc, + pub target_format: TextureFormat, + pub egui_rpass: Arc>, } struct SurfaceState { @@ -66,6 +72,13 @@ impl<'a> Painter<'a> { } } + /// Get the [`RenderState`]. + /// + /// Will return [`None`] if the render state has not been initialized yet. + pub fn get_render_state(&self) -> Option { + self.render_state.as_ref().cloned() + } + async fn init_render_state( &self, adapter: &Adapter, @@ -74,13 +87,13 @@ impl<'a> Painter<'a> { let (device, queue) = pollster::block_on(adapter.request_device(&self.device_descriptor, None)).unwrap(); - let egui_rpass = renderer::RenderPass::new(&device, target_format, self.msaa_samples); + let rpass = renderer::RenderPass::new(&device, target_format, self.msaa_samples); RenderState { - device, - queue, + device: Arc::new(device), + queue: Arc::new(queue), target_format, - egui_rpass, + egui_rpass: Arc::new(RwLock::new(rpass)), } } @@ -246,27 +259,22 @@ impl<'a> Painter<'a> { pixels_per_point, }; - for (id, image_delta) in &textures_delta.set { - render_state.egui_rpass.update_texture( + { + let mut rpass = render_state.egui_rpass.write(); + for (id, image_delta) in &textures_delta.set { + rpass.update_texture(&render_state.device, &render_state.queue, *id, image_delta); + } + + rpass.update_buffers( &render_state.device, &render_state.queue, - *id, - image_delta, + clipped_primitives, + &screen_descriptor, ); } - for id in &textures_delta.free { - render_state.egui_rpass.free_texture(id); - } - - render_state.egui_rpass.update_buffers( - &render_state.device, - &render_state.queue, - clipped_primitives, - &screen_descriptor, - ); // Record all render passes. - render_state.egui_rpass.execute( + render_state.egui_rpass.read().execute( &mut encoder, &output_view, clipped_primitives, @@ -279,6 +287,13 @@ impl<'a> Painter<'a> { }), ); + { + let mut rpass = render_state.egui_rpass.write(); + for id in &textures_delta.free { + rpass.free_texture(id); + } + } + // Submit the commands. render_state.queue.submit(std::iter::once(encoder.finish())); diff --git a/egui_demo_app/Cargo.toml b/egui_demo_app/Cargo.toml index b58004859..cec9a269a 100644 --- a/egui_demo_app/Cargo.toml +++ b/egui_demo_app/Cargo.toml @@ -34,7 +34,7 @@ serde = [ syntax_highlighting = ["egui_demo_lib/syntax_highlighting"] glow = ["eframe/glow"] -wgpu = ["eframe/wgpu"] +wgpu = ["eframe/wgpu", "bytemuck", "pollster"] [dependencies] @@ -45,6 +45,7 @@ egui_demo_lib = { version = "0.18.0", path = "../egui_demo_lib", features = ["ch # Optional dependencies: +bytemuck = { version = "1.9.1", optional = true } egui_extras = { version = "0.18.0", optional = true, path = "../egui_extras" } # feature "http": @@ -54,6 +55,7 @@ image = { version = "0.24", optional = true, default-features = false, features "png", ] } poll-promise = { version = "0.1", optional = true, default-features = false } +pollster = { version = "0.2.5", optional = true } # feature "persistence": serde = { version = "1", optional = true, features = ["derive"] } diff --git a/egui_demo_app/src/apps/custom3d.rs b/egui_demo_app/src/apps/custom3d_glow.rs similarity index 92% rename from egui_demo_app/src/apps/custom3d.rs rename to egui_demo_app/src/apps/custom3d_glow.rs index bd2ccc368..d11823f31 100644 --- a/egui_demo_app/src/apps/custom3d.rs +++ b/egui_demo_app/src/apps/custom3d_glow.rs @@ -11,9 +11,11 @@ pub struct Custom3d { } impl Custom3d { - pub fn new(gl: &glow::Context) -> Self { + pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self { Self { - rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(gl))), + rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new( + cc.gl.as_ref().expect("GL Enabled"), + ))), angle: 0.0, } } @@ -58,15 +60,13 @@ impl Custom3d { let angle = self.angle; let rotating_triangle = self.rotating_triangle.clone(); + let cb = egui_glow::CallbackFn::new(move |_info, painter| { + rotating_triangle.lock().paint(painter.gl(), angle); + }); + let callback = egui::PaintCallback { rect, - callback: std::sync::Arc::new(move |_info, render_ctx| { - if let Some(painter) = render_ctx.downcast_ref::() { - rotating_triangle.lock().paint(painter.gl(), angle); - } else { - eprintln!("Can't do custom painting because we are not using a glow context"); - } - }), + callback: Arc::new(cb), }; ui.painter().add(callback); } diff --git a/egui_demo_app/src/apps/custom3d_wgpu.rs b/egui_demo_app/src/apps/custom3d_wgpu.rs new file mode 100644 index 000000000..6d49307da --- /dev/null +++ b/egui_demo_app/src/apps/custom3d_wgpu.rs @@ -0,0 +1,177 @@ +use std::sync::Arc; + +use eframe::{ + egui_wgpu::{self, wgpu}, + wgpu::util::DeviceExt, +}; + +pub struct Custom3d { + angle: f32, +} + +impl Custom3d { + pub fn new<'a>(cc: &'a eframe::CreationContext<'a>) -> Self { + // Get the WGPU render state from the eframe creation context. This can also be retrieved + // from `eframe::Frame` when you don't have a `CreationContext` available. + let render_state = cc.render_state.as_ref().expect("WGPU enabled"); + + let device = &render_state.device; + + let shader = device.create_shader_module(&wgpu::ShaderModuleDescriptor { + label: None, + source: wgpu::ShaderSource::Wgsl(include_str!("./custom3d_wgpu_shader.wgsl").into()), + }); + + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[&bind_group_layout], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[render_state.target_format.into()], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: None, + contents: bytemuck::cast_slice(&[0.0]), + usage: wgpu::BufferUsages::COPY_DST + | wgpu::BufferUsages::MAP_WRITE + | wgpu::BufferUsages::UNIFORM, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }], + }); + + // Because the graphics pipeline must have the same lifetime as the egui render pass, + // instead of storing the pipeline in our `Custom3D` struct, we insert it into the + // `paint_callback_resources` type map, which is stored alongside the render pass. + render_state + .egui_rpass + .write() + .paint_callback_resources + .insert(TriangleRenderResources { + pipeline, + bind_group, + uniform_buffer, + }); + + Self { angle: 0.0 } + } +} + +impl eframe::App for Custom3d { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("The triangle is being painted using "); + ui.hyperlink_to("WGPU", "https://wgpu.rs"); + ui.label(" (Portable Rust graphics API awesomeness)"); + }); + ui.label( + "It's not a very impressive demo, but it shows you can embed 3D inside of egui.", + ); + + egui::Frame::canvas(ui.style()).show(ui, |ui| { + self.custom_painting(ui); + }); + ui.label("Drag to rotate!"); + ui.add(egui_demo_lib::egui_github_link_file!()); + }); + } +} + +impl Custom3d { + fn custom_painting(&mut self, ui: &mut egui::Ui) { + let (rect, response) = + ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); + + self.angle += response.drag_delta().x * 0.01; + + // 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. + // + // 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, paint_callback_resources| { + let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap(); + + resources.prepare(device, queue, angle); + }) + .paint(move |_info, rpass, paint_callback_resources| { + let resources: &TriangleRenderResources = paint_callback_resources.get().unwrap(); + + resources.paint(rpass); + }); + + let callback = egui::PaintCallback { + rect, + callback: Arc::new(cb), + }; + + ui.painter().add(callback); + } +} + +struct TriangleRenderResources { + pipeline: wgpu::RenderPipeline, + bind_group: wgpu::BindGroup, + uniform_buffer: wgpu::Buffer, +} + +impl TriangleRenderResources { + fn prepare(&self, _device: &wgpu::Device, queue: &wgpu::Queue, angle: f32) { + // Update our uniform buffer with the angle from the UI + queue.write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[angle])); + } + + fn paint<'rpass>(&'rpass self, rpass: &mut wgpu::RenderPass<'rpass>) { + // Draw our triangle! + rpass.set_pipeline(&self.pipeline); + rpass.set_bind_group(0, &self.bind_group, &[]); + rpass.draw(0..3, 0..1); + } +} diff --git a/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl b/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl new file mode 100644 index 000000000..9273ce873 --- /dev/null +++ b/egui_demo_app/src/apps/custom3d_wgpu_shader.wgsl @@ -0,0 +1,39 @@ +struct VertexOut { + [[location(0)]] color: vec4; + [[builtin(position)]] position: vec4; +}; + +struct Uniforms { + angle: f32; +}; + +[[group(0), binding(0)]] +var uniforms: Uniforms; + +var v_positions: array, 3> = array, 3>( + vec2(0.0, 1.0), + vec2(1.0, -1.0), + vec2(-1.0, -1.0), +); + +var v_colors: array, 3> = array, 3>( + vec4(1.0, 0.0, 0.0, 1.0), + vec4(0.0, 1.0, 0.0, 1.0), + vec4(0.0, 0.0, 1.0, 1.0), +); + +[[stage(vertex)]] +fn vs_main([[builtin(vertex_index)]] v_idx: u32) -> VertexOut { + var out: VertexOut; + + out.position = vec4(v_positions[v_idx], 0.0, 1.0); + out.position.x = out.position.x * cos(uniforms.angle); + out.color = v_colors[v_idx]; + + return out; +} + +[[stage(fragment)]] +fn fs_main(in: VertexOut) -> [[location(0)]] vec4 { + return in.color; +} diff --git a/egui_demo_app/src/apps/mod.rs b/egui_demo_app/src/apps/mod.rs index 62ad4a472..1e28bbd6b 100644 --- a/egui_demo_app/src/apps/mod.rs +++ b/egui_demo_app/src/apps/mod.rs @@ -1,13 +1,19 @@ -#[cfg(feature = "glow")] -mod custom3d; +#[cfg(all(feature = "glow", not(feature = "wgpu")))] +mod custom3d_glow; + +#[cfg(feature = "wgpu")] +mod custom3d_wgpu; mod fractal_clock; #[cfg(feature = "http")] mod http_app; -#[cfg(feature = "glow")] -pub use custom3d::Custom3d; +#[cfg(all(feature = "glow", not(feature = "wgpu")))] +pub use custom3d_glow::Custom3d; + +#[cfg(feature = "wgpu")] +pub use custom3d_wgpu::Custom3d; pub use fractal_clock::FractalClock; diff --git a/egui_demo_app/src/wrap_app.rs b/egui_demo_app/src/wrap_app.rs index 78edeab7b..ff43549b5 100644 --- a/egui_demo_app/src/wrap_app.rs +++ b/egui_demo_app/src/wrap_app.rs @@ -92,9 +92,7 @@ pub struct State { /// Wraps many demo/test apps into one. pub struct WrapApp { state: State, - // not serialized (because it contains OpenGL buffers etc) - #[cfg(feature = "glow")] - custom3d: Option, + custom3d: crate::apps::Custom3d, dropped_files: Vec, } @@ -103,8 +101,7 @@ impl WrapApp { #[allow(unused_mut)] let mut slf = Self { state: State::default(), - #[cfg(feature = "glow")] - custom3d: cc.gl.as_ref().map(|gl| crate::apps::Custom3d::new(gl)), + custom3d: crate::apps::Custom3d::new(cc), dropped_files: Default::default(), }; @@ -149,14 +146,11 @@ impl WrapApp { ), ]; - #[cfg(feature = "glow")] - if let Some(custom3d) = &mut self.custom3d { - vec.push(( - "🔺 3D painting", - "custom3e", - custom3d as &mut dyn eframe::App, - )); - } + vec.push(( + "🔺 3D painting", + "custom3d", + &mut self.custom3d as &mut dyn eframe::App, + )); vec.push(( "🎨 Color test", @@ -224,9 +218,7 @@ impl eframe::App for WrapApp { #[cfg(feature = "glow")] fn on_exit(&mut self, gl: Option<&glow::Context>) { - if let Some(custom3d) = &mut self.custom3d { - custom3d.on_exit(gl); - } + self.custom3d.on_exit(gl); } } diff --git a/egui_glow/src/lib.rs b/egui_glow/src/lib.rs index ccb234e90..6d3aa3cf3 100644 --- a/egui_glow/src/lib.rs +++ b/egui_glow/src/lib.rs @@ -9,7 +9,7 @@ pub mod painter; pub use glow; -pub use painter::Painter; +pub use painter::{CallbackFn, Painter}; mod misc_util; mod post_process; mod shader_version; diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index 092ac0fdc..9bd0eb7f7 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, sync::Arc}; use egui::{ emath::Rect, - epaint::{Color32, Mesh, Primitive, Vertex}, + epaint::{Color32, Mesh, PaintCallbackInfo, Primitive, Vertex}, }; use glow::HasContext as _; use memoffset::offset_of; @@ -68,6 +68,29 @@ pub struct Painter { destroyed: bool, } +/// A callback function that can be used to compose an [`egui::PaintCallback`] for custom rendering +/// with [`glow`]. +/// +/// The callback is passed, the [`egui::PaintCallbackInfo`] and the [`Painter`] which can be used to +/// access the OpenGL context. +/// +/// # Example +/// +/// See the [custom3d_glow] demo source for a detailed usage example. +/// +/// [custom3d_glow]: +/// https://github.com/emilk/egui/blob/master/egui_demo_app/src/apps/custom3d_wgpu.rs +pub struct CallbackFn { + f: Box, +} + +impl CallbackFn { + pub fn new(callback: F) -> Self { + let f = Box::new(callback); + CallbackFn { f } + } +} + impl Painter { /// Create painter. /// @@ -381,7 +404,11 @@ impl Painter { screen_size_px, }; - callback.call(&info, self); + if let Some(callback) = callback.callback.downcast_ref::() { + (callback.f)(info, self); + } else { + tracing::warn!("Warning: Unsupported render callback. Expected egui_glow::CallbackFn"); + } check_for_gl_error!(&self.gl, "callback"); diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index ee3f88fed..ae79b3624 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -1,6 +1,6 @@ //! The different shapes that can be painted. -use std::sync::Arc; +use std::{any::Any, sync::Arc}; use crate::{ text::{FontId, Fonts, Galley}, @@ -747,21 +747,19 @@ pub struct PaintCallback { /// Paint something custom (e.g. 3D stuff). /// - /// The argument is the render context, and what it contains depends on the backend. - /// In `eframe` it will be `egui_glow::Painter`. + /// 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` + /// backend requires a `egui_wgpu::CallbackFn`. /// - /// The rendering backend is responsible for first setting the active viewport to [`Self::rect`]. + /// If the type cannnot be downcast to the type expected by the current backend the callback + /// will not be drawn. /// - /// The rendering backend is also responsible for restoring any state, - /// such as the bound shader program and vertex array. - pub callback: Arc, -} - -impl PaintCallback { - #[inline] - pub fn call(&self, info: &PaintCallbackInfo, render_ctx: &mut dyn std::any::Any) { - (self.callback)(info, render_ctx); - } + /// The rendering backend is responsible for first setting the active viewport to + /// [`Self::rect`]. + /// + /// The rendering backend is also responsible for restoring any state, such as the bound shader + /// program, vertex array, etc. + pub callback: Arc, } impl std::fmt::Debug for PaintCallback { diff --git a/examples/custom_3d_glow/src/main.rs b/examples/custom_3d_glow/src/main.rs index e182ac12c..f3f0c0e69 100644 --- a/examples/custom_3d_glow/src/main.rs +++ b/examples/custom_3d_glow/src/main.rs @@ -76,13 +76,9 @@ impl MyApp { let callback = egui::PaintCallback { rect, - callback: std::sync::Arc::new(move |_info, render_ctx| { - if let Some(painter) = render_ctx.downcast_ref::() { - rotating_triangle.lock().paint(painter.gl(), angle); - } else { - eprintln!("Can't do custom painting because we are not using a glow context"); - } - }), + callback: std::sync::Arc::new(egui_glow::CallbackFn::new(move |_info, painter| { + rotating_triangle.lock().paint(painter.gl(), angle); + })), }; ui.painter().add(callback); } diff --git a/examples/custom_3d_three-d/src/main.rs b/examples/custom_3d_three-d/src/main.rs index 1d88e86b5..a6a129726 100644 --- a/examples/custom_3d_three-d/src/main.rs +++ b/examples/custom_3d_three-d/src/main.rs @@ -60,15 +60,11 @@ impl MyApp { let callback = egui::PaintCallback { rect, - callback: std::sync::Arc::new(move |info, render_ctx| { - if let Some(painter) = render_ctx.downcast_ref::() { - with_three_d_context(painter.gl(), |three_d| { - paint_with_three_d(three_d, info, angle); - }); - } else { - eprintln!("Can't do custom painting because we are not using a glow context"); - } - }), + callback: std::sync::Arc::new(egui_glow::CallbackFn::new(move |info, painter| { + with_three_d_context(painter.gl(), |three_d| { + paint_with_three_d(three_d, &info, angle); + }); + })), }; ui.painter().add(callback); }