dear_imgui_wgpu/renderer/mod.rs
1//! Main WGPU renderer implementation
2//!
3//! This module contains the main WgpuRenderer struct and its implementation,
4//! following the pattern from imgui_impl_wgpu.cpp
5//!
6//! Texture Updates Flow (ImGui 1.92+)
7//! - During `Context::render()`, Dear ImGui emits a list of textures to be processed in
8//! `DrawData::textures()` (see `dear_imgui_rs::render::DrawData::textures`). Each item is an
9//! `ImTextureData*` with a `Status` field:
10//! - `WantCreate`: create a GPU texture, upload all pixels, set `TexID`, then set status `OK`.
11//! - `WantUpdates`: upload `UpdateRect` (and any queued rects) then set `OK`.
12//! - `WantDestroy`: schedule/destroy GPU texture; if unused for some frames, set `Destroyed`.
13//! - This backend honors these transitions in its texture module; users can simply pass
14//! `&mut TextureData` to UI/draw calls and let the backend handle the rest.
15
16use crate::GammaMode;
17use crate::{
18 FrameResources, RenderResources, RendererError, RendererResult, ShaderManager, Uniforms,
19 WgpuBackendData, WgpuInitInfo, WgpuTextureManager,
20};
21use dear_imgui_rs::{BackendFlags, Context, render::DrawData};
22#[cfg(feature = "mv-log")]
23use std::sync::{Mutex, OnceLock};
24use wgpu::*;
25
26// Debug logging helper (off by default). Enable by building this crate with
27// `--features mv-log` to see multi-viewport renderer traces.
28#[allow(unused_macros)]
29macro_rules! mvlog {
30 ($($arg:tt)*) => {
31 if cfg!(feature = "mv-log") { eprintln!($($arg)*); }
32 }
33}
34/// Main WGPU renderer for Dear ImGui
35
36///
37/// This corresponds to the main renderer functionality in imgui_impl_wgpu.cpp
38pub struct WgpuRenderer {
39 /// Backend data
40 backend_data: Option<WgpuBackendData>,
41 /// Shader manager
42 shader_manager: ShaderManager,
43 /// Texture manager
44 texture_manager: WgpuTextureManager,
45 /// Default texture for fallback
46 default_texture: Option<TextureView>,
47 /// Registered font atlas texture id (if created via font-atlas fallback)
48 font_texture_id: Option<u64>,
49 /// Gamma mode: automatic (by format), force linear (1.0), or force 2.2
50 gamma_mode: GammaMode,
51 /// Clear color used for secondary viewports (multi-viewport mode)
52 #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
53 viewport_clear_color: Color,
54}
55
56impl WgpuRenderer {
57 /// Create a new WGPU renderer with full initialization (recommended)
58 ///
59 /// This is the preferred way to create a WGPU renderer as it ensures proper
60 /// initialization order and is consistent with other backends.
61 ///
62 /// # Arguments
63 /// * `init_info` - WGPU initialization information (device, queue, format)
64 /// * `imgui_ctx` - Dear ImGui context to configure
65 ///
66 /// # Example
67 /// ```rust,no_run
68 /// use dear_imgui_rs::Context;
69 /// use dear_imgui_wgpu::{WgpuRenderer, WgpuInitInfo};
70 ///
71 /// # fn main() -> Result<(), dear_imgui_wgpu::RendererError> {
72 /// # let (device, queue) = todo!("initialize a WGPU Device/Queue");
73 /// # let surface_format = wgpu::TextureFormat::Bgra8UnormSrgb;
74 /// # let mut imgui_context = Context::create();
75 /// let init_info = WgpuInitInfo::new(device, queue, surface_format);
76 /// let mut renderer = WgpuRenderer::new(init_info, &mut imgui_context)?;
77 /// # Ok(()) }
78 /// ```
79 pub fn new(init_info: WgpuInitInfo, imgui_ctx: &mut Context) -> RendererResult<Self> {
80 // Native and wasm experimental path: fully configure context, including font atlas.
81 #[cfg(any(
82 not(target_arch = "wasm32"),
83 all(target_arch = "wasm32", feature = "wasm-font-atlas-experimental")
84 ))]
85 {
86 let mut renderer = Self::empty();
87 renderer.init_with_context(init_info, imgui_ctx)?;
88 Ok(renderer)
89 }
90
91 // Default wasm path: skip font atlas manipulation for safety.
92 #[cfg(all(target_arch = "wasm32", not(feature = "wasm-font-atlas-experimental")))]
93 {
94 Self::new_without_font_atlas(init_info, imgui_ctx)
95 }
96 }
97
98 /// Create an empty WGPU renderer for advanced usage
99 ///
100 /// This creates an uninitialized renderer that must be initialized later
101 /// using `init_with_context()`. Most users should use `new()` instead.
102 ///
103 /// # Example
104 /// ```rust,no_run
105 /// use dear_imgui_rs::Context;
106 /// use dear_imgui_wgpu::{WgpuRenderer, WgpuInitInfo};
107 ///
108 /// # fn main() -> Result<(), dear_imgui_wgpu::RendererError> {
109 /// # let (device, queue) = todo!("initialize a WGPU Device/Queue");
110 /// # let surface_format = wgpu::TextureFormat::Bgra8UnormSrgb;
111 /// # let mut imgui_context = Context::create();
112 /// let mut renderer = WgpuRenderer::empty();
113 /// let init_info = WgpuInitInfo::new(device, queue, surface_format);
114 /// renderer.init_with_context(init_info, &mut imgui_context)?;
115 /// # Ok(()) }
116 /// ```
117 pub fn empty() -> Self {
118 Self {
119 backend_data: None,
120 shader_manager: ShaderManager::new(),
121 texture_manager: WgpuTextureManager::new(),
122 default_texture: None,
123 font_texture_id: None,
124 gamma_mode: GammaMode::Auto,
125 #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
126 viewport_clear_color: Color::BLACK,
127 }
128 }
129
130 /// Initialize the renderer
131 ///
132 /// This corresponds to ImGui_ImplWGPU_Init in the C++ implementation
133 pub fn init(&mut self, init_info: WgpuInitInfo) -> RendererResult<()> {
134 // Create backend data
135 let mut backend_data = WgpuBackendData::new(init_info);
136
137 // Preflight: ensure the render target format is render-attachable and blendable.
138 // The ImGui pipeline always uses alpha blending; non-blendable formats will
139 // fail validation later with less actionable errors.
140 let fmt = backend_data.render_target_format;
141 if let Some(adapter) = backend_data.adapter.as_ref() {
142 let fmt_features = adapter.get_texture_format_features(fmt);
143 if !fmt_features
144 .allowed_usages
145 .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
146 || !fmt_features
147 .flags
148 .contains(wgpu::TextureFormatFeatureFlags::BLENDABLE)
149 {
150 return Err(RendererError::InvalidRenderState(format!(
151 "Render target format {:?} is not suitable for ImGui WGPU renderer (requires RENDER_ATTACHMENT + BLENDABLE). allowed_usages={:?} flags={:?}",
152 fmt, fmt_features.allowed_usages, fmt_features.flags
153 )));
154 }
155 }
156
157 // Initialize render resources
158 backend_data
159 .render_resources
160 .initialize(&backend_data.device)?;
161
162 // Initialize shaders
163 self.shader_manager.initialize(&backend_data.device)?;
164
165 // Create default texture (1x1 white pixel)
166 let default_texture =
167 self.create_default_texture(&backend_data.device, &backend_data.queue)?;
168 self.default_texture = Some(default_texture);
169
170 // Create device objects (pipeline, etc.)
171 self.create_device_objects(&mut backend_data)?;
172
173 self.backend_data = Some(backend_data);
174 Ok(())
175 }
176
177 /// Initialize the renderer with ImGui context configuration (without font atlas for WASM)
178 ///
179 /// This is a variant of init_with_context that skips font atlas preparation,
180 /// useful for WASM builds where font atlas memory sharing is problematic.
181 pub fn new_without_font_atlas(
182 init_info: WgpuInitInfo,
183 imgui_ctx: &mut Context,
184 ) -> RendererResult<Self> {
185 let mut renderer = Self::empty();
186
187 // First initialize the renderer
188 renderer.init(init_info)?;
189
190 // Then configure the ImGui context with backend capabilities
191 renderer.configure_imgui_context(imgui_ctx);
192
193 // Skip font atlas preparation for WASM
194 // The default font will be used automatically by Dear ImGui
195
196 Ok(renderer)
197 }
198
199 /// Initialize the renderer with ImGui context configuration
200 ///
201 /// This is a convenience method that combines init() and configure_imgui_context()
202 /// to ensure proper initialization order, similar to the glow backend approach.
203 pub fn init_with_context(
204 &mut self,
205 init_info: WgpuInitInfo,
206 imgui_ctx: &mut Context,
207 ) -> RendererResult<()> {
208 // First initialize the renderer
209 self.init(init_info)?;
210
211 // Then configure the ImGui context with backend capabilities
212 // This must be done BEFORE preparing the font atlas
213 self.configure_imgui_context(imgui_ctx);
214
215 // Finally prepare the font atlas
216 self.prepare_font_atlas(imgui_ctx)?;
217
218 Ok(())
219 }
220
221 /// Set gamma mode
222 pub fn set_gamma_mode(&mut self, mode: GammaMode) {
223 self.gamma_mode = mode;
224 }
225
226 /// Set clear color for secondary viewports (multi-viewport mode).
227 ///
228 /// This color is used as the load/clear color when rendering ImGui-created
229 /// platform windows via `RenderPlatformWindowsDefault`. It is independent
230 /// from whatever clear color your main swapchain uses.
231 #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
232 pub fn set_viewport_clear_color(&mut self, color: Color) {
233 self.viewport_clear_color = color;
234 }
235
236 /// Get current clear color for secondary viewports.
237 #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
238 pub fn viewport_clear_color(&self) -> Color {
239 self.viewport_clear_color
240 }
241
242 /// Configure Dear ImGui context with WGPU backend capabilities
243 pub fn configure_imgui_context(&self, imgui_context: &mut Context) {
244 let should_set_name = imgui_context.io().backend_renderer_name().is_none();
245 if should_set_name {
246 let _ = imgui_context.set_renderer_name(Some(format!(
247 "dear-imgui-wgpu {}",
248 env!("CARGO_PKG_VERSION")
249 )));
250 }
251
252 let io = imgui_context.io_mut();
253 let mut flags = io.backend_flags();
254
255 // Set WGPU renderer capabilities
256 // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
257 flags.insert(BackendFlags::RENDERER_HAS_VTX_OFFSET);
258 // We can honor ImGuiPlatformIO::Textures[] requests during render.
259 flags.insert(BackendFlags::RENDERER_HAS_TEXTURES);
260
261 #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
262 {
263 // We can render additional platform windows
264 flags.insert(BackendFlags::RENDERER_HAS_VIEWPORTS);
265 }
266
267 io.set_backend_flags(flags);
268 }
269
270 /// Prepare font atlas for rendering
271 pub fn prepare_font_atlas(&mut self, imgui_ctx: &mut Context) -> RendererResult<()> {
272 if let Some(backend_data) = &self.backend_data {
273 let device = backend_data.device.clone();
274 let queue = backend_data.queue.clone();
275 self.reload_font_texture(imgui_ctx, &device, &queue)?;
276 // Fallback: if draw_data-based texture updates are not triggered for the font atlas
277 // on this Dear ImGui version/config, upload the font atlas now and assign a TexID.
278 if self.font_texture_id.is_none() {
279 if let Some(tex_id) =
280 self.try_upload_font_atlas_legacy(imgui_ctx, &device, &queue)?
281 {
282 if cfg!(debug_assertions) {
283 tracing::debug!(
284 target: "dear-imgui-wgpu",
285 "[dear-imgui-wgpu][debug] Font atlas uploaded via fallback (legacy-only) path; user textures use modern ImTextureData. tex_id={}",
286 tex_id
287 );
288 }
289 self.font_texture_id = Some(tex_id);
290 }
291 } else if cfg!(debug_assertions) {
292 tracing::debug!(
293 target: "dear-imgui-wgpu",
294 "[dear-imgui-wgpu][debug] Font atlas tex_id already set: {:?}",
295 self.font_texture_id
296 );
297 }
298 }
299 Ok(())
300 }
301
302 // create_device_objects moved to renderer/pipeline.rs
303
304 /// Create a default 1x1 white texture
305 fn create_default_texture(
306 &self,
307 device: &Device,
308 queue: &Queue,
309 ) -> RendererResult<TextureView> {
310 let texture = device.create_texture(&TextureDescriptor {
311 label: Some("Dear ImGui Default Texture"),
312 size: Extent3d {
313 width: 1,
314 height: 1,
315 depth_or_array_layers: 1,
316 },
317 mip_level_count: 1,
318 sample_count: 1,
319 dimension: TextureDimension::D2,
320 format: TextureFormat::Rgba8Unorm,
321 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
322 view_formats: &[],
323 });
324
325 // Upload white pixel
326 queue.write_texture(
327 wgpu::TexelCopyTextureInfo {
328 texture: &texture,
329 mip_level: 0,
330 origin: wgpu::Origin3d::ZERO,
331 aspect: wgpu::TextureAspect::All,
332 },
333 &[255u8, 255u8, 255u8, 255u8], // RGBA white
334 wgpu::TexelCopyBufferLayout {
335 offset: 0,
336 bytes_per_row: Some(4),
337 rows_per_image: Some(1),
338 },
339 Extent3d {
340 width: 1,
341 height: 1,
342 depth_or_array_layers: 1,
343 },
344 );
345
346 Ok(texture.create_view(&TextureViewDescriptor::default()))
347 }
348
349 /// Load font texture from Dear ImGui context
350 ///
351 /// With the new texture management system in Dear ImGui 1.92+, font textures are
352 /// automatically managed through ImDrawData->Textures[] during rendering.
353 /// However, we need to ensure the font atlas is built and ready before the first render.
354 // reload_font_texture moved to renderer/font_atlas.rs
355
356 /// Legacy/fallback path: upload font atlas texture immediately and assign TexID.
357 /// Returns Some(tex_id) on success, None if texdata is unavailable.
358 // try_upload_font_atlas_legacy moved to renderer/font_atlas.rs
359
360 /// Get the texture manager
361 pub fn texture_manager(&self) -> &WgpuTextureManager {
362 &self.texture_manager
363 }
364
365 /// Get the texture manager mutably
366 pub fn texture_manager_mut(&mut self) -> &mut WgpuTextureManager {
367 &mut self.texture_manager
368 }
369
370 /// Check if the renderer is initialized
371 pub fn is_initialized(&self) -> bool {
372 self.backend_data.is_some()
373 }
374
375 /// Update a single texture manually
376 ///
377 /// This corresponds to ImGui_ImplWGPU_UpdateTexture in the C++ implementation.
378 /// Use this when you need precise control over texture update timing.
379 ///
380 /// # Returns
381 ///
382 /// Returns a `TextureUpdateResult` that contains any status/ID updates that need
383 /// to be applied to the texture data. This follows Rust's principle of explicit
384 /// state management.
385 ///
386 /// # Example
387 ///
388 /// ```rust,no_run
389 /// # use dear_imgui_wgpu::*;
390 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
391 /// # // Assume `renderer` has already been created and initialized elsewhere.
392 /// # let mut renderer: WgpuRenderer = todo!();
393 /// # let mut texture_data = dear_imgui_rs::TextureData::new();
394 /// let result = renderer.update_texture(&texture_data)?;
395 /// result.apply_to(&mut texture_data);
396 /// # Ok(())
397 /// # }
398 /// ```
399 pub fn update_texture(
400 &mut self,
401 texture_data: &dear_imgui_rs::TextureData,
402 ) -> RendererResult<crate::TextureUpdateResult> {
403 if let Some(backend_data) = &mut self.backend_data {
404 let result = self.texture_manager.update_single_texture(
405 texture_data,
406 &backend_data.device,
407 &backend_data.queue,
408 )?;
409
410 // Invalidate any cached bind groups for this texture id so that subsequent
411 // draws will see the updated texture view.
412 match result {
413 crate::TextureUpdateResult::Created { texture_id } => {
414 backend_data
415 .render_resources
416 .remove_image_bind_group(texture_id.id());
417 }
418 crate::TextureUpdateResult::Updated | crate::TextureUpdateResult::Destroyed => {
419 let id = texture_data.tex_id().id();
420 if id != 0 {
421 backend_data.render_resources.remove_image_bind_group(id);
422 }
423 }
424 crate::TextureUpdateResult::Failed | crate::TextureUpdateResult::NoAction => {}
425 }
426
427 Ok(result)
428 } else {
429 Err(RendererError::InvalidRenderState(
430 "Renderer not initialized".to_string(),
431 ))
432 }
433 }
434
435 /// Called every frame to prepare for rendering
436 ///
437 /// This corresponds to ImGui_ImplWGPU_NewFrame in the C++ implementation
438 pub fn new_frame(&mut self) -> RendererResult<()> {
439 let needs_recreation = if let Some(backend_data) = &self.backend_data {
440 backend_data.pipeline_state.is_none()
441 } else {
442 false
443 };
444
445 if needs_recreation {
446 // Extract the backend data temporarily to avoid borrow checker issues
447 let mut backend_data = self.backend_data.take().unwrap();
448 self.create_device_objects(&mut backend_data)?;
449 self.backend_data = Some(backend_data);
450 }
451 Ok(())
452 }
453
454 /// Render Dear ImGui draw data
455 ///
456 /// This corresponds to ImGui_ImplWGPU_RenderDrawData in the C++ implementation
457 pub fn render_draw_data(
458 &mut self,
459 draw_data: &DrawData,
460 render_pass: &mut RenderPass,
461 ) -> RendererResult<()> {
462 // Early out if nothing to draw (avoid binding/drawing without buffers)
463 let mut total_vtx_count = 0usize;
464 let mut total_idx_count = 0usize;
465 for dl in draw_data.draw_lists() {
466 total_vtx_count += dl.vtx_buffer().len();
467 total_idx_count += dl.idx_buffer().len();
468 }
469 if total_vtx_count == 0 || total_idx_count == 0 {
470 return Ok(());
471 }
472
473 let backend_data = self.backend_data.as_mut().ok_or_else(|| {
474 RendererError::InvalidRenderState("Renderer not initialized".to_string())
475 })?;
476
477 // Avoid rendering when minimized
478 let fb_width = (draw_data.display_size[0] * draw_data.framebuffer_scale[0]) as i32;
479 let fb_height = (draw_data.display_size[1] * draw_data.framebuffer_scale[1]) as i32;
480 if fb_width <= 0 || fb_height <= 0 || !draw_data.valid() {
481 return Ok(());
482 }
483
484 self.texture_manager.handle_texture_updates(
485 draw_data,
486 &backend_data.device,
487 &backend_data.queue,
488 &mut backend_data.render_resources,
489 );
490
491 // Advance to next frame
492 backend_data.next_frame();
493
494 // Prepare frame resources
495 Self::prepare_frame_resources_static(draw_data, backend_data)?;
496
497 // Compute gamma based on renderer mode
498 let gamma = match self.gamma_mode {
499 GammaMode::Auto => Uniforms::gamma_for_format(backend_data.render_target_format),
500 GammaMode::Linear => 1.0,
501 GammaMode::Gamma22 => 2.2,
502 };
503
504 // Setup render state
505 Self::setup_render_state_static(draw_data, render_pass, backend_data, gamma)?;
506 // Override viewport to the provided framebuffer size to avoid partial viewport issues
507 render_pass.set_viewport(0.0, 0.0, fb_width as f32, fb_height as f32, 0.0, 1.0);
508
509 // Setup render state structure (for callbacks and custom texture bindings)
510 // Note: We need to be careful with lifetimes here, so we'll set it just before rendering
511 // and clear it immediately after
512 unsafe {
513 // Use _Nil variant as our bindings export it
514 let platform_io = dear_imgui_rs::sys::igGetPlatformIO_Nil();
515
516 // Create a temporary render state structure
517 let mut render_state = crate::WgpuRenderState::new(&backend_data.device, render_pass);
518
519 // Set the render state pointer
520 (*platform_io).Renderer_RenderState =
521 &mut render_state as *mut _ as *mut std::ffi::c_void;
522
523 // Render draw lists with the render state exposed
524 let result = Self::render_draw_lists_static(
525 &mut self.texture_manager,
526 &self.default_texture,
527 draw_data,
528 render_pass,
529 backend_data,
530 gamma,
531 );
532
533 // Clear the render state pointer
534 (*platform_io).Renderer_RenderState = std::ptr::null_mut();
535
536 if let Err(e) = result {
537 eprintln!("[wgpu-mv] render_draw_lists_static error: {:?}", e);
538 return Err(e);
539 }
540 }
541
542 Ok(())
543 }
544
545 pub fn render_draw_data_with_fb_size(
546 &mut self,
547 draw_data: &DrawData,
548 render_pass: &mut RenderPass,
549 fb_width: u32,
550 fb_height: u32,
551 ) -> RendererResult<()> {
552 // Public helper used by the main window: advance frame resources as usual.
553 self.render_draw_data_with_fb_size_ex(draw_data, render_pass, fb_width, fb_height, true)
554 }
555
556 /// Internal variant that optionally skips advancing the frame index.
557 ///
558 /// When `advance_frame` is `false`, we reuse the current frame resources.
559 fn render_draw_data_with_fb_size_ex(
560 &mut self,
561 draw_data: &DrawData,
562 render_pass: &mut RenderPass,
563 fb_width: u32,
564 fb_height: u32,
565 advance_frame: bool,
566 ) -> RendererResult<()> {
567 // Log only when the override framebuffer size doesn't match the draw data scale.
568 // This helps diagnose HiDPI/viewport scaling issues without spamming per-frame traces.
569 #[cfg(feature = "mv-log")]
570 {
571 static LAST_MISMATCH: OnceLock<Mutex<Option<(u32, u32, u32, u32, bool)>>> =
572 OnceLock::new();
573 let last = LAST_MISMATCH.get_or_init(|| Mutex::new(None));
574 let expected_w = (draw_data.display_size()[0] * draw_data.framebuffer_scale()[0])
575 .round()
576 .max(0.0) as u32;
577 let expected_h = (draw_data.display_size()[1] * draw_data.framebuffer_scale()[1])
578 .round()
579 .max(0.0) as u32;
580 if expected_w != fb_width || expected_h != fb_height {
581 let key = (expected_w, expected_h, fb_width, fb_height, advance_frame);
582 let mut guard = last.lock().unwrap();
583 if *guard != Some(key) {
584 mvlog!(
585 "[wgpu-mv] fb mismatch expected=({}, {}) override=({}, {}) disp=({:.1},{:.1}) fb_scale=({:.2},{:.2}) main={}",
586 expected_w,
587 expected_h,
588 fb_width,
589 fb_height,
590 draw_data.display_size()[0],
591 draw_data.display_size()[1],
592 draw_data.framebuffer_scale()[0],
593 draw_data.framebuffer_scale()[1],
594 advance_frame
595 );
596 *guard = Some(key);
597 }
598 }
599 }
600 let total_vtx_count: usize = draw_data.draw_lists().map(|dl| dl.vtx_buffer().len()).sum();
601 let total_idx_count: usize = draw_data.draw_lists().map(|dl| dl.idx_buffer().len()).sum();
602 if total_vtx_count == 0 || total_idx_count == 0 {
603 return Ok(());
604 }
605 let backend_data = self.backend_data.as_mut().ok_or_else(|| {
606 RendererError::InvalidRenderState("Renderer not initialized".to_string())
607 })?;
608
609 // Skip if invalid/minimized
610 if fb_width == 0 || fb_height == 0 || !draw_data.valid() {
611 return Ok(());
612 }
613
614 self.texture_manager.handle_texture_updates(
615 draw_data,
616 &backend_data.device,
617 &backend_data.queue,
618 &mut backend_data.render_resources,
619 );
620
621 if advance_frame {
622 backend_data.next_frame();
623 }
624 Self::prepare_frame_resources_static(draw_data, backend_data)?;
625
626 let gamma = match self.gamma_mode {
627 GammaMode::Auto => Uniforms::gamma_for_format(backend_data.render_target_format),
628 GammaMode::Linear => 1.0,
629 GammaMode::Gamma22 => 2.2,
630 };
631
632 Self::setup_render_state_static(draw_data, render_pass, backend_data, gamma)?;
633
634 unsafe {
635 let platform_io = dear_imgui_rs::sys::igGetPlatformIO_Nil();
636 let mut render_state = crate::WgpuRenderState::new(&backend_data.device, render_pass);
637 (*platform_io).Renderer_RenderState =
638 &mut render_state as *mut _ as *mut std::ffi::c_void;
639
640 // Reuse core routine but clamp scissor by overriding framebuffer bounds.
641 // Extract common bind group handles up front to avoid borrowing conflicts with render_resources.
642 let device = backend_data.device.clone();
643 let (common_layout, uniform_buffer, default_common_bg) = {
644 let ub = backend_data
645 .render_resources
646 .uniform_buffer()
647 .ok_or_else(|| {
648 RendererError::InvalidRenderState(
649 "Uniform buffer not initialized".to_string(),
650 )
651 })?;
652 (
653 ub.bind_group_layout().clone(),
654 ub.buffer().clone(),
655 ub.bind_group().clone(),
656 )
657 };
658 let mut current_sampler_id: Option<u64> = None;
659
660 let mut global_idx_offset: u32 = 0;
661 let mut global_vtx_offset: i32 = 0;
662 let clip_off = draw_data.display_pos();
663 let clip_scale = draw_data.framebuffer_scale();
664 let fbw = fb_width as f32;
665 let fbh = fb_height as f32;
666
667 for draw_list in draw_data.draw_lists() {
668 let vtx_buffer = draw_list.vtx_buffer();
669 let idx_buffer = draw_list.idx_buffer();
670 for cmd in draw_list.commands() {
671 match cmd {
672 dear_imgui_rs::render::DrawCmd::Elements {
673 count,
674 cmd_params,
675 raw_cmd,
676 } => {
677 // Texture bind group resolution mirrors render_draw_lists_static
678 // Resolve effective ImTextureID using raw_cmd (modern texture path)
679 let tex_id = dear_imgui_rs::sys::ImDrawCmd_GetTexID(
680 raw_cmd as *mut dear_imgui_rs::sys::ImDrawCmd,
681 ) as u64;
682
683 // Switch common bind group (sampler) if this texture uses a custom sampler.
684 let desired_sampler_id = if tex_id == 0 {
685 None
686 } else {
687 self.texture_manager.custom_sampler_id_for_texture(tex_id)
688 };
689 if desired_sampler_id != current_sampler_id {
690 if let Some(sampler_id) = desired_sampler_id {
691 if let Some(bg0) = self
692 .texture_manager
693 .get_or_create_common_bind_group_for_sampler(
694 &device,
695 &common_layout,
696 &uniform_buffer,
697 sampler_id,
698 )
699 {
700 render_pass.set_bind_group(0, &bg0, &[]);
701 } else {
702 render_pass.set_bind_group(0, &default_common_bg, &[]);
703 }
704 } else {
705 render_pass.set_bind_group(0, &default_common_bg, &[]);
706 }
707 current_sampler_id = desired_sampler_id;
708 }
709
710 let texture_bind_group = if tex_id == 0 {
711 if let Some(default_tex) = &self.default_texture {
712 backend_data
713 .render_resources
714 .get_or_create_image_bind_group(
715 &backend_data.device,
716 0,
717 default_tex,
718 )?
719 .clone()
720 } else {
721 return Err(RendererError::InvalidRenderState(
722 "Default texture not available".to_string(),
723 ));
724 }
725 } else if let Some(wgpu_texture) =
726 self.texture_manager.get_texture(tex_id)
727 {
728 backend_data
729 .render_resources
730 .get_or_create_image_bind_group(
731 &backend_data.device,
732 tex_id,
733 &wgpu_texture.texture_view,
734 )?
735 .clone()
736 } else if let Some(default_tex) = &self.default_texture {
737 backend_data
738 .render_resources
739 .get_or_create_image_bind_group(
740 &backend_data.device,
741 0,
742 default_tex,
743 )?
744 .clone()
745 } else {
746 return Err(RendererError::InvalidRenderState(
747 "Texture not found and no default texture".to_string(),
748 ));
749 };
750 render_pass.set_bind_group(1, &texture_bind_group, &[]);
751
752 // Compute clip rect in framebuffer space
753 let mut clip_min_x =
754 (cmd_params.clip_rect[0] - clip_off[0]) * clip_scale[0];
755 let mut clip_min_y =
756 (cmd_params.clip_rect[1] - clip_off[1]) * clip_scale[1];
757 let mut clip_max_x =
758 (cmd_params.clip_rect[2] - clip_off[0]) * clip_scale[0];
759 let mut clip_max_y =
760 (cmd_params.clip_rect[3] - clip_off[1]) * clip_scale[1];
761 // Clamp to override framebuffer bounds
762 clip_min_x = clip_min_x.max(0.0);
763 clip_min_y = clip_min_y.max(0.0);
764 clip_max_x = clip_max_x.min(fbw);
765 clip_max_y = clip_max_y.min(fbh);
766 if clip_max_x <= clip_min_x || clip_max_y <= clip_min_y {
767 continue;
768 }
769 render_pass.set_scissor_rect(
770 clip_min_x as u32,
771 clip_min_y as u32,
772 (clip_max_x - clip_min_x) as u32,
773 (clip_max_y - clip_min_y) as u32,
774 );
775 let start_index = cmd_params.idx_offset as u32 + global_idx_offset;
776 let end_index = start_index + count as u32;
777 let vertex_offset = (cmd_params.vtx_offset as i32) + global_vtx_offset;
778 render_pass.draw_indexed(start_index..end_index, vertex_offset, 0..1);
779 }
780 dear_imgui_rs::render::DrawCmd::ResetRenderState => {
781 Self::setup_render_state_static(
782 draw_data,
783 render_pass,
784 backend_data,
785 gamma,
786 )?;
787 current_sampler_id = None;
788 }
789 dear_imgui_rs::render::DrawCmd::RawCallback { .. } => {
790 // Unsupported raw callbacks; skip.
791 }
792 }
793 }
794
795 global_idx_offset += idx_buffer.len() as u32;
796 global_vtx_offset += vtx_buffer.len() as i32;
797 }
798
799 (*platform_io).Renderer_RenderState = std::ptr::null_mut();
800 }
801
802 Ok(())
803 }
804
805 /// Prepare frame resources (buffers)
806 // prepare_frame_resources_static moved to renderer/draw.rs
807
808 /// Setup render state
809 ///
810 /// This corresponds to ImGui_ImplWGPU_SetupRenderState in the C++ implementation
811 // setup_render_state_static moved to renderer/draw.rs
812
813 /// Render all draw lists
814 // render_draw_lists_static moved to renderer/draw.rs
815
816 /// Invalidate device objects
817 ///
818 /// This corresponds to ImGui_ImplWGPU_InvalidateDeviceObjects in the C++ implementation
819 pub fn invalidate_device_objects(&mut self) -> RendererResult<()> {
820 if let Some(ref mut backend_data) = self.backend_data {
821 backend_data.pipeline_state = None;
822 backend_data.render_resources = RenderResources::new();
823
824 // Clear frame resources
825 for frame_resources in &mut backend_data.frame_resources {
826 *frame_resources = FrameResources::new();
827 }
828 }
829
830 // Clear texture manager
831 self.texture_manager.clear();
832 self.default_texture = None;
833 self.font_texture_id = None;
834
835 Ok(())
836 }
837
838 /// Shutdown the renderer
839 ///
840 /// This corresponds to ImGui_ImplWGPU_Shutdown in the C++ implementation
841 pub fn shutdown(&mut self) {
842 self.invalidate_device_objects().ok();
843 self.backend_data = None;
844 }
845}
846
847// Submodules for renderer features
848mod draw;
849mod external_textures;
850mod font_atlas;
851#[cfg(feature = "multi-viewport-winit")]
852pub mod multi_viewport;
853#[cfg(feature = "multi-viewport-sdl3")]
854pub mod multi_viewport_sdl3;
855mod pipeline;
856#[cfg(feature = "multi-viewport-sdl3")]
857mod sdl3_raw_window_handle;
858
859impl Default for WgpuRenderer {
860 fn default() -> Self {
861 Self::empty()
862 }
863}