Skip to main content

dear_imgui_wgpu/renderer/
draw.rs

1// Renderer draw helpers: preflight, resource preparation, and one command executor.
2
3use std::ops::Range;
4
5use super::*;
6use crate::{FrameResources, wgpu};
7use dear_imgui_rs::{
8    TextureId,
9    render::{DrawData, DrawIdx, DrawRequirements, RawCallbackCommand},
10};
11
12// ImGui index type is currently u16 in dear-imgui-rs, but keep this derived so
13// future upgrades to u32 require fewer backend changes.
14const IMGUI_INDEX_FORMAT: wgpu::IndexFormat = if std::mem::size_of::<DrawIdx>() == 2 {
15    wgpu::IndexFormat::Uint16
16} else {
17    wgpu::IndexFormat::Uint32
18};
19
20/// Physical dimensions of the WGPU render target receiving Dear ImGui commands.
21///
22/// WGPU render passes do not expose attachment dimensions. Applications must therefore pass the
23/// extent of the texture view used to create the render pass instead of asking the renderer to
24/// infer it from Dear ImGui's logical display metrics.
25#[derive(Copy, Clone, Debug, Eq, PartialEq)]
26pub struct FramebufferExtent {
27    width: u32,
28    height: u32,
29}
30
31impl FramebufferExtent {
32    /// Creates an extent. A zero width or height represents a target that cannot be drawn yet.
33    pub const fn new(width: u32, height: u32) -> Self {
34        Self { width, height }
35    }
36
37    /// Returns the extent of a WGPU texture.
38    pub fn from_texture(texture: &wgpu::Texture) -> Self {
39        let size = texture.size();
40        Self::new(size.width, size.height)
41    }
42
43    /// Returns the physical width in pixels.
44    pub const fn width(self) -> u32 {
45        self.width
46    }
47
48    /// Returns the physical height in pixels.
49    pub const fn height(self) -> u32 {
50        self.height
51    }
52
53    /// Returns whether the target has no drawable area.
54    pub const fn is_empty(self) -> bool {
55        self.width == 0 || self.height == 0
56    }
57
58    fn width_f32(self) -> f32 {
59        self.width as f32
60    }
61
62    fn height_f32(self) -> f32 {
63        self.height as f32
64    }
65}
66
67fn project_scissor_rect(
68    clip_rect: [f32; 4],
69    clip_off: [f32; 2],
70    clip_scale: [f32; 2],
71    extent: FramebufferExtent,
72) -> RendererResult<Option<[u32; 4]>> {
73    let transformed = [
74        (clip_rect[0] - clip_off[0]) * clip_scale[0],
75        (clip_rect[1] - clip_off[1]) * clip_scale[1],
76        (clip_rect[2] - clip_off[0]) * clip_scale[0],
77        (clip_rect[3] - clip_off[1]) * clip_scale[1],
78    ];
79    if transformed.iter().any(|value| !value.is_finite()) {
80        return Err(RendererError::InvalidRenderState(
81            "draw command contains a non-finite clip rectangle".to_owned(),
82        ));
83    }
84
85    let clip_min_x = transformed[0].max(0.0);
86    let clip_min_y = transformed[1].max(0.0);
87    let clip_max_x = transformed[2].min(extent.width_f32());
88    let clip_max_y = transformed[3].min(extent.height_f32());
89    if clip_max_x <= clip_min_x || clip_max_y <= clip_min_y {
90        return Ok(None);
91    }
92    let scissor = [
93        clip_min_x as u32,
94        clip_min_y as u32,
95        (clip_max_x - clip_min_x) as u32,
96        (clip_max_y - clip_min_y) as u32,
97    ];
98    if scissor[2] == 0 || scissor[3] == 0 {
99        Ok(None)
100    } else {
101        Ok(Some(scissor))
102    }
103}
104
105pub(super) enum PreparedDrawCommand<'draw> {
106    Elements {
107        image_bind_group: wgpu::BindGroup,
108        scissor: [u32; 4],
109        indices: Range<u32>,
110        base_vertex: i32,
111    },
112    ResetRenderState,
113    SetSampler(PreparedSampler),
114    RawCallback(RawCallbackCommand<'draw>),
115}
116
117#[derive(Copy, Clone)]
118pub(super) enum PreparedSampler {
119    Linear,
120    Nearest,
121}
122
123pub(super) struct PreparedDrawData<'draw> {
124    commands: Vec<PreparedDrawCommand<'draw>>,
125    has_elements: bool,
126}
127
128impl PreparedDrawData<'_> {
129    pub(super) fn is_empty(&self) -> bool {
130        self.commands.is_empty()
131    }
132
133    pub(super) fn has_elements(&self) -> bool {
134        self.has_elements
135    }
136}
137
138pub(super) struct PreparedRenderState {
139    pipeline: wgpu::RenderPipeline,
140    vertex_buffer: Option<wgpu::Buffer>,
141    index_buffer: Option<wgpu::Buffer>,
142    linear_common_bind_group: wgpu::BindGroup,
143    nearest_common_bind_group: wgpu::BindGroup,
144}
145
146impl WgpuRenderer {
147    pub(super) fn preflight_draw_callback_support(
148        requirements: DrawRequirements,
149    ) -> RendererResult<()> {
150        #[cfg(target_arch = "wasm32")]
151        if requirements.requires_raw_callback_support() {
152            return Err(RendererError::RawDrawCallbackUnsupported);
153        }
154
155        #[cfg(not(target_arch = "wasm32"))]
156        let _ = requirements;
157
158        Ok(())
159    }
160
161    /// Uploads the frame's vertex and index buffers after command preflight succeeds.
162    pub(super) fn prepare_frame_resources_static(
163        draw_data: &DrawData,
164        frame_resources: &mut FrameResources,
165        device: &wgpu::Device,
166        queue: &wgpu::Queue,
167    ) -> RendererResult<()> {
168        let mut total_vtx_count = 0usize;
169        let mut total_idx_count = 0usize;
170        for draw_list in draw_data.draw_lists() {
171            total_vtx_count = total_vtx_count
172                .checked_add(draw_list.vtx_buffer().len())
173                .ok_or(RendererError::DrawBufferOffsetOverflow { buffer: "vertex" })?;
174            total_idx_count = total_idx_count
175                .checked_add(draw_list.idx_buffer().len())
176                .ok_or(RendererError::DrawBufferOffsetOverflow { buffer: "index" })?;
177        }
178
179        if total_vtx_count == 0 && total_idx_count == 0 {
180            return Ok(());
181        }
182        let mut vertices = Vec::with_capacity(total_vtx_count);
183        let mut indices = Vec::with_capacity(total_idx_count);
184        for draw_list in draw_data.draw_lists() {
185            vertices.extend_from_slice(draw_list.vtx_buffer());
186            indices.extend_from_slice(draw_list.idx_buffer());
187        }
188
189        if total_vtx_count != 0 {
190            frame_resources.ensure_vertex_buffer_capacity(device, total_vtx_count)?;
191            frame_resources.upload_vertex_data(queue, &vertices)?;
192        }
193        if total_idx_count != 0 {
194            frame_resources.ensure_index_buffer_capacity(device, total_idx_count)?;
195            frame_resources.upload_index_data(queue, &indices)?;
196        }
197        Ok(())
198    }
199
200    pub(super) fn prepare_draw_data<'draw>(
201        texture_manager: &WgpuTextureManager,
202        default_texture: &Option<wgpu::TextureView>,
203        draw_data: &'draw DrawData,
204        extent: FramebufferExtent,
205        backend_data: &mut WgpuBackendData,
206    ) -> RendererResult<PreparedDrawData<'draw>> {
207        Self::preflight_draw_callback_support(draw_data.requirements())?;
208
209        let mut commands = Vec::new();
210        let mut global_idx_offset = 0u32;
211        let mut global_vtx_offset = 0i32;
212        let clip_off = draw_data.display_pos();
213        let clip_scale = draw_data.framebuffer_scale();
214        let mut has_elements = false;
215
216        for draw_list in draw_data.draw_lists() {
217            let vertices = draw_list.vtx_buffer();
218            let indices = draw_list.idx_buffer();
219            for command in draw_list.commands() {
220                match command {
221                    dear_imgui_rs::render::DrawCmd::Elements { count, cmd_params } => {
222                        if count == 0 {
223                            continue;
224                        }
225
226                        let local_end = cmd_params.idx_offset.checked_add(count).ok_or(
227                            RendererError::DrawBufferOffsetOverflow {
228                                buffer: "command index",
229                            },
230                        )?;
231                        if local_end > indices.len() {
232                            return Err(RendererError::DrawCommandIndexRangeOutOfBounds {
233                                start: cmd_params.idx_offset,
234                                end: local_end,
235                                len: indices.len(),
236                            });
237                        }
238                        let max_index = indices[cmd_params.idx_offset..local_end]
239                            .iter()
240                            .map(|index| *index as usize)
241                            .max()
242                            .unwrap_or(0);
243                        let referenced_vertex = cmd_params
244                            .vtx_offset
245                            .checked_add(max_index)
246                            .ok_or(RendererError::DrawBufferOffsetOverflow {
247                                buffer: "command vertex",
248                            })?;
249                        if referenced_vertex >= vertices.len() {
250                            return Err(RendererError::DrawCommandVertexOutOfBounds {
251                                index: referenced_vertex,
252                                len: vertices.len(),
253                            });
254                        }
255
256                        let count = u32::try_from(count).map_err(|_| {
257                            RendererError::DrawBufferTooLarge {
258                                buffer: "command index",
259                            }
260                        })?;
261                        let local_index = u32::try_from(cmd_params.idx_offset).map_err(|_| {
262                            RendererError::DrawBufferTooLarge {
263                                buffer: "command index",
264                            }
265                        })?;
266                        let start = global_idx_offset.checked_add(local_index).ok_or(
267                            RendererError::DrawBufferOffsetOverflow {
268                                buffer: "command index",
269                            },
270                        )?;
271                        let end = start.checked_add(count).ok_or(
272                            RendererError::DrawBufferOffsetOverflow {
273                                buffer: "command index",
274                            },
275                        )?;
276                        let local_vertex = i32::try_from(cmd_params.vtx_offset).map_err(|_| {
277                            RendererError::DrawBufferTooLarge {
278                                buffer: "command vertex",
279                            }
280                        })?;
281                        let base_vertex = global_vtx_offset.checked_add(local_vertex).ok_or(
282                            RendererError::DrawBufferOffsetOverflow {
283                                buffer: "command vertex",
284                            },
285                        )?;
286
287                        let Some(scissor) = project_scissor_rect(
288                            cmd_params.clip_rect,
289                            clip_off,
290                            clip_scale,
291                            extent,
292                        )?
293                        else {
294                            continue;
295                        };
296
297                        let texture_id = cmd_params.texture_id;
298                        let (cache_id, texture_view) = if texture_id.is_null() {
299                            (
300                                TextureId::null(),
301                                default_texture.as_ref().ok_or_else(|| {
302                                    RendererError::InvalidRenderState(
303                                        "default WGPU texture is not available".to_owned(),
304                                    )
305                                })?,
306                            )
307                        } else {
308                            (
309                                texture_id,
310                                texture_manager
311                                    .texture_view(texture_id)
312                                    .ok_or(RendererError::InvalidTextureId(texture_id))?,
313                            )
314                        };
315                        let image_bind_group = backend_data
316                            .render_resources
317                            .get_or_create_image_bind_group(
318                                &backend_data.device,
319                                cache_id,
320                                texture_view,
321                            )?
322                            .clone();
323
324                        commands.push(PreparedDrawCommand::Elements {
325                            image_bind_group,
326                            scissor,
327                            indices: start..end,
328                            base_vertex,
329                        });
330                        has_elements = true;
331                    }
332                    dear_imgui_rs::render::DrawCmd::ResetRenderState => {
333                        commands.push(PreparedDrawCommand::ResetRenderState);
334                    }
335                    dear_imgui_rs::render::DrawCmd::SetSamplerLinear => {
336                        commands.push(PreparedDrawCommand::SetSampler(PreparedSampler::Linear));
337                    }
338                    dear_imgui_rs::render::DrawCmd::SetSamplerNearest => {
339                        commands.push(PreparedDrawCommand::SetSampler(PreparedSampler::Nearest));
340                    }
341                    dear_imgui_rs::render::DrawCmd::RawCallback(callback) => {
342                        commands.push(PreparedDrawCommand::RawCallback(callback));
343                    }
344                }
345            }
346
347            let index_count = u32::try_from(indices.len())
348                .map_err(|_| RendererError::DrawBufferTooLarge { buffer: "index" })?;
349            global_idx_offset = global_idx_offset
350                .checked_add(index_count)
351                .ok_or(RendererError::DrawBufferOffsetOverflow { buffer: "index" })?;
352            let vertex_count = i32::try_from(vertices.len())
353                .map_err(|_| RendererError::DrawBufferTooLarge { buffer: "vertex" })?;
354            global_vtx_offset = global_vtx_offset
355                .checked_add(vertex_count)
356                .ok_or(RendererError::DrawBufferOffsetOverflow { buffer: "vertex" })?;
357        }
358
359        Ok(PreparedDrawData {
360            commands,
361            has_elements,
362        })
363    }
364
365    pub(super) fn prepare_render_state_static(
366        draw_data: &DrawData,
367        backend_data: &mut WgpuBackendData,
368        gamma: f32,
369        has_elements: bool,
370    ) -> RendererResult<PreparedRenderState> {
371        let pipeline = backend_data
372            .pipeline_state
373            .as_ref()
374            .ok_or_else(|| RendererError::InvalidRenderState("Pipeline not created".to_owned()))?
375            .clone();
376        let device = backend_data.device.clone();
377        let queue = backend_data.queue.clone();
378        let frame_resources = backend_data.acquire_frame_resources()?;
379        Self::prepare_frame_resources_static(draw_data, frame_resources, &device, &queue)?;
380        let vertex_buffer = frame_resources.vertex_buffer().cloned();
381        let index_buffer = frame_resources.index_buffer().cloned();
382        if has_elements && (vertex_buffer.is_none() || index_buffer.is_none()) {
383            return Err(RendererError::InvalidRenderState(
384                "draw elements require initialized vertex and index buffers".to_owned(),
385            ));
386        }
387
388        let matrix =
389            Uniforms::create_orthographic_matrix(draw_data.display_pos(), draw_data.display_size());
390        let mut uniforms = Uniforms::new();
391        uniforms.update(matrix, gamma);
392        let uniform = frame_resources.uniform_buffer()?;
393        uniform.update(&queue, &uniforms);
394
395        Ok(PreparedRenderState {
396            pipeline,
397            vertex_buffer,
398            index_buffer,
399            linear_common_bind_group: uniform.bind_group().clone(),
400            nearest_common_bind_group: frame_resources.nearest_common_bind_group()?.clone(),
401        })
402    }
403
404    fn setup_prepared_render_state(
405        render_pass: &mut wgpu::RenderPass<'_>,
406        extent: FramebufferExtent,
407        state: &PreparedRenderState,
408    ) {
409        render_pass.set_viewport(0.0, 0.0, extent.width_f32(), extent.height_f32(), 0.0, 1.0);
410        render_pass.set_pipeline(&state.pipeline);
411        render_pass.set_bind_group(0, &state.linear_common_bind_group, &[]);
412        if let (Some(vertex_buffer), Some(index_buffer)) =
413            (&state.vertex_buffer, &state.index_buffer)
414        {
415            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
416            render_pass.set_index_buffer(index_buffer.slice(..), IMGUI_INDEX_FORMAT);
417        }
418    }
419
420    pub(super) fn execute_prepared_draw_data(
421        prepared: PreparedDrawData<'_>,
422        state: &PreparedRenderState,
423        extent: FramebufferExtent,
424        render_pass: &mut wgpu::RenderPass<'_>,
425        platform_io: *mut dear_imgui_rs::sys::ImGuiPlatformIO,
426        device: &wgpu::Device,
427    ) -> RendererResult<()> {
428        unsafe {
429            RendererRenderStateGuard::<crate::WgpuRenderStateStorage>::preflight(platform_io)
430        }
431        .map_err(super::map_renderer_render_state_error)?;
432        Self::setup_prepared_render_state(render_pass, extent, state);
433
434        let mut callback_state = crate::WgpuRenderStateStorage::new(device, render_pass);
435        let guard = unsafe { RendererRenderStateGuard::install(platform_io, &mut callback_state) }
436            .map_err(super::map_renderer_render_state_error)?;
437
438        for command in prepared.commands {
439            match command {
440                PreparedDrawCommand::Elements {
441                    image_bind_group,
442                    scissor,
443                    indices,
444                    base_vertex,
445                } => {
446                    render_pass.set_bind_group(1, &image_bind_group, &[]);
447                    render_pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]);
448                    render_pass.draw_indexed(indices, base_vertex, 0..1);
449                }
450                PreparedDrawCommand::ResetRenderState => {
451                    Self::setup_prepared_render_state(render_pass, extent, state)
452                }
453                PreparedDrawCommand::SetSampler(sampler) => {
454                    let bind_group = match sampler {
455                        PreparedSampler::Linear => &state.linear_common_bind_group,
456                        PreparedSampler::Nearest => &state.nearest_common_bind_group,
457                    };
458                    render_pass.set_bind_group(0, bind_group, &[]);
459                }
460                PreparedDrawCommand::RawCallback(callback) => {
461                    unsafe { callback.invoke() };
462                    guard
463                        .validate()
464                        .map_err(super::map_renderer_render_state_error)?;
465                }
466            }
467        }
468
469        guard
470            .finish()
471            .map_err(super::map_renderer_render_state_error)
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::{FramebufferExtent, project_scissor_rect};
478
479    #[test]
480    fn scissor_projection_rejects_non_finite_values_before_clamping() {
481        let extent = FramebufferExtent {
482            width: 64,
483            height: 64,
484        };
485        for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
486            let error =
487                project_scissor_rect([invalid, 0.0, 32.0, 32.0], [0.0, 0.0], [1.0, 1.0], extent)
488                    .unwrap_err();
489            assert!(matches!(error, crate::RendererError::InvalidRenderState(_)));
490        }
491    }
492
493    #[test]
494    fn scissor_projection_clamps_only_finite_rectangles() {
495        let extent = FramebufferExtent {
496            width: 64,
497            height: 64,
498        };
499        assert_eq!(
500            project_scissor_rect([-8.0, -4.0, 72.0, 68.0], [0.0, 0.0], [1.0, 1.0], extent,)
501                .unwrap(),
502            Some([0, 0, 64, 64])
503        );
504    }
505}