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