demo_vk/demo/
egui_integration.rs

1use {
2    crate::{
3        demo::Graphics,
4        graphics::{
5            streaming_renderer::{
6                Mesh, StreamingRenderer, Texture, TextureAtlas, TextureLoader,
7                TrianglesMesh, Vertex,
8            },
9            vulkan::Frame,
10        },
11        unwrap_here,
12    },
13    anyhow::Result,
14    ash::vk,
15    egui::{epaint::Primitive, ImageData, ViewportInfo},
16    egui_winit::EventResponse,
17    nalgebra::Matrix4,
18    std::{collections::HashMap, sync::Arc},
19    winit::{dpi::PhysicalSize, event::WindowEvent, window::Window},
20};
21
22/// The EGUI painter is a complete EGUI integration for use within demos.
23///
24/// It maintains internal state for rendering directly to the swapchain image,
25/// and processing Winit events.
26pub struct EguiPainter {
27    state: egui_winit::State,
28    projection: Matrix4<f32>,
29    used_meshes: Vec<TrianglesMesh>,
30    free_meshes: Vec<TrianglesMesh>,
31    egui_textures: HashMap<egui::TextureId, i32>,
32    texture_loader: TextureLoader,
33    atlas: TextureAtlas,
34    renderer: StreamingRenderer,
35}
36
37/// Builds a projection matrix for the screen based on the current
38/// points_per_pixel and screen dimensions. This ensures that EGUI UI items
39/// correctly adhere to display scaling requirements for high-dpi displays.
40fn ortho_projection(
41    pixels_per_point: f32,
42    width: f32,
43    height: f32,
44) -> Matrix4<f32> {
45    let w = width / pixels_per_point;
46    let h = height / pixels_per_point;
47    #[rustfmt::skip]
48    let projection = Matrix4::new(
49        2.0 / w,  0.0,     0.0, -1.0,
50        0.0,     2.0 / h, 0.0, -1.0,
51        0.0,      0.0,     1.0, 0.0,
52        0.0,      0.0,     0.0, 1.0,
53    );
54    projection
55}
56
57/// Converts an EGUI clipping rect into a Vulkan vk::Rect2D, accounting for
58/// the pixels_per_point setting used by EGUI to scale UI elements.
59fn egui_rect_to_vk_rect(pixels_per_point: f32, rect: egui::Rect) -> vk::Rect2D {
60    vk::Rect2D {
61        offset: vk::Offset2D {
62            x: (pixels_per_point * rect.min.x) as i32,
63            y: (pixels_per_point * rect.min.y) as i32,
64        },
65        extent: vk::Extent2D {
66            width: (pixels_per_point * rect.width()) as u32,
67            height: (pixels_per_point * rect.height()) as u32,
68        },
69    }
70}
71
72impl EguiPainter {
73    pub fn new(graphics: &Graphics, window: &Window) -> Result<Self> {
74        let state = egui_winit::State::new(
75            egui::Context::default(),
76            egui::ViewportId::ROOT,
77            window,
78            Some(window.scale_factor() as f32),
79            None,
80            None,
81        );
82        let pixels_per_point =
83            egui_winit::pixels_per_point(state.egui_ctx(), window);
84        let PhysicalSize { width, height } = window.inner_size();
85        let atlas = unwrap_here!(
86            "Create EGUI painter texture atlas",
87            TextureAtlas::new(&graphics.vulkan)
88        );
89        let texture_loader = unwrap_here!(
90            "Create EGUI texture loader",
91            TextureLoader::new(graphics.vulkan.clone())
92        );
93        let renderer = unwrap_here!(
94            "Create EGUI painter streaming renderer instance",
95            StreamingRenderer::new(
96                &graphics.vulkan,
97                graphics.swapchain.format(),
98                &graphics.frames_in_flight,
99                &atlas
100            )
101        );
102        Ok(Self {
103            state,
104            projection: ortho_projection(
105                pixels_per_point,
106                width as f32,
107                height as f32,
108            ),
109            used_meshes: vec![],
110            free_meshes: vec![],
111            egui_textures: HashMap::new(),
112            texture_loader,
113            atlas,
114            renderer,
115        })
116    }
117
118    /// Processes a window event so the UI will respond to clicks and
119    /// keystrokes.
120    pub fn on_window_event(
121        &mut self,
122        window: &Window,
123        event: &WindowEvent,
124    ) -> EventResponse {
125        if let &WindowEvent::Resized(PhysicalSize { width, height }) = event {
126            self.projection = ortho_projection(
127                egui_winit::pixels_per_point(self.state.egui_ctx(), window),
128                width as f32,
129                height as f32,
130            );
131        }
132        self.state.on_window_event(window, event)
133    }
134
135    /// Checks for compatibility with the swapchain and rebuilds resources if
136    /// needed.
137    pub fn rebuild_swapchain_resources(
138        &mut self,
139        gfx: &Graphics,
140    ) -> Result<()> {
141        if self.renderer.image_format() == gfx.swapchain.format() {
142            return Ok(()); // formats are compatible, nothing to be donwe
143        }
144
145        unwrap_here!(
146            "Wait for frames to finish before rebuilding EGUI painter resources",
147            gfx.frames_in_flight.wait_for_all_frames_to_complete()
148        );
149
150        // free all existing meshes and associated materials
151        self.free_meshes.clear();
152        self.used_meshes.clear();
153
154        self.renderer = unwrap_here!(
155            "Rebuild streaming renderer for the EGUI painter",
156            StreamingRenderer::new(
157                &gfx.vulkan,
158                gfx.swapchain.format(),
159                &gfx.frames_in_flight,
160                &self.atlas
161            )
162        );
163
164        // resources must be rebuilt
165        Ok(())
166    }
167
168    /// Draws the EGUI UI to the currently bound color attachment.
169    ///
170    /// # Safety
171    ///
172    /// - This function assumes that a render pass is already started (dynamic
173    ///   or not)
174    /// - This function assumes that the color attachment 0 is either a
175    ///   swapchain image or an image with the same dimensions and format.
176    /// - This function assumes that the viewport has already been set.
177    pub unsafe fn draw(&mut self, gfx: &Graphics, frame: &Frame) -> Result<()> {
178        self.renderer
179            .bind_texture_atlas(&gfx.vulkan, frame, &self.atlas);
180        unwrap_here!(
181            "Prepare EGUI clipped primitive meshes",
182            self.renderer.prepare_meshes(
183                &gfx.vulkan,
184                frame,
185                &self
186                    .used_meshes
187                    .iter()
188                    .map(|mesh| mesh as &dyn Mesh)
189                    .collect::<Vec<_>>(),
190            )
191        );
192        unwrap_here!(
193            "Write EGUI draw commands to frame command buffer",
194            self.renderer.write_draw_commands(&gfx.vulkan, frame)
195        );
196        Ok(())
197    }
198
199    pub fn run(
200        &mut self,
201        gfx: &Graphics,
202        window: &Window,
203        run_ui: impl FnMut(&egui::Context),
204    ) -> Result<()> {
205        let raw_input = {
206            let mut viewport_info = ViewportInfo::default();
207            egui_winit::update_viewport_info(
208                &mut viewport_info,
209                self.state.egui_ctx(),
210                window,
211                false,
212            );
213            let viewport_id = self.state.egui_input().viewport_id;
214            self.state
215                .egui_input_mut()
216                .viewports
217                .insert(viewport_id, viewport_info);
218
219            let PhysicalSize { width, height } = window.inner_size();
220            self.state.egui_input_mut().screen_rect = Some(egui::Rect {
221                min: egui::pos2(0.0, 0.0),
222                max: egui::pos2(width as f32, height as f32),
223            });
224
225            self.state.take_egui_input(window)
226        };
227        let full_output = self.state.egui_ctx().run(raw_input, run_ui);
228        self.state
229            .handle_platform_output(window, full_output.platform_output);
230
231        // create new textures if needed
232        for (egui_texture_id, delta) in &full_output.textures_delta.set {
233            let needs_replaced = if let Some(existing_texture_id) =
234                self.egui_textures.get(egui_texture_id)
235            {
236                // a texture exists, check to see if it needs to be reallocated
237                // based on the delta size
238                let existing_texture =
239                    self.atlas.get_texture(*existing_texture_id);
240
241                (existing_texture.width() as usize) < delta.image.width()
242                    || (existing_texture.height() as usize)
243                        < delta.image.height()
244            } else {
245                true // texture doesn't exist, so it always "needs replaced"
246            };
247
248            // create a new texture and update the texture map
249            if needs_replaced {
250                // no existing texture, so create a new one
251                log::info!("Creating egui texture: {:?}", egui_texture_id);
252
253                let texture = unwrap_here!(
254                    "Create EGUI texture",
255                    Texture::builder()
256                        .ctx(&gfx.vulkan)
257                        .dimensions((
258                            delta.image.width() as u32,
259                            delta.image.height() as u32,
260                        ))
261                        .format(vk::Format::R8G8B8A8_UNORM)
262                        .memory_property_flags(
263                            vk::MemoryPropertyFlags::DEVICE_LOCAL,
264                        )
265                        .image_usage_flags(
266                            vk::ImageUsageFlags::TRANSFER_DST
267                                | vk::ImageUsageFlags::SAMPLED,
268                        )
269                        .build()
270                );
271
272                self.egui_textures.insert(
273                    *egui_texture_id,
274                    self.atlas.add_texture(&gfx.vulkan, Arc::new(texture)),
275                );
276            }
277        }
278
279        // The previous block created any new textures that might be needed, so
280        // here we just need to write texture data.
281        for (egui_texture_id, delta) in full_output.textures_delta.set {
282            let texture_id = if let Some(texture_id) =
283                self.egui_textures.get(&egui_texture_id)
284            {
285                *texture_id
286            } else {
287                continue;
288            };
289            let texture = self.atlas.get_texture(texture_id);
290            let ImageData::Color(color) = delta.image;
291            let pixels = color
292                .pixels
293                .iter()
294                .flat_map(|color| color.to_srgba_unmultiplied())
295                .collect::<Vec<u8>>();
296            let offset = if let Some([w, h]) = delta.pos {
297                [w as u32, h as u32]
298            } else {
299                [0, 0]
300            };
301            let size = {
302                let [w, h] = color.size;
303                [w as u32, h as u32]
304            };
305            unwrap_here!(
306                "Update image data in EGUI texture",
307                self.texture_loader.tex_sub_image(
308                    &gfx.vulkan,
309                    texture,
310                    &pixels,
311                    offset,
312                    size,
313                )
314            );
315        }
316
317        let pixels_per_point = full_output.pixels_per_point;
318
319        // reset all meshes
320        self.free_meshes.extend(self.used_meshes.drain(0..));
321
322        // Fill mesh with geometry
323        let clipped_primitives = self.state.egui_ctx().tessellate(
324            full_output.shapes,
325            self.state.egui_ctx().pixels_per_point(),
326        );
327
328        for clipped_primitive in &clipped_primitives {
329            let mut triangles_mesh = self.get_next_free_mesh();
330            triangles_mesh.set_scissor(egui_rect_to_vk_rect(
331                pixels_per_point,
332                clipped_primitive.clip_rect,
333            ));
334            match &clipped_primitive.primitive {
335                Primitive::Mesh(mesh) => {
336                    let texture_id = *self
337                        .egui_textures
338                        .get(&mesh.texture_id)
339                        .unwrap_or(&-1);
340                    triangles_mesh.indexed_triangles(
341                        mesh.vertices.iter().map(|egui_vertex| {
342                            Vertex::new(
343                                [egui_vertex.pos.x, egui_vertex.pos.y, 0.0],
344                                [egui_vertex.uv.x, egui_vertex.uv.y],
345                                egui_vertex.color.to_normalized_gamma_f32(),
346                                texture_id,
347                            )
348                        }),
349                        mesh.indices.iter().copied(),
350                    );
351                }
352                Primitive::Callback(_) => {
353                    log::warn!(
354                        "Callbacks unsupported by this backend (currently)"
355                    );
356                }
357            }
358            self.used_meshes.push(triangles_mesh);
359        }
360
361        Ok(())
362    }
363
364    fn get_next_free_mesh(&mut self) -> TrianglesMesh {
365        let mut mesh = self.free_meshes.pop().unwrap_or_else(|| {
366            TrianglesMesh::new(1_000, self.renderer.default_material().clone())
367        });
368        mesh.clear();
369        mesh.set_transform(self.projection);
370        mesh
371    }
372}