1use std::ops::Range;
4
5use super::*;
6use crate::{FrameResources, wgpu};
7use dear_imgui_rs::{
8 TextureId,
9 render::{DrawData, DrawIdx, DrawRequirements, RawCallbackCommand},
10};
11
12const 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#[derive(Copy, Clone, Debug, Eq, PartialEq)]
26pub struct FramebufferExtent {
27 width: u32,
28 height: u32,
29}
30
31impl FramebufferExtent {
32 pub const fn new(width: u32, height: u32) -> Self {
34 Self { width, height }
35 }
36
37 pub fn from_texture(texture: &wgpu::Texture) -> Self {
39 let size = texture.size();
40 Self::new(size.width, size.height)
41 }
42
43 pub const fn width(self) -> u32 {
45 self.width
46 }
47
48 pub const fn height(self) -> u32 {
50 self.height
51 }
52
53 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 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}