Skip to main content

vk_graph_egui/
lib.rs

1//! `egui` renderer integration for `vk-graph`.
2
3#![deny(missing_docs)]
4#![deny(rustdoc::broken_intra_doc_links)]
5
6pub use egui;
7use vk_graph::{
8    cmd::{LoadOp, StoreOp},
9    driver::{
10        ash::vk,
11        buffer::BufferInfo,
12        device::Device,
13        graphics::{BlendInfo, GraphicsPipeline, GraphicsPipelineInfo},
14        image::{Image, ImageInfo},
15        shader::Shader,
16        sync::AccessType,
17    },
18    node::AnyImageNode,
19    pool::{Lease, Pool as _, hash::HashPool},
20};
21
22use {
23    bytemuck::cast_slice,
24    egui_winit::winit::{event::Event, raw_window_handle::HasDisplayHandle, window::Window},
25    std::{borrow::Cow, collections::HashMap, sync::Arc},
26    vk_graph::Graph,
27    vk_shader_macros::include_glsl,
28};
29
30/// Immediate-mode GUI integration for rendering `egui` output with `vk-graph`.
31pub struct Egui {
32    /// The shared `egui` context used to build and retain UI state.
33    pub ctx: egui::Context,
34
35    egui_winit: egui_winit::State,
36    textures: HashMap<egui::TextureId, Arc<Lease<Image>>>,
37    cache: HashPool,
38    ppl: GraphicsPipeline,
39    next_tex_id: u64,
40    user_textures: HashMap<egui::TextureId, AnyImageNode>,
41}
42
43impl Egui {
44    /// Creates a new `egui` renderer for the given device and display target.
45    pub fn new(device: &Device, display_target: &dyn HasDisplayHandle) -> Self {
46        let ppl = GraphicsPipeline::create(
47            device,
48            GraphicsPipelineInfo::builder()
49                .blend(BlendInfo {
50                    blend_enable: true,
51                    src_color_blend_factor: vk::BlendFactor::ONE,
52                    dst_color_blend_factor: vk::BlendFactor::ONE_MINUS_SRC_ALPHA,
53                    color_blend_op: vk::BlendOp::ADD,
54                    src_alpha_blend_factor: vk::BlendFactor::ONE,
55                    dst_alpha_blend_factor: vk::BlendFactor::ONE,
56                    alpha_blend_op: vk::BlendOp::ADD,
57                    color_write_mask: vk::ColorComponentFlags::R
58                        | vk::ColorComponentFlags::G
59                        | vk::ColorComponentFlags::B
60                        | vk::ColorComponentFlags::A,
61                })
62                .cull_mode(vk::CullModeFlags::NONE),
63            [
64                Shader::new_vertex(include_glsl!("shaders/egui.vert").as_slice()),
65                Shader::new_fragment(include_glsl!("shaders/egui.frag").as_slice()),
66            ],
67        )
68        .expect("invalid egui pipeline");
69
70        let ctx = egui::Context::default();
71        let max_texture_side = Some(
72            device
73                .physical
74                .properties_v1_0
75                .limits
76                .max_image_dimension2_d as usize,
77        );
78        let egui_winit = egui_winit::State::new(
79            ctx.clone(),
80            egui::ViewportId::ROOT,
81            display_target,
82            None,
83            None,
84            max_texture_side,
85        );
86
87        Self {
88            ppl,
89            ctx,
90            egui_winit,
91            textures: HashMap::default(),
92            cache: HashPool::new(device),
93            next_tex_id: 0,
94            user_textures: HashMap::default(),
95        }
96    }
97
98    fn bind_and_update_textures(
99        &mut self,
100        deltas: &egui::TexturesDelta,
101        graph: &mut Graph,
102    ) -> HashMap<egui::TextureId, AnyImageNode> {
103        let mut bound_tex = deltas
104            .set
105            .iter()
106            .map(|(id, delta)| {
107                let pixels = match &delta.image {
108                    egui::ImageData::Color(image) => {
109                        assert_eq!(image.width() * image.height(), image.pixels.len());
110                        Cow::Borrowed(&image.pixels)
111                    }
112                };
113
114                let tmp_buf = {
115                    let mut buf = self
116                        .cache
117                        .resource(BufferInfo::host_mem(
118                            (pixels.len() * delta.image.bytes_per_pixel()) as u64,
119                            vk::BufferUsageFlags::TRANSFER_SRC,
120                        ))
121                        .expect("missing egui texture upload buffer");
122                    buf.copy_from_slice(0, cast_slice(&pixels));
123                    graph.bind_resource(buf)
124                };
125
126                if let Some(pos) = delta.pos {
127                    let image =
128                        graph.bind_resource(self.textures.remove(id).expect("missing texture"));
129
130                    graph
131                        .begin_cmd()
132                        .debug_name("copy buffer to image")
133                        .copy_buffer_to_image(
134                            tmp_buf,
135                            image,
136                            [vk::BufferImageCopy {
137                                buffer_offset: 0,
138                                buffer_row_length: delta.image.width() as u32,
139                                buffer_image_height: delta.image.height() as u32,
140                                image_offset: vk::Offset3D {
141                                    x: pos[0] as i32,
142                                    y: pos[1] as i32,
143                                    z: 0,
144                                },
145                                image_extent: vk::Extent3D {
146                                    width: delta.image.width() as u32,
147                                    height: delta.image.height() as u32,
148                                    depth: 1,
149                                },
150                                image_subresource: vk::ImageSubresourceLayers {
151                                    aspect_mask: vk::ImageAspectFlags::COLOR,
152                                    mip_level: 0,
153                                    base_array_layer: 0,
154                                    layer_count: 1,
155                                },
156                            }],
157                        )
158                        .end_cmd();
159                    (*id, AnyImageNode::from(image))
160                } else {
161                    let image = graph.bind_resource(
162                        self.cache
163                            .resource(ImageInfo::image_2d(
164                                delta.image.width() as u32,
165                                delta.image.height() as u32,
166                                vk::Format::R8G8B8A8_UNORM,
167                                vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST,
168                            ))
169                            .expect("missing egui texture image"),
170                    );
171
172                    graph.copy_buffer_to_image(tmp_buf, image);
173                    (*id, AnyImageNode::from(image))
174                }
175            })
176            .collect::<HashMap<_, _>>();
177
178        // Bind the rest of the textures
179        for (id, image) in self.textures.drain() {
180            bound_tex.insert(id, AnyImageNode::from(graph.bind_resource(image)));
181        }
182
183        // Add user textures
184        for (id, node) in self.user_textures.drain() {
185            bound_tex.insert(id, node);
186        }
187
188        bound_tex
189    }
190
191    fn unbind_and_free(
192        &mut self,
193        bound_tex: HashMap<egui::TextureId, AnyImageNode>,
194        graph: &mut Graph,
195        deltas: &egui::TexturesDelta,
196    ) {
197        // Unbind textures
198        for (id, tex) in bound_tex.iter() {
199            if let AnyImageNode::Pooled(tex) = tex
200                && let egui::TextureId::Managed(_) = *id
201            {
202                self.textures.insert(*id, graph.resource(*tex).clone());
203            }
204        }
205
206        // Free textures
207        for id in deltas.free.iter() {
208            self.textures.remove(id);
209        }
210
211        self.next_tex_id = 0;
212    }
213
214    fn draw_primitive(
215        &mut self,
216        shapes: Vec<egui::epaint::ClippedShape>,
217        bound_tex: &HashMap<egui::TextureId, AnyImageNode>,
218        graph: &mut Graph,
219        target: impl Into<AnyImageNode>,
220    ) {
221        let target = target.into();
222        let target_info = graph.resource(target).info;
223        for egui::ClippedPrimitive {
224            clip_rect,
225            primitive,
226        } in self.ctx.tessellate(shapes, self.ctx.pixels_per_point())
227        {
228            match primitive {
229                egui::epaint::Primitive::Mesh(mesh) => {
230                    // Skip when there are no vertices to paint since we cannot allocate a buffer
231                    // of length 0
232                    if mesh.vertices.is_empty() || mesh.indices.is_empty() {
233                        continue;
234                    }
235                    let texture = bound_tex
236                        .get(&mesh.texture_id)
237                        .expect("missing egui mesh texture");
238
239                    let idx_buf = {
240                        let mut buf = self
241                            .cache
242                            .resource(BufferInfo::host_mem(
243                                (mesh.indices.len() * 4) as u64,
244                                vk::BufferUsageFlags::INDEX_BUFFER,
245                            ))
246                            .expect("missing egui index buffer");
247                        buf.copy_from_slice(0, cast_slice(&mesh.indices));
248                        buf
249                    };
250                    let idx_buf = graph.bind_resource(idx_buf);
251
252                    let vert_buf = {
253                        let mut buf = self
254                            .cache
255                            .resource(BufferInfo::host_mem(
256                                (mesh.vertices.len() * std::mem::size_of::<egui::epaint::Vertex>())
257                                    as u64,
258                                vk::BufferUsageFlags::VERTEX_BUFFER,
259                            ))
260                            .expect("missing egui vertex buffer");
261                        buf.copy_from_slice(0, cast_slice(&mesh.vertices));
262                        buf
263                    };
264                    let vert_buf = graph.bind_resource(vert_buf);
265
266                    #[repr(C)]
267                    #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
268                    struct PushConstants {
269                        screen_size: [f32; 2],
270                    }
271
272                    let pixels_per_point = self.ctx.pixels_per_point();
273
274                    let push_constants = PushConstants {
275                        screen_size: [
276                            target_info.width as f32 / pixels_per_point,
277                            target_info.height as f32 / pixels_per_point,
278                        ],
279                    };
280
281                    let num_indices = mesh.indices.len() as u32;
282
283                    let x = (clip_rect.min.x * pixels_per_point) as i32;
284                    let y = (clip_rect.min.y * pixels_per_point) as i32;
285
286                    let width = ((clip_rect.max.x - clip_rect.min.x) * pixels_per_point) as u32;
287                    let height = ((clip_rect.max.y - clip_rect.min.y) * pixels_per_point) as u32;
288
289                    graph
290                        .begin_cmd()
291                        .debug_name("Egui pass")
292                        .bind_pipeline(&self.ppl)
293                        .resource_access(idx_buf, AccessType::IndexBuffer)
294                        .resource_access(vert_buf, AccessType::VertexBuffer)
295                        .shader_resource_access(0, *texture, AccessType::FragmentShaderReadOther)
296                        .color_attachment_image(0, target, LoadOp::Load, StoreOp::Store)
297                        .record_cmd(move |cmd| {
298                            cmd.bind_index_buffer(idx_buf, 0, vk::IndexType::UINT32)
299                                .bind_vertex_buffer(0, vert_buf, 0)
300                                .push_constants(0, cast_slice(&[push_constants]))
301                                .set_scissor(
302                                    0,
303                                    &[vk::Rect2D {
304                                        offset: vk::Offset2D { x, y },
305                                        extent: vk::Extent2D { width, height },
306                                    }],
307                                )
308                                .draw_indexed(num_indices, 1, 0, 0, 0);
309                        });
310                }
311                _ => panic!("Primitive callback not yet supported."),
312            }
313        }
314    }
315
316    /// Runs one `egui` frame, updates textures, and records draw commands into `graph`.
317    pub fn run(
318        &mut self,
319        window: &Window,
320        events: &[Event<()>],
321        target: impl Into<AnyImageNode>,
322        graph: &mut Graph,
323        ui_fn: impl FnMut(&mut egui::Ui),
324    ) {
325        // Update events and generate shapes and texture deltas
326        for event in events {
327            if let Event::WindowEvent { event, .. } = event {
328                #[allow(unused_must_use)]
329                {
330                    self.egui_winit.on_window_event(window, event);
331                }
332            }
333        }
334        let raw_input = self.egui_winit.take_egui_input(window);
335        let full_output = self.ctx.run_ui(raw_input, ui_fn);
336
337        self.egui_winit
338            .handle_platform_output(window, full_output.platform_output);
339
340        let deltas = full_output.textures_delta;
341
342        let bound_tex = self.bind_and_update_textures(&deltas, graph);
343
344        self.draw_primitive(full_output.shapes, &bound_tex, graph, target);
345
346        self.unbind_and_free(bound_tex, graph, &deltas);
347    }
348
349    /// Registers a user-provided image so it can be referenced from `egui`.
350    pub fn register_texture(&mut self, tex: impl Into<AnyImageNode>) -> egui::TextureId {
351        let id = egui::TextureId::User(self.next_tex_id);
352        self.next_tex_id += 1;
353        self.user_textures.insert(id, tex.into());
354        id
355    }
356}