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