Skip to main content

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}