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