diff --git a/CHANGELOG.md b/CHANGELOG.md index 590ece1a03e6..43835a029faf 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 8cff8bf68995..c860b087b7a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,15 +87,6 @@ version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" -[[package]] -name = "approx" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" -dependencies = [ - "num-traits", -] - [[package]] name = "arboard" version = "2.1.0" @@ -493,16 +484,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cgmath" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" -dependencies = [ - "approx", - "num-traits", -] - [[package]] name = "chrono" version = "0.4.19" @@ -871,16 +852,6 @@ dependencies = [ "glow", ] -[[package]] -name = "custom_3d_three-d" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_glow", - "glow", - "three-d", -] - [[package]] name = "custom_font" version = "0.1.0" @@ -1168,6 +1139,7 @@ dependencies = [ "egui", "pollster", "tracing", + "type-map", "wgpu", "winit", ] @@ -1192,6 +1164,7 @@ dependencies = [ name = "egui_demo_app" version = "0.18.0" dependencies = [ + "bytemuck", "chrono", "console_error_panic_hook", "eframe", @@ -1201,6 +1174,7 @@ dependencies = [ "ehttp", "image", "poll-promise", + "pollster", "serde", "tracing-subscriber", "tracing-wasm", @@ -1662,16 +1636,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" -[[package]] -name = "gloo-timers" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "glow" version = "0.11.2" @@ -1829,11 +1793,6 @@ name = "half" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" -dependencies = [ - "num-traits", - "serde", - "zerocopy", -] [[package]] name = "hashbrown" @@ -2072,12 +2031,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libm" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" - [[package]] name = "line-wrap" version = "0.1.1" @@ -2416,7 +2369,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -3428,18 +3380,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - [[package]] name = "syntect" version = "4.6.0" @@ -3528,23 +3468,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "three-d" -version = "0.12.0" -source = "git+https://github.com/asny/three-d.git?rev=43f210668197e8b1dc50ea9f074d2b8426ecb0f2#43f210668197e8b1dc50ea9f074d2b8426ecb0f2" -dependencies = [ - "cgmath", - "gloo-timers", - "glow", - "half", - "js-sys", - "serde", - "thiserror", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "time" version = "0.1.43" @@ -3720,6 +3643,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" @@ -3905,8 +3837,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" dependencies = [ "cfg-if 1.0.0", - "serde", - "serde_json", "wasm-bindgen-macro", ] @@ -4473,27 +4403,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332f188cc1bcf1fe1064b8c58d150f497e697f49774aa846f2dc949d9a25f236" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0fbc82b82efe24da867ee52e015e58178684bd9dd64c34e66bdf21da2582a9f" -dependencies = [ - "proc-macro2", - "syn", - "synstructure", -] - [[package]] name = "zstd" version = "0.10.0+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index fc26b0625b79..2aca361b57fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ "examples/confirm_exit", "examples/custom_3d_glow", - "examples/custom_3d_three-d", + # "examples/custom_3d_three-d", "examples/custom_font", "examples/custom_font_style", "examples/custom_window_frame", diff --git a/docs/egui_demo_app.js b/docs/egui_demo_app.js index b1a17055d738..9dd0bbf258ea 100644 --- a/docs/egui_demo_app.js +++ b/docs/egui_demo_app.js @@ -213,21 +213,21 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_28(arg0, arg1) { - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0fd05312e5982956(arg0, arg1); + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h517980ac832385bd(arg0, arg1); } function __wbg_adapter_31(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h67fa6b1a144b91cc(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h006a9139d694e1ca(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_34(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h67fa6b1a144b91cc(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h006a9139d694e1ca(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_37(arg0, arg1) { try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); - wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha165bf8c3b3285b8(retptr, arg0, arg1); + wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h2e68196b4392f7ca(retptr, arg0, arg1); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; if (r1) { @@ -260,15 +260,15 @@ function makeClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_40(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hbc9fd031a6ee69eb(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5a876099e8c37418(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_43(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hbc9fd031a6ee69eb(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5a876099e8c37418(arg0, arg1, addHeapObject(arg2)); } function __wbg_adapter_46(arg0, arg1, arg2) { - wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha2f39f81315ed630(arg0, arg1, addHeapObject(arg2)); + wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd89f67ab5d72c155(arg0, arg1, addHeapObject(arg2)); } /** @@ -1537,32 +1537,32 @@ async function init(input) { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1269 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 371, __wbg_adapter_28); + imports.wbg.__wbindgen_closure_wrapper3052 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1094, __wbg_adapter_28); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1270 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 371, __wbg_adapter_31); + imports.wbg.__wbindgen_closure_wrapper3053 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1094, __wbg_adapter_31); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1271 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 371, __wbg_adapter_34); + imports.wbg.__wbindgen_closure_wrapper3054 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1094, __wbg_adapter_34); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1280 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 371, __wbg_adapter_37); + imports.wbg.__wbindgen_closure_wrapper3063 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1094, __wbg_adapter_37); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1513 = function(arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 524, __wbg_adapter_40); + imports.wbg.__wbindgen_closure_wrapper3293 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 1235, __wbg_adapter_40); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1514 = function(arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 524, __wbg_adapter_43); + imports.wbg.__wbindgen_closure_wrapper3294 = function(arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 1235, __wbg_adapter_43); return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper1552 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 544, __wbg_adapter_46); + imports.wbg.__wbindgen_closure_wrapper3332 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1255, __wbg_adapter_46); return addHeapObject(ret); }; diff --git a/docs/egui_demo_app_bg.wasm b/docs/egui_demo_app_bg.wasm index b17b5e866f6f..9b654d957c4c 100644 Binary files a/docs/egui_demo_app_bg.wasm and b/docs/egui_demo_app_bg.wasm differ diff --git a/eframe/src/epi.rs b/eframe/src/epi.rs index d04e5089a135..cfbbffd9392d 100644 --- a/eframe/src/epi.rs +++ b/eframe/src/epi.rs @@ -29,6 +29,10 @@ 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 +339,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 750093ba368a..57d571a7cea3 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 093ce162c370..349a34a4798d 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 332e80750cc7..1cb36c747bce 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 d357021e56b3..a636f40550a1 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 b9258e229463..e8a840ebfa19 100644 --- a/egui-wgpu/Cargo.toml +++ b/egui-wgpu/Cargo.toml @@ -35,6 +35,7 @@ egui = { version = "0.18.1", path = "../egui", default-features = false, feature "bytemuck", ] } +type-map = "0.5.0" bytemuck = "1.7" tracing = "0.1" wgpu = { version = "0.12", features = ["webgl"] } diff --git a/egui-wgpu/src/lib.rs b/egui-wgpu/src/lib.rs index 0fe21b84eb35..89cb09c84781 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 89312c170075..94cd6ced79ce 100644 --- a/egui-wgpu/src/renderer.rs +++ b/egui-wgpu/src/renderer.rs @@ -2,10 +2,56 @@ 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. +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() + } + + 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 + } + + 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 +107,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 +263,7 @@ impl RenderPass { texture_bind_group_layout, textures: HashMap::new(), next_user_texture_id: 0, + paint_callback_resources: TypeMap::default(), } } @@ -258,13 +308,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 +329,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; + } - rpass.set_scissor_rect(x, y, width, height); + 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); + match primitive { Primitive::Mesh(mesh) => { if let Some((_texture, bind_group)) = self.textures.get(&mesh.texture_id) { @@ -328,8 +371,56 @@ 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 { + 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, + ); + } } } } @@ -587,8 +678,16 @@ 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 { + // TODO: Should we warn in the console about this, or provide some other way to + // debug ignored paint callbacks? + continue; + }; + + (cbfn.prepare)(device, queue, &mut self.paint_callback_resources); } } } @@ -633,3 +732,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 96ddec735683..173d7bc10258 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,25 @@ 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); + } + for id in &textures_delta.free { + rpass.free_texture(id); + } + + 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, diff --git a/egui_demo_app/Cargo.toml b/egui_demo_app/Cargo.toml index b58004859e1e..79723a955207 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] @@ -42,6 +42,8 @@ chrono = { version = "0.4", features = ["js-sys", "wasmbind"] } eframe = { version = "0.18.0", path = "../eframe", default-features = false } egui = { version = "0.18.0", path = "../egui", features = ["extra_debug_asserts"] } egui_demo_lib = { version = "0.18.0", path = "../egui_demo_lib", features = ["chrono"] } +bytemuck = { version = "1.9.1", optional = true } +pollster = { version = "0.2.5", optional = true } # Optional dependencies: 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 bd2ccc368a4d..d11823f31375 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 000000000000..6d49307daf31 --- /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 000000000000..9273ce8730f2 --- /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 62ad4a472eac..1e28bbd6b4e3 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 78edeab7b973..ff43549b56fb 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 ccb234e900c6..6d3aa3cf3177 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 092ac0fdcb02..3c6b48c57ba2 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,18 @@ pub struct Painter { destroyed: bool, } +// TODO: Documentation +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 +393,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"); + } check_for_gl_error!(&self.gl, "callback"); diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index ee3f88fed45a..ae79b3624706 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 e182ac12c81b..f3f0c0e69fe7 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 1d88e86b5007..a6a129726a19 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); }