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            if imgui_ctx
277                .io()
278                .backend_flags()
279                .contains(BackendFlags::RENDERER_HAS_TEXTURES)
280            {
281                // New backend texture system: font textures are produced via DrawData::textures()
282                // requests; do not assign a legacy TexID.
283                return Ok(());
284            }
285
286            // Legacy fallback: upload font atlas texture immediately and assign TexID.
287            if self.font_texture_id.is_none() {
288                if let Some(tex_id) =
289                    self.try_upload_font_atlas_legacy(imgui_ctx, &device, &queue)?
290                {
291                    if cfg!(debug_assertions) {
292                        tracing::debug!(
293                            target: "dear-imgui-wgpu",
294                            "[dear-imgui-wgpu][debug] Font atlas uploaded via legacy fallback path. tex_id={}",
295                            tex_id
296                        );
297                    }
298                    self.font_texture_id = Some(tex_id);
299                }
300            }
301        }
302        Ok(())
303    }
304
305    // create_device_objects moved to renderer/pipeline.rs
306
307    /// Create a default 1x1 white texture
308    fn create_default_texture(
309        &self,
310        device: &Device,
311        queue: &Queue,
312    ) -> RendererResult<TextureView> {
313        let texture = device.create_texture(&TextureDescriptor {
314            label: Some("Dear ImGui Default Texture"),
315            size: Extent3d {
316                width: 1,
317                height: 1,
318                depth_or_array_layers: 1,
319            },
320            mip_level_count: 1,
321            sample_count: 1,
322            dimension: TextureDimension::D2,
323            format: TextureFormat::Rgba8Unorm,
324            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
325            view_formats: &[],
326        });
327
328        // Upload white pixel
329        queue.write_texture(
330            wgpu::TexelCopyTextureInfo {
331                texture: &texture,
332                mip_level: 0,
333                origin: wgpu::Origin3d::ZERO,
334                aspect: wgpu::TextureAspect::All,
335            },
336            &[255u8, 255u8, 255u8, 255u8], // RGBA white
337            wgpu::TexelCopyBufferLayout {
338                offset: 0,
339                bytes_per_row: Some(4),
340                rows_per_image: Some(1),
341            },
342            Extent3d {
343                width: 1,
344                height: 1,
345                depth_or_array_layers: 1,
346            },
347        );
348
349        Ok(texture.create_view(&TextureViewDescriptor::default()))
350    }
351
352    /// Load font texture from Dear ImGui context
353    ///
354    /// With the new texture management system in Dear ImGui 1.92+, font textures are
355    /// automatically managed through ImDrawData->Textures[] during rendering.
356    /// However, we need to ensure the font atlas is built and ready before the first render.
357    // reload_font_texture moved to renderer/font_atlas.rs
358
359    /// Legacy/fallback path: upload font atlas texture immediately and assign TexID.
360    /// Returns Some(tex_id) on success, None if texdata is unavailable.
361    // try_upload_font_atlas_legacy moved to renderer/font_atlas.rs
362
363    /// Get the texture manager
364    pub fn texture_manager(&self) -> &WgpuTextureManager {
365        &self.texture_manager
366    }
367
368    /// Get the texture manager mutably
369    pub fn texture_manager_mut(&mut self) -> &mut WgpuTextureManager {
370        &mut self.texture_manager
371    }
372
373    /// Check if the renderer is initialized
374    pub fn is_initialized(&self) -> bool {
375        self.backend_data.is_some()
376    }
377
378    /// Update a single texture manually
379    ///
380    /// This corresponds to ImGui_ImplWGPU_UpdateTexture in the C++ implementation.
381    /// Use this when you need precise control over texture update timing.
382    ///
383    /// # Returns
384    ///
385    /// Returns a `TextureUpdateResult` that contains any status/ID updates that need
386    /// to be applied to the texture data. This follows Rust's principle of explicit
387    /// state management.
388    ///
389    /// # Example
390    ///
391    /// ```rust,no_run
392    /// # use dear_imgui_wgpu::*;
393    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
394    /// # // Assume `renderer` has already been created and initialized elsewhere.
395    /// # let mut renderer: WgpuRenderer = todo!();
396    /// # let mut texture_data = dear_imgui_rs::TextureData::new();
397    /// let result = renderer.update_texture(&texture_data)?;
398    /// result.apply_to(&mut texture_data);
399    /// # Ok(())
400    /// # }
401    /// ```
402    pub fn update_texture(
403        &mut self,
404        texture_data: &dear_imgui_rs::TextureData,
405    ) -> RendererResult<crate::TextureUpdateResult> {
406        if let Some(backend_data) = &mut self.backend_data {
407            let result = self.texture_manager.update_single_texture(
408                texture_data,
409                &backend_data.device,
410                &backend_data.queue,
411            )?;
412
413            // Invalidate any cached bind groups for this texture id so that subsequent
414            // draws will see the updated texture view.
415            match result {
416                crate::TextureUpdateResult::Created { texture_id } => {
417                    backend_data
418                        .render_resources
419                        .remove_image_bind_group(texture_id.id());
420                }
421                crate::TextureUpdateResult::Updated | crate::TextureUpdateResult::Destroyed => {
422                    let id = texture_data.tex_id().id();
423                    if id != 0 {
424                        backend_data.render_resources.remove_image_bind_group(id);
425                    }
426                }
427                crate::TextureUpdateResult::Failed | crate::TextureUpdateResult::NoAction => {}
428            }
429
430            Ok(result)
431        } else {
432            Err(RendererError::InvalidRenderState(
433                "Renderer not initialized".to_string(),
434            ))
435        }
436    }
437
438    /// Called every frame to prepare for rendering
439    ///
440    /// This corresponds to ImGui_ImplWGPU_NewFrame in the C++ implementation
441    pub fn new_frame(&mut self) -> RendererResult<()> {
442        let needs_recreation = if let Some(backend_data) = &self.backend_data {
443            backend_data.pipeline_state.is_none()
444        } else {
445            false
446        };
447
448        if needs_recreation {
449            // Extract the backend data temporarily to avoid borrow checker issues
450            let mut backend_data = self.backend_data.take().unwrap();
451            self.create_device_objects(&mut backend_data)?;
452            self.backend_data = Some(backend_data);
453        }
454        Ok(())
455    }
456
457    /// Render Dear ImGui draw data
458    ///
459    /// This corresponds to ImGui_ImplWGPU_RenderDrawData in the C++ implementation
460    pub fn render_draw_data(
461        &mut self,
462        draw_data: &DrawData,
463        render_pass: &mut RenderPass,
464    ) -> RendererResult<()> {
465        // Early out if nothing to draw (avoid binding/drawing without buffers)
466        let mut total_vtx_count = 0usize;
467        let mut total_idx_count = 0usize;
468        for dl in draw_data.draw_lists() {
469            total_vtx_count += dl.vtx_buffer().len();
470            total_idx_count += dl.idx_buffer().len();
471        }
472        if total_vtx_count == 0 || total_idx_count == 0 {
473            return Ok(());
474        }
475
476        let backend_data = self.backend_data.as_mut().ok_or_else(|| {
477            RendererError::InvalidRenderState("Renderer not initialized".to_string())
478        })?;
479
480        // Avoid rendering when minimized
481        let fb_width = (draw_data.display_size[0] * draw_data.framebuffer_scale[0]) as i32;
482        let fb_height = (draw_data.display_size[1] * draw_data.framebuffer_scale[1]) as i32;
483        if fb_width <= 0 || fb_height <= 0 || !draw_data.valid() {
484            return Ok(());
485        }
486
487        self.texture_manager.handle_texture_updates(
488            draw_data,
489            &backend_data.device,
490            &backend_data.queue,
491            &mut backend_data.render_resources,
492        );
493
494        // Advance to next frame
495        backend_data.next_frame();
496
497        // Prepare frame resources
498        Self::prepare_frame_resources_static(draw_data, backend_data)?;
499
500        // Compute gamma based on renderer mode
501        let gamma = match self.gamma_mode {
502            GammaMode::Auto => Uniforms::gamma_for_format(backend_data.render_target_format),
503            GammaMode::Linear => 1.0,
504            GammaMode::Gamma22 => 2.2,
505        };
506
507        // Setup render state
508        Self::setup_render_state_static(draw_data, render_pass, backend_data, gamma)?;
509        // Override viewport to the provided framebuffer size to avoid partial viewport issues
510        render_pass.set_viewport(0.0, 0.0, fb_width as f32, fb_height as f32, 0.0, 1.0);
511
512        // Setup render state structure (for callbacks and custom texture bindings)
513        // Note: We need to be careful with lifetimes here, so we'll set it just before rendering
514        // and clear it immediately after
515        unsafe {
516            // Use _Nil variant as our bindings export it
517            let platform_io = dear_imgui_rs::sys::igGetPlatformIO_Nil();
518
519            // Create a temporary render state structure
520            let mut render_state = crate::WgpuRenderState::new(&backend_data.device, render_pass);
521
522            // Set the render state pointer
523            (*platform_io).Renderer_RenderState =
524                &mut render_state as *mut _ as *mut std::ffi::c_void;
525
526            // Render draw lists with the render state exposed
527            let result = Self::render_draw_lists_static(
528                &mut self.texture_manager,
529                &self.default_texture,
530                draw_data,
531                render_pass,
532                backend_data,
533                gamma,
534            );
535
536            // Clear the render state pointer
537            (*platform_io).Renderer_RenderState = std::ptr::null_mut();
538
539            if let Err(e) = result {
540                eprintln!("[wgpu-mv] render_draw_lists_static error: {:?}", e);
541                return Err(e);
542            }
543        }
544
545        Ok(())
546    }
547
548    pub fn render_draw_data_with_fb_size(
549        &mut self,
550        draw_data: &DrawData,
551        render_pass: &mut RenderPass,
552        fb_width: u32,
553        fb_height: u32,
554    ) -> RendererResult<()> {
555        // Public helper used by the main window: advance frame resources as usual.
556        self.render_draw_data_with_fb_size_ex(draw_data, render_pass, fb_width, fb_height, true)
557    }
558
559    /// Internal variant that optionally skips advancing the frame index.
560    ///
561    /// When `advance_frame` is `false`, we reuse the current frame resources.
562    fn render_draw_data_with_fb_size_ex(
563        &mut self,
564        draw_data: &DrawData,
565        render_pass: &mut RenderPass,
566        fb_width: u32,
567        fb_height: u32,
568        advance_frame: bool,
569    ) -> RendererResult<()> {
570        // Log only when the override framebuffer size doesn't match the draw data scale.
571        // This helps diagnose HiDPI/viewport scaling issues without spamming per-frame traces.
572        #[cfg(feature = "mv-log")]
573        {
574            static LAST_MISMATCH: OnceLock<Mutex<Option<(u32, u32, u32, u32, bool)>>> =
575                OnceLock::new();
576            let last = LAST_MISMATCH.get_or_init(|| Mutex::new(None));
577            let expected_w = (draw_data.display_size()[0] * draw_data.framebuffer_scale()[0])
578                .round()
579                .max(0.0) as u32;
580            let expected_h = (draw_data.display_size()[1] * draw_data.framebuffer_scale()[1])
581                .round()
582                .max(0.0) as u32;
583            if expected_w != fb_width || expected_h != fb_height {
584                let key = (expected_w, expected_h, fb_width, fb_height, advance_frame);
585                let mut guard = last.lock().unwrap();
586                if *guard != Some(key) {
587                    mvlog!(
588                        "[wgpu-mv] fb mismatch expected=({}, {}) override=({}, {}) disp=({:.1},{:.1}) fb_scale=({:.2},{:.2}) main={}",
589                        expected_w,
590                        expected_h,
591                        fb_width,
592                        fb_height,
593                        draw_data.display_size()[0],
594                        draw_data.display_size()[1],
595                        draw_data.framebuffer_scale()[0],
596                        draw_data.framebuffer_scale()[1],
597                        advance_frame
598                    );
599                    *guard = Some(key);
600                }
601            }
602        }
603        let total_vtx_count: usize = draw_data.draw_lists().map(|dl| dl.vtx_buffer().len()).sum();
604        let total_idx_count: usize = draw_data.draw_lists().map(|dl| dl.idx_buffer().len()).sum();
605        if total_vtx_count == 0 || total_idx_count == 0 {
606            return Ok(());
607        }
608        let backend_data = self.backend_data.as_mut().ok_or_else(|| {
609            RendererError::InvalidRenderState("Renderer not initialized".to_string())
610        })?;
611
612        // Skip if invalid/minimized
613        if fb_width == 0 || fb_height == 0 || !draw_data.valid() {
614            return Ok(());
615        }
616
617        self.texture_manager.handle_texture_updates(
618            draw_data,
619            &backend_data.device,
620            &backend_data.queue,
621            &mut backend_data.render_resources,
622        );
623
624        if advance_frame {
625            backend_data.next_frame();
626        }
627        Self::prepare_frame_resources_static(draw_data, backend_data)?;
628
629        let gamma = match self.gamma_mode {
630            GammaMode::Auto => Uniforms::gamma_for_format(backend_data.render_target_format),
631            GammaMode::Linear => 1.0,
632            GammaMode::Gamma22 => 2.2,
633        };
634
635        Self::setup_render_state_static(draw_data, render_pass, backend_data, gamma)?;
636
637        unsafe {
638            let platform_io = dear_imgui_rs::sys::igGetPlatformIO_Nil();
639            let mut render_state = crate::WgpuRenderState::new(&backend_data.device, render_pass);
640            (*platform_io).Renderer_RenderState =
641                &mut render_state as *mut _ as *mut std::ffi::c_void;
642
643            // Reuse core routine but clamp scissor by overriding framebuffer bounds.
644            // Extract common bind group handles up front to avoid borrowing conflicts with render_resources.
645            let device = backend_data.device.clone();
646            let (common_layout, uniform_buffer, default_common_bg) = {
647                let ub = backend_data
648                    .render_resources
649                    .uniform_buffer()
650                    .ok_or_else(|| {
651                        RendererError::InvalidRenderState(
652                            "Uniform buffer not initialized".to_string(),
653                        )
654                    })?;
655                (
656                    ub.bind_group_layout().clone(),
657                    ub.buffer().clone(),
658                    ub.bind_group().clone(),
659                )
660            };
661            let mut current_sampler_id: Option<u64> = None;
662
663            let mut global_idx_offset: u32 = 0;
664            let mut global_vtx_offset: i32 = 0;
665            let clip_off = draw_data.display_pos();
666            let clip_scale = draw_data.framebuffer_scale();
667            let fbw = fb_width as f32;
668            let fbh = fb_height as f32;
669
670            for draw_list in draw_data.draw_lists() {
671                let vtx_buffer = draw_list.vtx_buffer();
672                let idx_buffer = draw_list.idx_buffer();
673                for cmd in draw_list.commands() {
674                    match cmd {
675                        dear_imgui_rs::render::DrawCmd::Elements {
676                            count,
677                            cmd_params,
678                            raw_cmd,
679                        } => {
680                            // Texture bind group resolution mirrors render_draw_lists_static
681                            // Resolve effective ImTextureID using raw_cmd (modern texture path)
682                            let mut cmd_copy = *raw_cmd;
683                            let tex_id =
684                                dear_imgui_rs::sys::ImDrawCmd_GetTexID(&mut cmd_copy) as u64;
685
686                            // Switch common bind group (sampler) if this texture uses a custom sampler.
687                            let desired_sampler_id = if tex_id == 0 {
688                                None
689                            } else {
690                                self.texture_manager.custom_sampler_id_for_texture(tex_id)
691                            };
692                            if desired_sampler_id != current_sampler_id {
693                                if let Some(sampler_id) = desired_sampler_id {
694                                    if let Some(bg0) = self
695                                        .texture_manager
696                                        .get_or_create_common_bind_group_for_sampler(
697                                            &device,
698                                            &common_layout,
699                                            &uniform_buffer,
700                                            sampler_id,
701                                        )
702                                    {
703                                        render_pass.set_bind_group(0, &bg0, &[]);
704                                    } else {
705                                        render_pass.set_bind_group(0, &default_common_bg, &[]);
706                                    }
707                                } else {
708                                    render_pass.set_bind_group(0, &default_common_bg, &[]);
709                                }
710                                current_sampler_id = desired_sampler_id;
711                            }
712
713                            let texture_bind_group = if tex_id == 0 {
714                                if let Some(default_tex) = &self.default_texture {
715                                    backend_data
716                                        .render_resources
717                                        .get_or_create_image_bind_group(
718                                            &backend_data.device,
719                                            0,
720                                            default_tex,
721                                        )?
722                                        .clone()
723                                } else {
724                                    return Err(RendererError::InvalidRenderState(
725                                        "Default texture not available".to_string(),
726                                    ));
727                                }
728                            } else if let Some(wgpu_texture) =
729                                self.texture_manager.get_texture(tex_id)
730                            {
731                                backend_data
732                                    .render_resources
733                                    .get_or_create_image_bind_group(
734                                        &backend_data.device,
735                                        tex_id,
736                                        &wgpu_texture.texture_view,
737                                    )?
738                                    .clone()
739                            } else if let Some(default_tex) = &self.default_texture {
740                                backend_data
741                                    .render_resources
742                                    .get_or_create_image_bind_group(
743                                        &backend_data.device,
744                                        0,
745                                        default_tex,
746                                    )?
747                                    .clone()
748                            } else {
749                                return Err(RendererError::InvalidRenderState(
750                                    "Texture not found and no default texture".to_string(),
751                                ));
752                            };
753                            render_pass.set_bind_group(1, &texture_bind_group, &[]);
754
755                            // Compute clip rect in framebuffer space
756                            let mut clip_min_x =
757                                (cmd_params.clip_rect[0] - clip_off[0]) * clip_scale[0];
758                            let mut clip_min_y =
759                                (cmd_params.clip_rect[1] - clip_off[1]) * clip_scale[1];
760                            let mut clip_max_x =
761                                (cmd_params.clip_rect[2] - clip_off[0]) * clip_scale[0];
762                            let mut clip_max_y =
763                                (cmd_params.clip_rect[3] - clip_off[1]) * clip_scale[1];
764                            // Clamp to override framebuffer bounds
765                            clip_min_x = clip_min_x.max(0.0);
766                            clip_min_y = clip_min_y.max(0.0);
767                            clip_max_x = clip_max_x.min(fbw);
768                            clip_max_y = clip_max_y.min(fbh);
769                            if clip_max_x <= clip_min_x || clip_max_y <= clip_min_y {
770                                continue;
771                            }
772                            render_pass.set_scissor_rect(
773                                clip_min_x as u32,
774                                clip_min_y as u32,
775                                (clip_max_x - clip_min_x) as u32,
776                                (clip_max_y - clip_min_y) as u32,
777                            );
778                            let Ok(count_u32) = u32::try_from(count) else {
779                                continue;
780                            };
781                            let Ok(idx_offset_u32) = u32::try_from(cmd_params.idx_offset) else {
782                                continue;
783                            };
784                            let Some(start_index) = idx_offset_u32.checked_add(global_idx_offset)
785                            else {
786                                continue;
787                            };
788                            let Some(end_index) = start_index.checked_add(count_u32) else {
789                                continue;
790                            };
791                            let Ok(vtx_offset_i32) = i32::try_from(cmd_params.vtx_offset) else {
792                                continue;
793                            };
794                            let Some(vertex_offset) = vtx_offset_i32.checked_add(global_vtx_offset)
795                            else {
796                                continue;
797                            };
798                            render_pass.draw_indexed(start_index..end_index, vertex_offset, 0..1);
799                        }
800                        dear_imgui_rs::render::DrawCmd::ResetRenderState => {
801                            Self::setup_render_state_static(
802                                draw_data,
803                                render_pass,
804                                backend_data,
805                                gamma,
806                            )?;
807                            current_sampler_id = None;
808                        }
809                        dear_imgui_rs::render::DrawCmd::RawCallback { .. } => {
810                            // Unsupported raw callbacks; skip.
811                        }
812                    }
813                }
814
815                let idx_len_u32 = u32::try_from(idx_buffer.len())
816                    .map_err(|_| RendererError::Generic("index buffer too large".to_string()))?;
817                global_idx_offset =
818                    global_idx_offset.checked_add(idx_len_u32).ok_or_else(|| {
819                        RendererError::Generic("index buffer offset overflow".to_string())
820                    })?;
821
822                let vtx_len_i32 = i32::try_from(vtx_buffer.len())
823                    .map_err(|_| RendererError::Generic("vertex buffer too large".to_string()))?;
824                global_vtx_offset =
825                    global_vtx_offset.checked_add(vtx_len_i32).ok_or_else(|| {
826                        RendererError::Generic("vertex buffer offset overflow".to_string())
827                    })?;
828            }
829
830            (*platform_io).Renderer_RenderState = std::ptr::null_mut();
831        }
832
833        Ok(())
834    }
835
836    /// Prepare frame resources (buffers)
837    // prepare_frame_resources_static moved to renderer/draw.rs
838
839    /// Setup render state
840    ///
841    /// This corresponds to ImGui_ImplWGPU_SetupRenderState in the C++ implementation
842    // setup_render_state_static moved to renderer/draw.rs
843
844    /// Render all draw lists
845    // render_draw_lists_static moved to renderer/draw.rs
846
847    /// Invalidate device objects
848    ///
849    /// This corresponds to ImGui_ImplWGPU_InvalidateDeviceObjects in the C++ implementation
850    pub fn invalidate_device_objects(&mut self) -> RendererResult<()> {
851        if let Some(ref mut backend_data) = self.backend_data {
852            backend_data.pipeline_state = None;
853            backend_data.render_resources = RenderResources::new();
854
855            // Clear frame resources
856            for frame_resources in &mut backend_data.frame_resources {
857                *frame_resources = FrameResources::new();
858            }
859        }
860
861        // Clear texture manager
862        self.texture_manager.clear();
863        self.default_texture = None;
864        self.font_texture_id = None;
865
866        Ok(())
867    }
868
869    /// Shutdown the renderer
870    ///
871    /// This corresponds to ImGui_ImplWGPU_Shutdown in the C++ implementation
872    pub fn shutdown(&mut self) {
873        self.invalidate_device_objects().ok();
874        self.backend_data = None;
875    }
876}
877
878// Submodules for renderer features
879mod draw;
880mod external_textures;
881mod font_atlas;
882#[cfg(feature = "multi-viewport-winit")]
883pub mod multi_viewport;
884#[cfg(feature = "multi-viewport-sdl3")]
885pub mod multi_viewport_sdl3;
886mod pipeline;
887#[cfg(feature = "multi-viewport-sdl3")]
888mod sdl3_raw_window_handle;
889
890impl Default for WgpuRenderer {
891    fn default() -> Self {
892        Self::empty()
893    }
894}
895
896#[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
897impl Drop for WgpuRenderer {
898    fn drop(&mut self) {
899        // Make any installed multi-viewport callbacks become a no-op if the
900        // renderer is dropped without an explicit disable/shutdown call.
901        #[cfg(feature = "multi-viewport-winit")]
902        {
903            multi_viewport::clear_for_drop(self as *mut WgpuRenderer);
904        }
905        #[cfg(feature = "multi-viewport-sdl3")]
906        {
907            multi_viewport_sdl3::clear_for_drop(self as *mut WgpuRenderer);
908        }
909    }
910}