diff --git a/CHANGELOG.md b/CHANGELOG.md index 590ece1a03e..43835a029fa 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 8cff8bf6899..3d6a8ae5da0 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 d04e5089a13..cf3973316ac 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 750093ba368..57d571a7cea 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 093ce162c37..349a34a4798 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 332e80750cc..1cb36c747bc 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 d357021e56b..a636f40550a 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 b9258e22946..f2e8b02b7c0 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 0fe21b84eb3..89cb09c8478 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 89312c17007..1c5c5863229 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 96ddec73568..968fa5e3a8c 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 b58004859e1..cec9a269ad6 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 bd2ccc368a4..d11823f3137 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 00000000000..6d49307daf3 --- /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 00000000000..9273ce8730f --- /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 62ad4a472ea..1e28bbd6b4e 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 78edeab7b97..ff43549b56f 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 ccb234e900c..6d3aa3cf317 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 092ac0fdcb0..9bd0eb7f7d9 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 ee3f88fed45..ae79b362470 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 e182ac12c81..f3f0c0e69fe 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 1d88e86b500..a6a129726a1 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); }