Skip to main content

vk_graph_imgui/
lib.rs

1//! Dear ImGui renderer integration for `vk-graph`.
2
3#![deny(missing_docs)]
4#![deny(rustdoc::broken_intra_doc_links)]
5
6/// Common imports for applications using the ImGui integration.
7pub mod prelude {
8    pub use super::{Condition, Frame, ImGui, Image, ImageSource, Ui, imgui};
9}
10
11pub use imgui::{self, Condition, Ui};
12
13type DrawCmdInfo = (usize, [f32; 4], usize, usize, TextureId);
14
15use {
16    bytemuck::cast_slice,
17    imgui::{Context, DrawCmd, DrawCmdParams, TextureId},
18    imgui_winit_support::{
19        winit::{event::Event, window::Window},
20        {HiDpiMode, WinitPlatform},
21    },
22    log::warn,
23    std::{collections::HashMap, marker::PhantomData, sync::Arc, time::Duration},
24    vk_graph::{
25        Graph,
26        cmd::{LoadOp, StoreOp},
27        driver::{
28            ash::vk,
29            buffer::{Buffer, BufferInfo},
30            device::Device,
31            graphics::{BlendInfo, GraphicsPipeline, GraphicsPipelineInfo},
32            image::{Image as DriverImage, ImageInfo},
33            shader::Shader,
34            sync::AccessType,
35        },
36        node::{AnyImageNode, ImageLeaseNode, ImageNode},
37        pool::{Lease, Pool},
38    },
39    vk_shader_macros::include_glsl,
40};
41
42/// Dear ImGui renderer state backed by `vk-graph` resources.
43#[derive(Debug)]
44pub struct ImGui {
45    context: Context,
46    font_atlas_image: Option<Arc<Lease<DriverImage>>>,
47    next_texture_id: usize,
48    pipeline: GraphicsPipeline,
49    platform: WinitPlatform,
50    user_images: HashMap<TextureId, AnyImageNode>,
51}
52
53/// Frame-scoped helper for registering user images with ImGui draw commands.
54pub struct Frame<'a> {
55    next_texture_id: &'a mut usize,
56    user_images: &'a mut HashMap<TextureId, AnyImageNode>,
57}
58
59/// A frame-scoped ImGui image registration.
60///
61/// Dropping this value releases the typed handle. The underlying texture binding remains valid
62/// until the current ImGui frame is rendered, because ImGui consumes draw commands after UI code
63/// returns.
64pub struct Image<'a> {
65    id: TextureId,
66    _frame: PhantomData<&'a Frame<'a>>,
67}
68
69/// Image source accepted by [`Frame::image`].
70#[derive(Clone, Copy, Debug)]
71pub struct ImageSource {
72    image: AnyImageNode,
73}
74
75fn supported_draw_cmd(draw_cmd: DrawCmd) -> Option<DrawCmdInfo> {
76    match draw_cmd {
77        DrawCmd::Elements {
78            count,
79            cmd_params:
80                DrawCmdParams {
81                    clip_rect,
82                    idx_offset,
83                    vtx_offset,
84                    texture_id,
85                    ..
86                },
87        } => Some((count, clip_rect, idx_offset, vtx_offset, texture_id)),
88        DrawCmd::ResetRenderState => {
89            warn!("unsupported imgui draw command: reset render state");
90            None
91        }
92        DrawCmd::RawCallback { .. } => {
93            warn!("unsupported imgui draw command: raw callback");
94            None
95        }
96    }
97}
98
99impl<'a> Frame<'a> {
100    /// Registers an image for use by ImGui widgets during this frame.
101    pub fn image(&mut self, image: impl Into<ImageSource>) -> Image<'_> {
102        let id = TextureId::new(*self.next_texture_id);
103        *self.next_texture_id += 1;
104        self.user_images.insert(id, image.into().image);
105
106        Image {
107            id,
108            _frame: PhantomData,
109        }
110    }
111}
112
113impl Image<'_> {
114    /// Returns the ImGui texture id for this image.
115    pub const fn id(&self) -> TextureId {
116        self.id
117    }
118}
119
120impl Drop for Image<'_> {
121    fn drop(&mut self) {}
122}
123
124impl From<AnyImageNode> for ImageSource {
125    fn from(image: AnyImageNode) -> Self {
126        Self { image }
127    }
128}
129
130impl From<ImageLeaseNode> for ImageSource {
131    fn from(image: ImageLeaseNode) -> Self {
132        Self {
133            image: image.into(),
134        }
135    }
136}
137
138impl From<ImageNode> for ImageSource {
139    fn from(image: ImageNode) -> Self {
140        Self {
141            image: image.into(),
142        }
143    }
144}
145
146impl ImGui {
147    /// Creates a new ImGui renderer for the given device.
148    pub fn new(device: &Device) -> Self {
149        let mut context = Context::create();
150        let platform = WinitPlatform::new(&mut context);
151        let pipeline = GraphicsPipeline::create(
152            device,
153            GraphicsPipelineInfo::builder()
154                .blend(BlendInfo::PRE_MULTIPLIED_ALPHA)
155                .cull_mode(vk::CullModeFlags::NONE),
156            [
157                Shader::new_vertex(include_glsl!("res/shader/imgui.vert").as_slice()),
158                Shader::new_fragment(include_glsl!("res/shader/imgui.frag").as_slice()),
159            ],
160        )
161        .expect("invalid imgui pipeline");
162
163        Self {
164            context,
165            font_atlas_image: None,
166            next_texture_id: 1,
167            pipeline,
168            platform,
169            user_images: Default::default(),
170        }
171    }
172
173    /*
174    TODO: This produces an image which is RGBA8 UNORM and has STORAGE set. *We* don't need storage
175    here and should instead ask the user what settings to give the output image.
176    */
177    /// Builds a frame, records the necessary draw commands, and returns the rendered image.
178    pub fn draw<P>(
179        &mut self,
180        dt: f32,
181        events: &[Event<()>],
182        window: &Window,
183        pool: &mut P,
184        graph: &mut Graph,
185        ui_func: impl FnOnce(&mut Frame<'_>, &mut Ui, &mut P, &mut Graph),
186    ) -> ImageLeaseNode
187    where
188        P: Pool<BufferInfo, Buffer> + Pool<ImageInfo, DriverImage>,
189    {
190        let hidpi = self.platform.hidpi_factor();
191
192        self.platform
193            .attach_window(self.context.io_mut(), window, HiDpiMode::Default);
194
195        if self.font_atlas_image.is_none() || self.platform.hidpi_factor() != hidpi {
196            self.lease_font_atlas_image(pool, graph);
197        }
198
199        let io = self.context.io_mut();
200        io.update_delta_time(Duration::from_secs_f32(dt));
201
202        for event in events {
203            self.platform.handle_event(io, window, event);
204        }
205
206        self.platform
207            .prepare_frame(io, window)
208            .expect("invalid imgui frame");
209
210        // Let the caller draw the GUI and register graph images used by image widgets.
211        let ui = self.context.frame();
212        let mut frame = Frame {
213            next_texture_id: &mut self.next_texture_id,
214            user_images: &mut self.user_images,
215        };
216
217        ui_func(&mut frame, ui, pool, graph);
218
219        self.platform.prepare_render(ui, window);
220        let draw_data = self.context.render();
221
222        let image = graph.bind_resource(
223            pool.resource(ImageInfo::image_2d(
224                window.inner_size().width,
225                window.inner_size().height,
226                vk::Format::R8G8B8A8_UNORM,
227                vk::ImageUsageFlags::COLOR_ATTACHMENT
228                    | vk::ImageUsageFlags::SAMPLED
229                    | vk::ImageUsageFlags::STORAGE
230                    | vk::ImageUsageFlags::TRANSFER_DST
231                    | vk::ImageUsageFlags::TRANSFER_SRC, /* TODO: Make TRANSFER_SRC an
232                                                          * "extra flags" */
233            ))
234            .expect("missing imgui output image")
235            .with_debug_name("ImGui Output"),
236        );
237        let font_atlas_image = graph.bind_resource(
238            self.font_atlas_image
239                .as_ref()
240                .expect("missing imgui font atlas image"),
241        );
242        let display_pos = draw_data.display_pos;
243        let framebuffer_scale = draw_data.framebuffer_scale;
244
245        graph.clear_color_image(image, [0f32; 4]);
246
247        if draw_data.draw_lists_count() == 0 {
248            return image;
249        }
250
251        for draw_list in draw_data.draw_lists() {
252            let indices = cast_slice(draw_list.idx_buffer());
253            let mut index_buf = pool
254                .resource(BufferInfo::host_mem(
255                    indices.len() as _,
256                    vk::BufferUsageFlags::INDEX_BUFFER,
257                ))
258                .expect("missing imgui index buffer");
259
260            {
261                Buffer::mapped_slice_mut(&mut index_buf)[0..indices.len()].copy_from_slice(indices);
262            }
263
264            let index_buf = graph.bind_resource(index_buf);
265
266            let vertices = draw_list.vtx_buffer();
267            let vertex_buf_len = vertices.len() * 20;
268            let mut vertex_buf = pool
269                .resource(BufferInfo::host_mem(
270                    vertex_buf_len as _,
271                    vk::BufferUsageFlags::VERTEX_BUFFER,
272                ))
273                .expect("missing imgui vertex buffer");
274
275            {
276                let vertex_buf = Buffer::mapped_slice_mut(&mut vertex_buf);
277                for (idx, vertex) in vertices.iter().enumerate() {
278                    let offset = idx * 20;
279                    vertex_buf[offset..offset + 8].copy_from_slice(cast_slice(&vertex.pos));
280                    vertex_buf[offset + 8..offset + 16].copy_from_slice(cast_slice(&vertex.uv));
281                    vertex_buf[offset + 16..offset + 20].copy_from_slice(&vertex.col);
282                }
283            }
284
285            let vertex_buf = graph.bind_resource(vertex_buf);
286
287            let draw_cmds = draw_list
288                .commands()
289                .filter_map(supported_draw_cmd)
290                .collect::<Vec<_>>();
291
292            let window_width =
293                self.platform.hidpi_factor() as f32 / window.inner_size().width as f32;
294            let window_height =
295                self.platform.hidpi_factor() as f32 / window.inner_size().height as f32;
296
297            for (index_count, clip_rect, first_index, vertex_offset, texture_id) in draw_cmds {
298                let texture = self
299                    .user_images
300                    .get(&texture_id)
301                    .copied()
302                    .unwrap_or_else(|| font_atlas_image.into());
303                let clip_rect = [
304                    (clip_rect[0] - display_pos[0]) * framebuffer_scale[0],
305                    (clip_rect[1] - display_pos[1]) * framebuffer_scale[1],
306                    (clip_rect[2] - display_pos[0]) * framebuffer_scale[0],
307                    (clip_rect[3] - display_pos[1]) * framebuffer_scale[1],
308                ];
309                let x = clip_rect[0].floor() as i32;
310                let y = clip_rect[1].floor() as i32;
311                let width = (clip_rect[2] - clip_rect[0]).ceil() as u32;
312                let height = (clip_rect[3] - clip_rect[1]).ceil() as u32;
313
314                graph
315                    .begin_cmd()
316                    .debug_name("imgui")
317                    .bind_pipeline(&self.pipeline)
318                    .resource_access(index_buf, AccessType::IndexBuffer)
319                    .resource_access(vertex_buf, AccessType::VertexBuffer)
320                    .shader_resource_access(
321                        0,
322                        texture,
323                        AccessType::FragmentShaderReadSampledImageOrUniformTexelBuffer,
324                    )
325                    .color_attachment_image(0, image, LoadOp::Load, StoreOp::Store)
326                    .record_cmd(move |cmd| {
327                        cmd.push_constants(0, &window_width.to_ne_bytes())
328                            .push_constants(4, &window_height.to_ne_bytes())
329                            .bind_index_buffer(index_buf, 0, vk::IndexType::UINT16)
330                            .bind_vertex_buffer(0, vertex_buf, 0)
331                            .set_scissor(
332                                0,
333                                &[vk::Rect2D {
334                                    offset: vk::Offset2D { x, y },
335                                    extent: vk::Extent2D { width, height },
336                                }],
337                            )
338                            .draw_indexed(
339                                index_count as _,
340                                1,
341                                first_index as _,
342                                vertex_offset as _,
343                                0,
344                            );
345                    });
346            }
347        }
348
349        self.user_images.clear();
350        self.next_texture_id = 1;
351
352        image
353    }
354
355    fn lease_font_atlas_image<P>(&mut self, pool: &mut P, graph: &mut Graph)
356    where
357        P: Pool<BufferInfo, Buffer> + Pool<ImageInfo, DriverImage>,
358    {
359        use imgui::{FontConfig, FontGlyphRanges, FontSource};
360
361        let hidpi_factor = self.platform.hidpi_factor();
362        self.context.io_mut().font_global_scale = (1.0 / hidpi_factor) as f32;
363
364        let font_size = (14.0 * hidpi_factor) as f32;
365        let fonts = self.context.fonts();
366        fonts.clear_fonts();
367        fonts.add_font(&[
368            FontSource::TtfData {
369                data: include_bytes!("../res/font/roboto/roboto-regular.ttf"),
370                size_pixels: font_size,
371                config: Some(FontConfig {
372                    rasterizer_multiply: 2.0,
373                    glyph_ranges: FontGlyphRanges::japanese(),
374                    ..FontConfig::default()
375                }),
376            },
377            FontSource::TtfData {
378                data: include_bytes!("../res/font/mplus-1p/mplus-1p-regular.ttf"),
379                size_pixels: font_size,
380                config: Some(FontConfig {
381                    oversample_h: 2,
382                    oversample_v: 2,
383                    // Range of glyphs to rasterize
384                    glyph_ranges: FontGlyphRanges::japanese(),
385                    ..FontConfig::default()
386                }),
387            },
388        ]);
389
390        let texture = fonts.build_rgba32_texture(); // TODO: Fix fb channel writes and use alpha8!
391        let temp_buf_len = texture.data.len();
392        let mut temp_buf = pool
393            .resource(BufferInfo::host_mem(
394                temp_buf_len as _,
395                vk::BufferUsageFlags::TRANSFER_SRC,
396            ))
397            .expect("missing imgui font atlas buffer");
398
399        {
400            let temp_buf = Buffer::mapped_slice_mut(&mut temp_buf);
401            temp_buf[0..temp_buf_len].copy_from_slice(texture.data);
402        }
403
404        let temp_buf = graph.bind_resource(temp_buf);
405        let image = pool
406            .resource(ImageInfo::image_2d(
407                texture.width,
408                texture.height,
409                vk::Format::R8G8B8A8_UNORM,
410                vk::ImageUsageFlags::SAMPLED
411                    | vk::ImageUsageFlags::STORAGE
412                    | vk::ImageUsageFlags::TRANSFER_DST,
413            ))
414            .expect("missing imgui font atlas image")
415            .with_debug_name("ImGui Font Atlas");
416
417        let image = graph.bind_resource(image);
418
419        graph.copy_buffer_to_image(temp_buf, image);
420
421        self.font_atlas_image = Some(graph.resource(image).clone());
422    }
423}
424
425#[cfg(test)]
426mod test {
427    use super::supported_draw_cmd;
428    use imgui::{DrawCmd, DrawCmdParams, TextureId};
429
430    #[test]
431    fn supported_draw_cmd_extracts_element_draws() {
432        let draw_cmd = DrawCmd::Elements {
433            count: 42,
434            cmd_params: DrawCmdParams {
435                clip_rect: [1.0, 2.0, 3.0, 4.0],
436                texture_id: TextureId::new(7),
437                vtx_offset: 5,
438                idx_offset: 6,
439            },
440        };
441
442        assert_eq!(
443            supported_draw_cmd(draw_cmd),
444            Some((42, [1.0, 2.0, 3.0, 4.0], 6, 5, TextureId::new(7)))
445        );
446    }
447
448    #[test]
449    fn supported_draw_cmd_skips_reset_render_state() {
450        assert_eq!(supported_draw_cmd(DrawCmd::ResetRenderState), None);
451    }
452}