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}