From 04158309e812d9ef6bbbd0c79c401d991df23925 Mon Sep 17 00:00:00 2001 From: follower Date: Sat, 2 Jul 2022 01:04:54 +1200 Subject: [PATCH] Add experimental egui integration This commit is authored by @follower, with some minor modifications by @hannobraun (squashing a lot of commits into one, running `cargo fmt`, and auto-applying Clippy suggestions). This commit re-adds `fj-viewer`'s dependency on winit, indirectly via `egui`'s winit integration. This is not desirable, but it is acceptable for now. Getting a working egui integration is worth more. @hannobraun will open an issue after merging this to track this problem. --- crates/fj-viewer/src/graphics/renderer.rs | 431 +++++++++++++++++++++- crates/fj-window/src/run.rs | 35 +- 2 files changed, 453 insertions(+), 13 deletions(-) diff --git a/crates/fj-viewer/src/graphics/renderer.rs b/crates/fj-viewer/src/graphics/renderer.rs index 636b96601..614b9ecf2 100644 --- a/crates/fj-viewer/src/graphics/renderer.rs +++ b/crates/fj-viewer/src/graphics/renderer.rs @@ -17,6 +17,29 @@ use super::{ uniforms::Uniforms, vertices::Vertices, DEPTH_FORMAT, }; +#[derive(Default)] +struct EguiOptionsState { + show_trace: bool, + show_layout_debug_on_hover: bool, + show_debug_text_example: bool, + show_original_ui: bool, + show_settings_ui: bool, + show_inspection_ui: bool, +} + +pub struct EguiState { + pub winit_state: egui_winit::State, + pub context: egui::Context, + rpass: egui_wgpu::renderer::RenderPass, + options: EguiOptionsState, +} + +impl std::fmt::Debug for EguiState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("EguiState {}") + } +} + /// Graphics rendering state and target abstraction #[derive(Debug)] pub struct Renderer { @@ -34,13 +57,71 @@ pub struct Renderer { pipelines: Pipelines, config_ui: ConfigUi, + + /// State required for integration with `egui`. + pub egui: EguiState, } impl Renderer { /// Returns a new `Renderer`. - pub async fn new(screen: &impl Screen) -> Result { + pub async fn new( + screen: &impl Screen, + ) -> Result { let instance = wgpu::Instance::new(wgpu::Backends::PRIMARY); + // + // NOTE: The implementation of the integration with `egui` is + // likely to need to change "significantly"[0] depending + // on what architecture approach is chosen going + // forward. + // + // The current implementation is somewhat complicated by + // virtue of "sitting somewhere in the middle" in + // relation to being neither a standalone integration + // nor fully using `egui` as a framework. + // + // This is a result of a combination of the current + // integration being "proof of concept" level; and, using + // `egui-winit` & `egui-wgpu` which are both relatively + // new additions to the core `egui` ecosystem. + // + // It is recommended to read the following for additional + // helpful context for choosing an architecture: + // + // * + // + // * + // + // [0] By way of specific example, the recent addition + // of Android support lead to considerable API + // change related to `wgpu` & `winit`, see: + // + // * + // + + // + // NOTE: If at some point you use `Painter` or similar and you + // get this error: + // + // `VK_ERROR_NATIVE_WINDOW_IN_USE_KHR` + // + // and/or: + // + // `wgpu_core::device: surface configuration failed: Native window is in use` + // + // it's *probably(?)* because the swapchain has already + // been created for the window (e.g. by an integration) + // and *not* because of a regression of this issue + // (probably): + // + // + // + // Don't ask me how I know. + // + + let egui_winit_state = egui_winit::State::new(4096, screen.window()); + let egui_context = egui::Context::default(); + // This is sound, as `window` is an object to create a surface upon. let surface = unsafe { instance.create_surface(screen.window()) }; @@ -138,6 +219,24 @@ impl Renderer { let config_ui = ConfigUi::new(&device, color_format)?; + // + // Note: We need to hold on to this otherwise (from my memory) + // it causes the egui font texture to get dropped after + // drawing one frame. + // + // This then results in an `egui_wgpu_backend` error of + // `BackendError::Internal` with message: + // + // "Texture 0 used but not live" + // + // See also: + // + let egui_rpass = egui_wgpu::renderer::RenderPass::new( + &device, + surface_config.format, + 1, + ); + Ok(Self { surface, device, @@ -153,6 +252,13 @@ impl Renderer { pipelines, config_ui, + + egui: EguiState { + context: egui_context, + winit_state: egui_winit_state, + rpass: egui_rpass, + options: Default::default(), + }, }) } @@ -185,7 +291,8 @@ impl Renderer { pub fn draw( &mut self, camera: &Camera, - config: &DrawConfig, + config: &mut DrawConfig, + window: &egui_winit::winit::window::Window, ) -> Result<(), DrawError> { let aspect_ratio = self.surface_config.width as f64 / self.surface_config.height as f64; @@ -238,16 +345,222 @@ impl Renderer { ); } - self.config_ui - .draw( - &self.device, - &mut encoder, - &color_view, - &self.surface_config, - &self.geometries.aabb, - config, - ) - .map_err(DrawError::Text)?; + if self.egui.options.show_original_ui { + self.config_ui + .draw( + &self.device, + &mut encoder, + &color_view, + &self.surface_config, + &self.geometries.aabb, + config, + ) + .map_err(DrawError::Text)?; + } + + // + // NOTE: The following comment was written for the original + // proof-of-concept which targeted older versions of + // Fornjot & `egui`, so some details may be outdated & + // not entirely apply to this updated implementation. + // + // It's included here in case it still provides some + // useful context. + // + // + // This integration is basically the result of locating the + // `.present()` call in the `egui` example, here: + // + // + // + // and then the equivalent call in `renderer.rs`, here: + // + // + // + // Then working backwards from there to merge the functionality. + // + // In addition, the following examples were also referenced: + // + // * "Make the example more like an actual use case #17" + // + // This removes some non-essential code from the example + // which helps clarify what's *actually* necessary. + // + // * "Update to 0.17, use official winit backend #18" + // + // This uses a more up-to-date `egui` version which + // included some API changes. + // It's still not the *latest* `egui` version though. + // + + let egui_input = self.egui.winit_state.take_egui_input(window); + self.egui.context.begin_frame(egui_input); + + fn get_bbox_size_text(aabb: &Aabb<3>) -> String { + /* Render size of model bounding box */ + let bbsize = aabb.size().components; + let info = format!( + "Model bounding box size:\n{:0.1} {:0.1} {:0.1}", + bbsize[0].into_f32(), + bbsize[1].into_f32(), + bbsize[2].into_f32() + ); + info + } + + egui::SidePanel::left("fj-left-panel").show(&self.egui.context, |ui| { + ui.add_space(16.0); + + ui.group(|ui| { + ui.checkbox(&mut config.draw_model, "Render model") + .on_hover_text_at_pointer("Toggle with 1"); + ui.checkbox(&mut config.draw_mesh, "Render mesh") + .on_hover_text_at_pointer("Toggle with 2"); + ui.checkbox(&mut config.draw_debug, "Render debug") + .on_hover_text_at_pointer("Toggle with 3"); + ui.checkbox( + &mut self.egui.options.show_original_ui, + "Render original UI", + ); + ui.add_space(16.0); + ui.strong(get_bbox_size_text(&self.geometries.aabb)); + }); + + ui.add_space(16.0); + + { + ui.group(|ui| { + ui.checkbox( + &mut self.egui.options.show_settings_ui, + "Show egui settings UI", + ); + if self.egui.options.show_settings_ui { + self.egui.context.settings_ui(ui); + } + }); + + ui.add_space(16.0); + + ui.group(|ui| { + ui.checkbox( + &mut self.egui.options.show_inspection_ui, + "Show egui inspection UI", + ); + if self.egui.options.show_inspection_ui { + ui.indent("indent-inspection-ui", |ui| { + self.egui.context.inspection_ui(ui); + }); + } + }); + } + + ui.add_space(16.0); + + { + // + // Originally this was only meant to be a simple demonstration + // of the `egui` `trace!()` macro... + // + // ...but it seems the trace feature can't be enabled + // separately from the layout debug feature, which all + // gets a bit messy... + // + // ...so, this instead shows one possible way to implement + // "trace only" style debug text on hover. + // + ui.group(|ui| { + let label_text = format!( + "Show debug text demo.{}", + if self.egui.options.show_debug_text_example { + " (Hover me.)" + } else { + "" + } + ); + + ui.style_mut().wrap = Some(false); + + if ui + .checkbox( + &mut self.egui.options.show_debug_text_example, + label_text, + ) + .hovered() + && self.egui.options.show_debug_text_example + { + let hover_pos = + ui.input().pointer.hover_pos().unwrap_or_default(); + ui.painter().debug_text( + hover_pos, + egui::Align2::LEFT_TOP, + egui::Color32::DEBUG_COLOR, + format!("{:#?}", &config), + ); + } + }); + } + + ui.add_space(16.0); + + { + // + // Demonstration of the `egui` layout debug functionality. + // + ui.group(|ui| { + // + + if ui + .checkbox( + &mut self.egui.options.show_layout_debug_on_hover, + "Show layout debug on hover.", + ) + .changed() + { + ui.ctx().set_debug_on_hover( + self.egui.options.show_layout_debug_on_hover, + ); + } + + ui.scope(|ui| { + if self.egui.options.show_trace { + egui::trace!(ui, format!("{:?}", &config)); + } + }); + + ui.indent("indent-show-trace", |ui| { + ui.set_enabled( + self.egui.options.show_layout_debug_on_hover, + ); + + ui.checkbox( + &mut self.egui.options.show_trace, + "Also show egui trace.", + ); + + // + }); + }); + } + + ui.add_space(16.0); + }); + + // End the UI frame. We could now handle the output and draw the UI with the backend. + let egui_output = self.egui.context.end_frame(); + let egui_paint_jobs = self.egui.context.tessellate(egui_output.shapes); + + self.paint_and_update_textures( + // + // Note: `scale_factor` can be overridden via `WINIT_X11_SCALE_FACTOR` environment variable, + // see: + // + window.scale_factor() as f32, + egui::Rgba::TRANSPARENT, + &egui_paint_jobs, + &egui_output.textures_delta, + &color_view, + &mut encoder, + ); let command_buffer = encoder.finish(); self.queue.submit(Some(command_buffer)); @@ -348,3 +661,97 @@ pub enum DrawError { /// Text rasterisation error. Text(String), } + +impl Renderer { + // + // Note: `egui` changed how it handles updating textures on + // the GPU between v0.17.0 & v0.18.0, this means we can't + // use the same approach as original proof-of-concept used. + // + // Unfortunately we can't use the helper function provided + // by `egui` here, as it is tightly integrated with `Painter` + // which assumes it is handling surface creation itself. + // + // Additionally, subsequent code changes significantly + // changed the API but haven't yet been released. + // + // And, to top it all off, the `Painter::paint_and_update_textures()` + // as it currently exists doesn't support a transparent + // clear color, which we rely on to overlay the UI on the + // already rendered model. + // + // So, as an interim measure, this code is a copy of the + // texture update code from . + // + // Update: Added transparency workaround. + // + fn paint_and_update_textures( + &mut self, + pixels_per_point: f32, + clear_color: egui::Rgba, + clipped_primitives: &[egui::ClippedPrimitive], + textures_delta: &egui::TexturesDelta, + output_view: &wgpu::TextureView, + encoder: &mut wgpu::CommandEncoder, + ) { + // Upload all resources for the GPU. + let screen_descriptor = egui_wgpu::renderer::ScreenDescriptor { + size_in_pixels: [ + self.surface_config.width, + self.surface_config.height, + ], + pixels_per_point, + }; + + for (id, image_delta) in &textures_delta.set { + self.egui.rpass.update_texture( + &self.device, + &self.queue, + *id, + image_delta, + ); + } + for id in &textures_delta.free { + self.egui.rpass.free_texture(id); + } + + self.egui.rpass.update_buffers( + &self.device, + &self.queue, + clipped_primitives, + &screen_descriptor, + ); + + // + // This approach is based on the original proof-of-concept + // integration which used `egui_wgpu_backend` and included + // the following comment for context: + // + // "Set this to `None` to overlay the UI on top of what's in the framebuffer" + // via + // + // Alternatively, for initial testing, you can use a colour without alpha + // (e.g. `Some(wgpu::Color {r:0.5, g:0.0, b:0.0, a:1.0})` ) in order + // to verify that the renderpass is doing *something*. + // + let clear_color_ = if clear_color == egui::Rgba::TRANSPARENT { + None + } else { + Some(wgpu::Color { + r: clear_color.r() as f64, + g: clear_color.g() as f64, + b: clear_color.b() as f64, + a: clear_color.a() as f64, + }) + }; + + // Record all render passes. + self.egui.rpass.execute( + encoder, + output_view, + clipped_primitives, + &screen_descriptor, + clear_color_, + ); + } +} diff --git a/crates/fj-window/src/run.rs b/crates/fj-window/src/run.rs index d48949754..a732d6026 100644 --- a/crates/fj-window/src/run.rs +++ b/crates/fj-window/src/run.rs @@ -84,6 +84,37 @@ pub fn run( } } + // + + if let Event::WindowEvent { + event: window_event, + .. + } = &event + { + // + // Note: In theory we could/should check if `egui` wants "exclusive" use + // of this event here. + // + // But with the current integration with Fornjot we're kinda blurring + // the lines between "app" and "platform", so for the moment we pass + // every event to both `egui` & Fornjot. + // + // The primary visible impact of this currently is that if you drag + // a title bar that overlaps the model then both the model & window + // get moved. + // + // TODO: Revisit this. + // + // TODO: Encapsulate the egui state/context access better. + // + renderer + .egui + .winit_state + .on_event(&renderer.egui.context, window_event); + } + + // + let event = match event { Event::WindowEvent { event: WindowEvent::CloseRequested, @@ -194,7 +225,9 @@ pub fn run( if let (Some(shape), Some(camera)) = (&shape, &mut camera) { camera.update_planes(&shape.aabb); - if let Err(err) = renderer.draw(camera, &draw_config) { + if let Err(err) = + renderer.draw(camera, &mut draw_config, window.window()) + { warn!("Draw error: {}", err); } }