dear_imgui_wgpu/
renderer.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};
22use wgpu::*;
23
24// Debug logging helper (off by default). Enable by building this crate with
25// `--features mv-log` to see multi-viewport renderer traces.
26#[allow(unused_macros)]
27macro_rules! mvlog {
28    ($($arg:tt)*) => {
29        if cfg!(feature = "mv-log") { eprintln!($($arg)*); }
30    }
31}
32
33/// Main WGPU renderer for Dear ImGui
34///
35/// This corresponds to the main renderer functionality in imgui_impl_wgpu.cpp
36pub struct WgpuRenderer {
37    /// Backend data
38    backend_data: Option<WgpuBackendData>,
39    /// Shader manager
40    shader_manager: ShaderManager,
41    /// Texture manager
42    texture_manager: WgpuTextureManager,
43    /// Default texture for fallback
44    default_texture: Option<TextureView>,
45    /// Registered font atlas texture id (if created via font-atlas fallback)
46    font_texture_id: Option<u64>,
47    /// Gamma mode: automatic (by format), force linear (1.0), or force 2.2
48    gamma_mode: GammaMode,
49}
50
51impl WgpuRenderer {
52    /// Create a new WGPU renderer with full initialization (recommended)
53    ///
54    /// This is the preferred way to create a WGPU renderer as it ensures proper
55    /// initialization order and is consistent with other backends.
56    ///
57    /// # Arguments
58    /// * `init_info` - WGPU initialization information (device, queue, format)
59    /// * `imgui_ctx` - Dear ImGui context to configure
60    ///
61    /// # Example
62    /// ```rust,no_run
63    /// use dear_imgui_wgpu::{WgpuRenderer, WgpuInitInfo};
64    ///
65    /// let init_info = WgpuInitInfo::new(device, queue, surface_format);
66    /// let mut renderer = WgpuRenderer::new(init_info, &mut imgui_context)?;
67    /// ```
68    pub fn new(init_info: WgpuInitInfo, imgui_ctx: &mut Context) -> RendererResult<Self> {
69        let mut renderer = Self::empty();
70        renderer.init_with_context(init_info, imgui_ctx)?;
71        Ok(renderer)
72    }
73
74    /// Create an empty WGPU renderer for advanced usage
75    ///
76    /// This creates an uninitialized renderer that must be initialized later
77    /// using `init_with_context()`. Most users should use `new()` instead.
78    ///
79    /// # Example
80    /// ```rust,no_run
81    /// use dear_imgui_wgpu::{WgpuRenderer, WgpuInitInfo};
82    ///
83    /// let mut renderer = WgpuRenderer::empty();
84    /// let init_info = WgpuInitInfo::new(device, queue, surface_format);
85    /// renderer.init_with_context(init_info, &mut imgui_context)?;
86    /// ```
87    pub fn empty() -> Self {
88        Self {
89            backend_data: None,
90            shader_manager: ShaderManager::new(),
91            texture_manager: WgpuTextureManager::new(),
92            default_texture: None,
93            font_texture_id: None,
94            gamma_mode: GammaMode::Auto,
95        }
96    }
97
98    /// Initialize the renderer
99    ///
100    /// This corresponds to ImGui_ImplWGPU_Init in the C++ implementation
101    pub fn init(&mut self, init_info: WgpuInitInfo) -> RendererResult<()> {
102        // Create backend data
103        let mut backend_data = WgpuBackendData::new(init_info);
104
105        // Initialize render resources
106        backend_data
107            .render_resources
108            .initialize(&backend_data.device)?;
109
110        // Initialize shaders
111        self.shader_manager.initialize(&backend_data.device)?;
112
113        // Create default texture (1x1 white pixel)
114        let default_texture =
115            self.create_default_texture(&backend_data.device, &backend_data.queue)?;
116        self.default_texture = Some(default_texture);
117
118        // Create device objects (pipeline, etc.)
119        self.create_device_objects(&mut backend_data)?;
120
121        self.backend_data = Some(backend_data);
122        Ok(())
123    }
124
125    /// Initialize the renderer with ImGui context configuration (without font atlas for WASM)
126    ///
127    /// This is a variant of init_with_context that skips font atlas preparation,
128    /// useful for WASM builds where font atlas memory sharing is problematic.
129    pub fn new_without_font_atlas(
130        init_info: WgpuInitInfo,
131        imgui_ctx: &mut Context,
132    ) -> RendererResult<Self> {
133        let mut renderer = Self::empty();
134
135        // First initialize the renderer
136        renderer.init(init_info)?;
137
138        // Then configure the ImGui context with backend capabilities
139        renderer.configure_imgui_context(imgui_ctx);
140
141        // Skip font atlas preparation for WASM
142        // The default font will be used automatically by Dear ImGui
143
144        Ok(renderer)
145    }
146
147    /// Initialize the renderer with ImGui context configuration
148    ///
149    /// This is a convenience method that combines init() and configure_imgui_context()
150    /// to ensure proper initialization order, similar to the glow backend approach.
151    pub fn init_with_context(
152        &mut self,
153        init_info: WgpuInitInfo,
154        imgui_ctx: &mut Context,
155    ) -> RendererResult<()> {
156        // First initialize the renderer
157        self.init(init_info)?;
158
159        // Then configure the ImGui context with backend capabilities
160        // This must be done BEFORE preparing the font atlas
161        self.configure_imgui_context(imgui_ctx);
162
163        // Finally prepare the font atlas
164        self.prepare_font_atlas(imgui_ctx)?;
165
166        Ok(())
167    }
168
169    /// Set gamma mode
170    pub fn set_gamma_mode(&mut self, mode: GammaMode) {
171        self.gamma_mode = mode;
172    }
173
174    /// Configure Dear ImGui context with WGPU backend capabilities
175    pub fn configure_imgui_context(&self, imgui_context: &mut Context) {
176        let io = imgui_context.io_mut();
177        let mut flags = io.backend_flags();
178
179        // Set WGPU renderer capabilities
180        // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
181        flags.insert(BackendFlags::RENDERER_HAS_VTX_OFFSET);
182        // We can honor ImGuiPlatformIO::Textures[] requests during render.
183        flags.insert(BackendFlags::RENDERER_HAS_TEXTURES);
184
185        #[cfg(feature = "multi-viewport")]
186        {
187            // We can render additional platform windows
188            flags.insert(BackendFlags::RENDERER_HAS_VIEWPORTS);
189        }
190
191        io.set_backend_flags(flags);
192        // Note: Dear ImGui doesn't expose set_backend_renderer_name in the Rust bindings yet
193    }
194
195    /// Prepare font atlas for rendering
196    pub fn prepare_font_atlas(&mut self, imgui_ctx: &mut Context) -> RendererResult<()> {
197        if let Some(backend_data) = &self.backend_data {
198            let device = backend_data.device.clone();
199            let queue = backend_data.queue.clone();
200            self.reload_font_texture(imgui_ctx, &device, &queue)?;
201            // Fallback: if draw_data-based texture updates are not triggered for the font atlas
202            // on this Dear ImGui version/config, upload the font atlas now and assign a TexID.
203            if self.font_texture_id.is_none() {
204                if let Some(tex_id) =
205                    self.try_upload_font_atlas_legacy(imgui_ctx, &device, &queue)?
206                {
207                    if cfg!(debug_assertions) {
208                        tracing::debug!(
209                            target: "dear-imgui-wgpu",
210                            "[dear-imgui-wgpu][debug] Font atlas uploaded via fallback (legacy-only) path; user textures use modern ImTextureData. tex_id={}",
211                            tex_id
212                        );
213                    }
214                    self.font_texture_id = Some(tex_id);
215                }
216            } else if cfg!(debug_assertions) {
217                tracing::debug!(
218                    target: "dear-imgui-wgpu",
219                    "[dear-imgui-wgpu][debug] Font atlas tex_id already set: {:?}",
220                    self.font_texture_id
221                );
222            }
223        }
224        Ok(())
225    }
226
227    /// Create device objects (pipeline, etc.)
228    ///
229    /// This corresponds to ImGui_ImplWGPU_CreateDeviceObjects in the C++ implementation
230    fn create_device_objects(&mut self, backend_data: &mut WgpuBackendData) -> RendererResult<()> {
231        let device = &backend_data.device;
232
233        // Create bind group layouts
234        let (common_layout, image_layout) = crate::shaders::create_bind_group_layouts(device);
235
236        // Create pipeline layout
237        let pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
238            label: Some("Dear ImGui Pipeline Layout"),
239            bind_group_layouts: &[&common_layout, &image_layout],
240            push_constant_ranges: &[],
241        });
242
243        // Get shader module
244        let shader_module = self.shader_manager.get_shader_module()?;
245
246        // Create vertex buffer layout
247        let vertex_buffer_layout = crate::shaders::create_vertex_buffer_layout();
248        let vertex_buffer_layouts = [vertex_buffer_layout];
249
250        // Create vertex state
251        let vertex_state =
252            crate::shaders::create_vertex_state(shader_module, &vertex_buffer_layouts);
253
254        // Create color target state
255        let color_target = ColorTargetState {
256            format: backend_data.render_target_format,
257            blend: Some(BlendState {
258                color: BlendComponent {
259                    src_factor: BlendFactor::SrcAlpha,
260                    dst_factor: BlendFactor::OneMinusSrcAlpha,
261                    operation: BlendOperation::Add,
262                },
263                alpha: BlendComponent {
264                    src_factor: BlendFactor::One,
265                    dst_factor: BlendFactor::OneMinusSrcAlpha,
266                    operation: BlendOperation::Add,
267                },
268            }),
269            write_mask: ColorWrites::ALL,
270        };
271
272        // Determine if we need gamma correction based on format
273        let use_gamma_correction = matches!(
274            backend_data.render_target_format,
275            TextureFormat::Rgba8UnormSrgb
276                | TextureFormat::Bgra8UnormSrgb
277                | TextureFormat::Bc1RgbaUnormSrgb
278                | TextureFormat::Bc2RgbaUnormSrgb
279                | TextureFormat::Bc3RgbaUnormSrgb
280                | TextureFormat::Bc7RgbaUnormSrgb
281        );
282
283        // Create fragment state
284        let color_targets = [Some(color_target)];
285        let fragment_state = crate::shaders::create_fragment_state(
286            shader_module,
287            &color_targets,
288            use_gamma_correction,
289        );
290
291        // Create depth stencil state if needed (matches imgui_impl_wgpu.cpp depth-stencil setup)
292        let depth_stencil = backend_data
293            .depth_stencil_format
294            .map(|format| DepthStencilState {
295                format,
296                depth_write_enabled: false, // matches WGPUOptionalBool_False in C++
297                depth_compare: CompareFunction::Always, // matches WGPUCompareFunction_Always
298                stencil: StencilState {
299                    front: StencilFaceState {
300                        compare: CompareFunction::Always, // matches WGPUCompareFunction_Always
301                        fail_op: StencilOperation::Keep,  // matches WGPUStencilOperation_Keep
302                        depth_fail_op: StencilOperation::Keep, // matches WGPUStencilOperation_Keep
303                        pass_op: StencilOperation::Keep,  // matches WGPUStencilOperation_Keep
304                    },
305                    back: StencilFaceState {
306                        compare: CompareFunction::Always, // matches WGPUCompareFunction_Always
307                        fail_op: StencilOperation::Keep,  // matches WGPUStencilOperation_Keep
308                        depth_fail_op: StencilOperation::Keep, // matches WGPUStencilOperation_Keep
309                        pass_op: StencilOperation::Keep,  // matches WGPUStencilOperation_Keep
310                    },
311                    read_mask: 0xff,  // default value
312                    write_mask: 0xff, // default value
313                },
314                bias: DepthBiasState::default(),
315            });
316
317        // Create render pipeline
318        let pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
319            label: Some("Dear ImGui Render Pipeline"),
320            layout: Some(&pipeline_layout),
321            vertex: vertex_state,
322            primitive: PrimitiveState {
323                topology: PrimitiveTopology::TriangleList,
324                strip_index_format: None,
325                front_face: FrontFace::Cw,
326                cull_mode: None,
327                polygon_mode: PolygonMode::Fill,
328                unclipped_depth: false,
329                conservative: false,
330            },
331            depth_stencil,
332            multisample: backend_data.init_info.pipeline_multisample_state,
333            fragment: Some(fragment_state),
334            multiview: None,
335            cache: None,
336        });
337
338        backend_data.pipeline_state = Some(pipeline);
339        Ok(())
340    }
341
342    /// Create a default 1x1 white texture
343    fn create_default_texture(
344        &self,
345        device: &Device,
346        queue: &Queue,
347    ) -> RendererResult<TextureView> {
348        let texture = device.create_texture(&TextureDescriptor {
349            label: Some("Dear ImGui Default Texture"),
350            size: Extent3d {
351                width: 1,
352                height: 1,
353                depth_or_array_layers: 1,
354            },
355            mip_level_count: 1,
356            sample_count: 1,
357            dimension: TextureDimension::D2,
358            format: TextureFormat::Rgba8Unorm,
359            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
360            view_formats: &[],
361        });
362
363        // Upload white pixel
364        queue.write_texture(
365            wgpu::TexelCopyTextureInfo {
366                texture: &texture,
367                mip_level: 0,
368                origin: wgpu::Origin3d::ZERO,
369                aspect: wgpu::TextureAspect::All,
370            },
371            &[255u8, 255u8, 255u8, 255u8], // RGBA white
372            wgpu::TexelCopyBufferLayout {
373                offset: 0,
374                bytes_per_row: Some(4),
375                rows_per_image: Some(1),
376            },
377            Extent3d {
378                width: 1,
379                height: 1,
380                depth_or_array_layers: 1,
381            },
382        );
383
384        Ok(texture.create_view(&TextureViewDescriptor::default()))
385    }
386
387    /// Load font texture from Dear ImGui context
388    ///
389    /// With the new texture management system in Dear ImGui 1.92+, font textures are
390    /// automatically managed through ImDrawData->Textures[] during rendering.
391    /// However, we need to ensure the font atlas is built and ready before the first render.
392    fn reload_font_texture(
393        &mut self,
394        imgui_ctx: &mut Context,
395        _device: &Device,
396        _queue: &Queue,
397    ) -> RendererResult<()> {
398        let mut fonts = imgui_ctx.font_atlas_mut();
399        // Build the font atlas if not already built
400        // This prepares the font data but doesn't create GPU textures yet
401        if !fonts.is_built() {
402            fonts.build();
403        }
404
405        // Do not manually set TexRef/TexID here. With BackendFlags::RENDERER_HAS_TEXTURES,
406        // Dear ImGui will emit texture requests (WantCreate/WantUpdates) via DrawData::textures(),
407        // and our texture manager will create/upload the font texture on demand during rendering.
408
409        Ok(())
410    }
411
412    /// Legacy/fallback path: upload font atlas texture immediately and assign TexID.
413    /// Returns Some(tex_id) on success, None if texdata is unavailable.
414    fn try_upload_font_atlas_legacy(
415        &mut self,
416        imgui_ctx: &mut Context,
417        device: &Device,
418        queue: &Queue,
419    ) -> RendererResult<Option<u64>> {
420        // SAFETY: Access raw TexData/bytes only to copy pixels. Requires fonts.build() called.
421        let fonts = imgui_ctx.font_atlas();
422        // Try to read raw texture data to determine bytes-per-pixel
423        let raw_tex = fonts.get_tex_data();
424        if raw_tex.is_null() {
425            if cfg!(debug_assertions) {
426                tracing::debug!(
427                    target: "dear-imgui-wgpu",
428                    "[dear-imgui-wgpu][debug] Font atlas TexData is null; skip legacy upload"
429                );
430            }
431            return Ok(None);
432        }
433        // Read metadata
434        let (width, height, bpp, pixels_slice): (u32, u32, i32, Option<&[u8]>) = unsafe {
435            let w = (*raw_tex).Width as u32;
436            let h = (*raw_tex).Height as u32;
437            let bpp = (*raw_tex).BytesPerPixel;
438            let px_ptr = (*raw_tex).Pixels as *const u8;
439            if px_ptr.is_null() || w == 0 || h == 0 {
440                (w, h, bpp, None)
441            } else {
442                let size = (w as usize) * (h as usize) * (bpp as usize).max(1);
443                (w, h, bpp, Some(std::slice::from_raw_parts(px_ptr, size)))
444            }
445        };
446
447        if let Some(src) = pixels_slice {
448            if cfg!(debug_assertions) {
449                tracing::debug!(
450                    target: "dear-imgui-wgpu",
451                    "[dear-imgui-wgpu][debug] Font atlas texdata: {}x{} bpp={} (fallback upload for font atlas)",
452                    width, height, bpp
453                );
454            }
455            // Convert to RGBA8 if needed
456            let (format, converted): (wgpu::TextureFormat, Vec<u8>) = if bpp == 4 {
457                (wgpu::TextureFormat::Rgba8Unorm, src.to_vec())
458            } else if bpp == 1 {
459                // Alpha8 -> RGBA8 (white RGB + alpha)
460                let mut out = Vec::with_capacity((width as usize) * (height as usize) * 4);
461                for &a in src.iter() {
462                    out.extend_from_slice(&[255, 255, 255, a]);
463                }
464                (wgpu::TextureFormat::Rgba8Unorm, out)
465            } else {
466                // Unexpected format; don't proceed
467                if cfg!(debug_assertions) {
468                    tracing::debug!(
469                        target: "dear-imgui-wgpu",
470                        "[dear-imgui-wgpu][debug] Unexpected font atlas bpp={} -> skip",
471                        bpp
472                    );
473                }
474                return Ok(None);
475            };
476
477            // Create WGPU texture
478            let texture = device.create_texture(&wgpu::TextureDescriptor {
479                label: Some("Dear ImGui Font Atlas"),
480                size: wgpu::Extent3d {
481                    width,
482                    height,
483                    depth_or_array_layers: 1,
484                },
485                mip_level_count: 1,
486                sample_count: 1,
487                dimension: wgpu::TextureDimension::D2,
488                format,
489                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
490                view_formats: &[],
491            });
492            let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
493            // Write with 256-byte aligned row pitch
494            let bpp = 4u32;
495            let unpadded = width * bpp;
496            let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
497            let padded = unpadded.div_ceil(align) * align;
498            if padded == unpadded {
499                queue.write_texture(
500                    wgpu::TexelCopyTextureInfo {
501                        texture: &texture,
502                        mip_level: 0,
503                        origin: wgpu::Origin3d::ZERO,
504                        aspect: wgpu::TextureAspect::All,
505                    },
506                    &converted,
507                    wgpu::TexelCopyBufferLayout {
508                        offset: 0,
509                        bytes_per_row: Some(unpadded),
510                        rows_per_image: Some(height),
511                    },
512                    wgpu::Extent3d {
513                        width,
514                        height,
515                        depth_or_array_layers: 1,
516                    },
517                );
518            } else {
519                let mut padded_buf = vec![0u8; (padded * height) as usize];
520                for row in 0..height as usize {
521                    let src = row * (unpadded as usize);
522                    let dst = row * (padded as usize);
523                    padded_buf[dst..dst + (unpadded as usize)]
524                        .copy_from_slice(&converted[src..src + (unpadded as usize)]);
525                }
526                queue.write_texture(
527                    wgpu::TexelCopyTextureInfo {
528                        texture: &texture,
529                        mip_level: 0,
530                        origin: wgpu::Origin3d::ZERO,
531                        aspect: wgpu::TextureAspect::All,
532                    },
533                    &padded_buf,
534                    wgpu::TexelCopyBufferLayout {
535                        offset: 0,
536                        bytes_per_row: Some(padded),
537                        rows_per_image: Some(height),
538                    },
539                    wgpu::Extent3d {
540                        width,
541                        height,
542                        depth_or_array_layers: 1,
543                    },
544                );
545                if cfg!(debug_assertions) {
546                    tracing::debug!(
547                        target: "dear-imgui-wgpu",
548                        "[dear-imgui-wgpu][debug] Upload font atlas with padded row pitch: unpadded={} padded={}",
549                        unpadded, padded
550                    );
551                }
552            }
553
554            // Register texture and set IDs so draw commands can bind it
555            let tex_id = self
556                .texture_manager
557                .register_texture(crate::WgpuTexture::new(texture, view));
558
559            // Set atlas texture id + status OK (updates TexRef and TexData)
560            {
561                let mut fonts_mut = imgui_ctx.font_atlas_mut();
562                fonts_mut.set_texture_id(dear_imgui_rs::TextureId::from(tex_id));
563            }
564            if cfg!(debug_assertions) {
565                tracing::debug!(
566                    target: "dear-imgui-wgpu",
567                    "[dear-imgui-wgpu][debug] Font atlas fallback upload complete: tex_id={}",
568                    tex_id
569                );
570            }
571
572            return Ok(Some(tex_id));
573        }
574        if cfg!(debug_assertions) {
575            tracing::debug!(
576                target: "dear-imgui-wgpu",
577                "[dear-imgui-wgpu][debug] Font atlas has no CPU pixel buffer; skipping fallback upload (renderer will use modern texture updates)"
578            );
579        }
580        Ok(None)
581    }
582
583    /// Get the texture manager
584    pub fn texture_manager(&self) -> &WgpuTextureManager {
585        &self.texture_manager
586    }
587
588    /// Get the texture manager mutably
589    pub fn texture_manager_mut(&mut self) -> &mut WgpuTextureManager {
590        &mut self.texture_manager
591    }
592
593    /// Check if the renderer is initialized
594    pub fn is_initialized(&self) -> bool {
595        self.backend_data.is_some()
596    }
597
598    /// Update a single texture manually
599    ///
600    /// This corresponds to ImGui_ImplWGPU_UpdateTexture in the C++ implementation.
601    /// Use this when you need precise control over texture update timing.
602    ///
603    /// # Returns
604    ///
605    /// Returns a `TextureUpdateResult` that contains any status/ID updates that need
606    /// to be applied to the texture data. This follows Rust's principle of explicit
607    /// state management.
608    ///
609    /// # Example
610    ///
611    /// ```rust,no_run
612    /// # use dear_imgui_wgpu::*;
613    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
614    /// # let mut renderer = WgpuRenderer::new();
615    /// # let mut texture_data = dear_imgui_rs::TextureData::new();
616    /// let result = renderer.update_texture(&texture_data)?;
617    /// result.apply_to(&mut texture_data);
618    /// # Ok(())
619    /// # }
620    /// ```
621    pub fn update_texture(
622        &mut self,
623        texture_data: &dear_imgui_rs::TextureData,
624    ) -> RendererResult<crate::TextureUpdateResult> {
625        if let Some(backend_data) = &self.backend_data {
626            self.texture_manager
627                .update_single_texture(texture_data, &backend_data.device, &backend_data.queue)
628                .map_err(RendererError::TextureCreationFailed)
629        } else {
630            Err(RendererError::InvalidRenderState(
631                "Renderer not initialized".to_string(),
632            ))
633        }
634    }
635
636    /// Called every frame to prepare for rendering
637    ///
638    /// This corresponds to ImGui_ImplWGPU_NewFrame in the C++ implementation
639    pub fn new_frame(&mut self) -> RendererResult<()> {
640        let needs_recreation = if let Some(backend_data) = &self.backend_data {
641            backend_data.pipeline_state.is_none()
642        } else {
643            false
644        };
645
646        if needs_recreation {
647            // Extract the backend data temporarily to avoid borrow checker issues
648            let mut backend_data = self.backend_data.take().unwrap();
649            self.create_device_objects(&mut backend_data)?;
650            self.backend_data = Some(backend_data);
651        }
652        Ok(())
653    }
654
655    /// Render Dear ImGui draw data
656    ///
657    /// This corresponds to ImGui_ImplWGPU_RenderDrawData in the C++ implementation
658    pub fn render_draw_data(
659        &mut self,
660        draw_data: &DrawData,
661        render_pass: &mut RenderPass,
662    ) -> RendererResult<()> {
663        mvlog!(
664            "[wgpu-mv] render_draw_data: valid={} lists={} fb_scale=({:.2},{:.2}) disp=({:.1},{:.1})",
665            draw_data.valid(),
666            draw_data.draw_lists_count(),
667            draw_data.framebuffer_scale()[0],
668            draw_data.framebuffer_scale()[1],
669            draw_data.display_size()[0],
670            draw_data.display_size()[1]
671        );
672        // Early out if nothing to draw (avoid binding/drawing without buffers)
673        let mut total_vtx_count = 0usize;
674        let mut total_idx_count = 0usize;
675        for dl in draw_data.draw_lists() {
676            total_vtx_count += dl.vtx_buffer().len();
677            total_idx_count += dl.idx_buffer().len();
678        }
679        if total_vtx_count == 0 || total_idx_count == 0 {
680            mvlog!("[wgpu-mv] no vertices/indices; skipping render");
681            return Ok(());
682        }
683
684        let backend_data = self.backend_data.as_mut().ok_or_else(|| {
685            RendererError::InvalidRenderState("Renderer not initialized".to_string())
686        })?;
687
688        // Avoid rendering when minimized
689        let fb_width = (draw_data.display_size[0] * draw_data.framebuffer_scale[0]) as i32;
690        let fb_height = (draw_data.display_size[1] * draw_data.framebuffer_scale[1]) as i32;
691        if fb_width <= 0 || fb_height <= 0 || !draw_data.valid() {
692            return Ok(());
693        }
694
695        mvlog!("[wgpu-mv] handle_texture_updates");
696        self.texture_manager.handle_texture_updates(
697            draw_data,
698            &backend_data.device,
699            &backend_data.queue,
700        );
701
702        // Advance to next frame
703        mvlog!("[wgpu-mv] next_frame before: {}", backend_data.frame_index);
704        backend_data.next_frame();
705        mvlog!("[wgpu-mv] next_frame after: {}", backend_data.frame_index);
706
707        // Prepare frame resources
708        mvlog!("[wgpu-mv] prepare_frame_resources");
709        Self::prepare_frame_resources_static(draw_data, backend_data)?;
710
711        // Compute gamma based on renderer mode
712        let gamma = match self.gamma_mode {
713            GammaMode::Auto => Uniforms::gamma_for_format(backend_data.render_target_format),
714            GammaMode::Linear => 1.0,
715            GammaMode::Gamma22 => 2.2,
716        };
717
718        // Setup render state
719        mvlog!("[wgpu-mv] setup_render_state");
720        Self::setup_render_state_static(draw_data, render_pass, backend_data, gamma)?;
721        // Override viewport to the provided framebuffer size to avoid partial viewport issues
722        render_pass.set_viewport(0.0, 0.0, fb_width as f32, fb_height as f32, 0.0, 1.0);
723
724        // Setup render state structure (for callbacks and custom texture bindings)
725        // Note: We need to be careful with lifetimes here, so we'll set it just before rendering
726        // and clear it immediately after
727        unsafe {
728            // Use _Nil variant as our bindings export it
729            let platform_io = dear_imgui_rs::sys::igGetPlatformIO_Nil();
730
731            // Create a temporary render state structure
732            mvlog!("[wgpu-mv] create render_state");
733            let mut render_state = crate::WgpuRenderState::new(&backend_data.device, render_pass);
734
735            // Set the render state pointer
736            (*platform_io).Renderer_RenderState =
737                &mut render_state as *mut _ as *mut std::ffi::c_void;
738
739            // Render draw lists with the render state exposed
740            let result = Self::render_draw_lists_static(
741                &mut self.texture_manager,
742                &self.default_texture,
743                draw_data,
744                render_pass,
745                backend_data,
746                gamma,
747            );
748
749            // Clear the render state pointer
750            (*platform_io).Renderer_RenderState = std::ptr::null_mut();
751
752            if let Err(e) = result {
753                eprintln!("[wgpu-mv] render_draw_lists_static error: {:?}", e);
754                return Err(e);
755            }
756        }
757
758        Ok(())
759    }
760
761    /// Render Dear ImGui draw data with explicit framebuffer size override
762    ///
763    /// This clones the logic of `render_draw_data` but clamps scissor to the provided
764    /// `fb_width`/`fb_height` instead of deriving it from `DrawData`.
765    pub fn render_draw_data_with_fb_size(
766        &mut self,
767        draw_data: &DrawData,
768        render_pass: &mut RenderPass,
769        fb_width: u32,
770        fb_height: u32,
771    ) -> RendererResult<()> {
772        mvlog!(
773            "[wgpu-mv] render_draw_data(with_fb) lists={} override_fb=({}, {}) disp=({:.1},{:.1})",
774            draw_data.draw_lists_count(),
775            fb_width,
776            fb_height,
777            draw_data.display_size()[0],
778            draw_data.display_size()[1]
779        );
780        let total_vtx_count: usize = draw_data.draw_lists().map(|dl| dl.vtx_buffer().len()).sum();
781        let total_idx_count: usize = draw_data.draw_lists().map(|dl| dl.idx_buffer().len()).sum();
782        if total_vtx_count == 0 || total_idx_count == 0 {
783            return Ok(());
784        }
785        let backend_data = self.backend_data.as_mut().ok_or_else(|| {
786            RendererError::InvalidRenderState("Renderer not initialized".to_string())
787        })?;
788
789        // Skip if invalid/minimized
790        if fb_width == 0 || fb_height == 0 || !draw_data.valid() {
791            return Ok(());
792        }
793
794        self.texture_manager.handle_texture_updates(
795            draw_data,
796            &backend_data.device,
797            &backend_data.queue,
798        );
799
800        backend_data.next_frame();
801        Self::prepare_frame_resources_static(draw_data, backend_data)?;
802
803        let gamma = match self.gamma_mode {
804            GammaMode::Auto => Uniforms::gamma_for_format(backend_data.render_target_format),
805            GammaMode::Linear => 1.0,
806            GammaMode::Gamma22 => 2.2,
807        };
808
809        Self::setup_render_state_static(draw_data, render_pass, backend_data, gamma)?;
810
811        unsafe {
812            let platform_io = dear_imgui_rs::sys::igGetPlatformIO_Nil();
813            let mut render_state = crate::WgpuRenderState::new(&backend_data.device, render_pass);
814            (*platform_io).Renderer_RenderState =
815                &mut render_state as *mut _ as *mut std::ffi::c_void;
816
817            // Reuse core routine but clamp scissor by overriding framebuffer bounds.
818            let mut global_idx_offset: u32 = 0;
819            let mut global_vtx_offset: i32 = 0;
820            let clip_off = draw_data.display_pos();
821            let clip_scale = draw_data.framebuffer_scale();
822            let fbw = fb_width as f32;
823            let fbh = fb_height as f32;
824
825            for draw_list in draw_data.draw_lists() {
826                let vtx_buffer = draw_list.vtx_buffer();
827                let idx_buffer = draw_list.idx_buffer();
828                let mut cmd_i = 0;
829                for cmd in draw_list.commands() {
830                    match cmd {
831                        dear_imgui_rs::render::DrawCmd::Elements {
832                            count,
833                            cmd_params,
834                            raw_cmd,
835                        } => {
836                            // Texture bind group resolution mirrors render_draw_lists_static
837                            let texture_bind_group = {
838                                // Resolve effective ImTextureID using raw_cmd (modern texture path)
839                                let tex_id = unsafe {
840                                    dear_imgui_rs::sys::ImDrawCmd_GetTexID(
841                                        raw_cmd as *mut dear_imgui_rs::sys::ImDrawCmd,
842                                    )
843                                } as u64;
844                                if tex_id == 0 {
845                                    if let Some(default_tex) = &self.default_texture {
846                                        backend_data
847                                            .render_resources
848                                            .get_or_create_image_bind_group(
849                                                &backend_data.device,
850                                                0,
851                                                default_tex,
852                                            )?
853                                            .clone()
854                                    } else {
855                                        return Err(RendererError::InvalidRenderState(
856                                            "Default texture not available".to_string(),
857                                        ));
858                                    }
859                                } else if let Some(wgpu_texture) =
860                                    self.texture_manager.get_texture(tex_id)
861                                {
862                                    backend_data
863                                        .render_resources
864                                        .get_or_create_image_bind_group(
865                                            &backend_data.device,
866                                            tex_id,
867                                            &wgpu_texture.texture_view,
868                                        )?
869                                        .clone()
870                                } else if let Some(default_tex) = &self.default_texture {
871                                    backend_data
872                                        .render_resources
873                                        .get_or_create_image_bind_group(
874                                            &backend_data.device,
875                                            0,
876                                            default_tex,
877                                        )?
878                                        .clone()
879                                } else {
880                                    return Err(RendererError::InvalidRenderState(
881                                        "Texture not found and no default texture".to_string(),
882                                    ));
883                                }
884                            };
885                            render_pass.set_bind_group(1, &texture_bind_group, &[]);
886
887                            // Compute clip rect in framebuffer space
888                            let mut clip_min_x =
889                                (cmd_params.clip_rect[0] - clip_off[0]) * clip_scale[0];
890                            let mut clip_min_y =
891                                (cmd_params.clip_rect[1] - clip_off[1]) * clip_scale[1];
892                            let mut clip_max_x =
893                                (cmd_params.clip_rect[2] - clip_off[0]) * clip_scale[0];
894                            let mut clip_max_y =
895                                (cmd_params.clip_rect[3] - clip_off[1]) * clip_scale[1];
896                            // Clamp to override framebuffer bounds
897                            clip_min_x = clip_min_x.max(0.0);
898                            clip_min_y = clip_min_y.max(0.0);
899                            clip_max_x = clip_max_x.min(fbw);
900                            clip_max_y = clip_max_y.min(fbh);
901                            if clip_max_x <= clip_min_x || clip_max_y <= clip_min_y {
902                                cmd_i += 1;
903                                continue;
904                            }
905                            render_pass.set_scissor_rect(
906                                clip_min_x as u32,
907                                clip_min_y as u32,
908                                (clip_max_x - clip_min_x) as u32,
909                                (clip_max_y - clip_min_y) as u32,
910                            );
911                            let start_index = cmd_params.idx_offset as u32 + global_idx_offset;
912                            let end_index = start_index + count as u32;
913                            let vertex_offset = (cmd_params.vtx_offset as i32) + global_vtx_offset;
914                            render_pass.draw_indexed(start_index..end_index, vertex_offset, 0..1);
915                        }
916                        dear_imgui_rs::render::DrawCmd::ResetRenderState => {
917                            Self::setup_render_state_static(
918                                draw_data,
919                                render_pass,
920                                backend_data,
921                                gamma,
922                            )?;
923                        }
924                        dear_imgui_rs::render::DrawCmd::RawCallback { .. } => {
925                            // Unsupported raw callbacks; skip.
926                        }
927                    }
928                    cmd_i += 1;
929                }
930
931                global_idx_offset += idx_buffer.len() as u32;
932                global_vtx_offset += vtx_buffer.len() as i32;
933            }
934
935            (*platform_io).Renderer_RenderState = std::ptr::null_mut();
936        }
937
938        Ok(())
939    }
940
941    /// Prepare frame resources (buffers)
942    fn prepare_frame_resources_static(
943        draw_data: &DrawData,
944        backend_data: &mut WgpuBackendData,
945    ) -> RendererResult<()> {
946        mvlog!("[wgpu-mv] totals start");
947        // Calculate total vertex and index counts
948        let mut total_vtx_count = 0;
949        let mut total_idx_count = 0;
950        for draw_list in draw_data.draw_lists() {
951            total_vtx_count += draw_list.vtx_buffer().len();
952            total_idx_count += draw_list.idx_buffer().len();
953        }
954        mvlog!(
955            "[wgpu-mv] totals vtx={} idx={}",
956            total_vtx_count,
957            total_idx_count
958        );
959
960        if total_vtx_count == 0 || total_idx_count == 0 {
961            return Ok(());
962        }
963
964        // Collect all vertices and indices first
965        let mut vertices = Vec::with_capacity(total_vtx_count);
966        let mut indices = Vec::with_capacity(total_idx_count);
967
968        for draw_list in draw_data.draw_lists() {
969            vertices.extend_from_slice(draw_list.vtx_buffer());
970            indices.extend_from_slice(draw_list.idx_buffer());
971        }
972
973        // Get current frame resources and update buffers
974        let frame_index = backend_data.frame_index % backend_data.num_frames_in_flight;
975        let frame_resources = &mut backend_data.frame_resources[frame_index as usize];
976
977        // Ensure buffer capacity and upload data
978        frame_resources
979            .ensure_vertex_buffer_capacity(&backend_data.device, total_vtx_count)
980            .map_err(RendererError::BufferCreationFailed)?;
981        frame_resources
982            .ensure_index_buffer_capacity(&backend_data.device, total_idx_count)
983            .map_err(RendererError::BufferCreationFailed)?;
984
985        frame_resources
986            .upload_vertex_data(&backend_data.queue, &vertices)
987            .map_err(RendererError::BufferCreationFailed)?;
988        frame_resources
989            .upload_index_data(&backend_data.queue, &indices)
990            .map_err(RendererError::BufferCreationFailed)?;
991
992        Ok(())
993    }
994
995    /// Setup render state
996    ///
997    /// This corresponds to ImGui_ImplWGPU_SetupRenderState in the C++ implementation
998    fn setup_render_state_static(
999        draw_data: &DrawData,
1000        render_pass: &mut RenderPass,
1001        backend_data: &WgpuBackendData,
1002        gamma: f32,
1003    ) -> RendererResult<()> {
1004        let pipeline = backend_data
1005            .pipeline_state
1006            .as_ref()
1007            .ok_or_else(|| RendererError::InvalidRenderState("Pipeline not created".to_string()))?;
1008
1009        // Setup viewport
1010        let fb_width = draw_data.display_size[0] * draw_data.framebuffer_scale[0];
1011        let fb_height = draw_data.display_size[1] * draw_data.framebuffer_scale[1];
1012        render_pass.set_viewport(0.0, 0.0, fb_width, fb_height, 0.0, 1.0);
1013
1014        // Set pipeline
1015        render_pass.set_pipeline(pipeline);
1016
1017        // Update uniforms
1018        let mvp =
1019            Uniforms::create_orthographic_matrix(draw_data.display_pos, draw_data.display_size);
1020        let mut uniforms = Uniforms::new();
1021        uniforms.update(mvp, gamma);
1022
1023        // Update uniform buffer
1024        if let Some(uniform_buffer) = backend_data.render_resources.uniform_buffer() {
1025            uniform_buffer.update(&backend_data.queue, &uniforms);
1026            render_pass.set_bind_group(0, uniform_buffer.bind_group(), &[]);
1027        }
1028
1029        // Set vertex and index buffers
1030        let frame_resources = &backend_data.frame_resources
1031            [(backend_data.frame_index % backend_data.num_frames_in_flight) as usize];
1032        if let (Some(vertex_buffer), Some(index_buffer)) = (
1033            frame_resources.vertex_buffer(),
1034            frame_resources.index_buffer(),
1035        ) {
1036            render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
1037            render_pass.set_index_buffer(index_buffer.slice(..), IndexFormat::Uint16);
1038        }
1039
1040        Ok(())
1041    }
1042
1043    /// Render all draw lists
1044    fn render_draw_lists_static(
1045        texture_manager: &mut WgpuTextureManager,
1046        default_texture: &Option<TextureView>,
1047        draw_data: &DrawData,
1048        render_pass: &mut RenderPass,
1049        backend_data: &mut WgpuBackendData,
1050        gamma: f32,
1051    ) -> RendererResult<()> {
1052        let mut global_vtx_offset = 0i32;
1053        let mut global_idx_offset = 0u32;
1054        let clip_scale = draw_data.framebuffer_scale;
1055        let clip_off = draw_data.display_pos;
1056        let fb_width = draw_data.display_size[0] * draw_data.framebuffer_scale[0];
1057        let fb_height = draw_data.display_size[1] * draw_data.framebuffer_scale[1];
1058
1059        let mut list_i = 0usize;
1060        for draw_list in draw_data.draw_lists() {
1061            mvlog!(
1062                "[wgpu-mv] list[{}]: vtx={} idx={} cmds~?",
1063                list_i,
1064                draw_list.vtx_buffer().len(),
1065                draw_list.idx_buffer().len()
1066            );
1067            let mut cmd_i = 0usize;
1068            for cmd in draw_list.commands() {
1069                match cmd {
1070                    dear_imgui_rs::render::DrawCmd::Elements {
1071                        count,
1072                        cmd_params,
1073                        raw_cmd,
1074                    } => {
1075                        mvlog!(
1076                            "[wgpu-mv] list[{}] cmd[{}]: count={} tex=?",
1077                            list_i,
1078                            cmd_i,
1079                            count
1080                        );
1081                        // Get texture bind group
1082                        //
1083                        // Dear ImGui 1.92+ (modern texture system): draw commands may carry
1084                        // an ImTextureRef whose `TexID` is 0 in the raw field, and the effective
1085                        // id must be resolved via ImDrawCmd_GetTexID using the backend's
1086                        // Renderer_RenderState. We call it here, after texture updates have been
1087                        // handled for this frame, so it returns a valid non-zero id.
1088                        let texture_bind_group = {
1089                            // Resolve effective ImTextureID now (after texture updates)
1090                            let tex_id = unsafe {
1091                                dear_imgui_rs::sys::ImDrawCmd_GetTexID(
1092                                    raw_cmd as *mut dear_imgui_rs::sys::ImDrawCmd,
1093                                )
1094                            } as u64;
1095                            if tex_id == 0 {
1096                                // Use default texture for null/invalid texture ID
1097                                if let Some(default_tex) = default_texture {
1098                                    backend_data
1099                                        .render_resources
1100                                        .get_or_create_image_bind_group(
1101                                            &backend_data.device,
1102                                            0,
1103                                            default_tex,
1104                                        )?
1105                                        .clone()
1106                                } else {
1107                                    return Err(RendererError::InvalidRenderState(
1108                                        "Default texture not available".to_string(),
1109                                    ));
1110                                }
1111                            } else if let Some(wgpu_texture) = texture_manager.get_texture(tex_id) {
1112                                backend_data
1113                                    .render_resources
1114                                    .get_or_create_image_bind_group(
1115                                        &backend_data.device,
1116                                        tex_id,
1117                                        wgpu_texture.view(),
1118                                    )?
1119                                    .clone()
1120                            } else {
1121                                // Fallback to default texture if texture not found
1122                                if let Some(default_tex) = default_texture {
1123                                    backend_data
1124                                        .render_resources
1125                                        .get_or_create_image_bind_group(
1126                                            &backend_data.device,
1127                                            0,
1128                                            default_tex,
1129                                        )?
1130                                        .clone()
1131                                } else {
1132                                    return Err(RendererError::InvalidRenderState(
1133                                        "Texture not found and no default texture".to_string(),
1134                                    ));
1135                                }
1136                            }
1137                        };
1138
1139                        // Set texture bind group
1140                        render_pass.set_bind_group(1, &texture_bind_group, &[]);
1141
1142                        // Project scissor/clipping rectangles into framebuffer space
1143                        let clip_min_x = (cmd_params.clip_rect[0] - clip_off[0]) * clip_scale[0];
1144                        let clip_min_y = (cmd_params.clip_rect[1] - clip_off[1]) * clip_scale[1];
1145                        let clip_max_x = (cmd_params.clip_rect[2] - clip_off[0]) * clip_scale[0];
1146                        let clip_max_y = (cmd_params.clip_rect[3] - clip_off[1]) * clip_scale[1];
1147
1148                        // Clamp to viewport
1149                        let clip_min_x = clip_min_x.max(0.0);
1150                        let clip_min_y = clip_min_y.max(0.0);
1151                        let clip_max_x = clip_max_x.min(fb_width);
1152                        let clip_max_y = clip_max_y.min(fb_height);
1153
1154                        if clip_max_x <= clip_min_x || clip_max_y <= clip_min_y {
1155                            continue;
1156                        }
1157
1158                        // Apply scissor/clipping rectangle
1159                        render_pass.set_scissor_rect(
1160                            clip_min_x as u32,
1161                            clip_min_y as u32,
1162                            (clip_max_x - clip_min_x) as u32,
1163                            (clip_max_y - clip_min_y) as u32,
1164                        );
1165
1166                        // Draw
1167                        let start_index = cmd_params.idx_offset as u32 + global_idx_offset;
1168                        let end_index = start_index + count as u32;
1169                        let vertex_offset = (cmd_params.vtx_offset as i32) + global_vtx_offset;
1170                        render_pass.draw_indexed(start_index..end_index, vertex_offset, 0..1);
1171                    }
1172                    dear_imgui_rs::render::DrawCmd::ResetRenderState => {
1173                        // Re-apply render state using the same gamma
1174                        Self::setup_render_state_static(
1175                            draw_data,
1176                            render_pass,
1177                            backend_data,
1178                            gamma,
1179                        )?;
1180                    }
1181                    dear_imgui_rs::render::DrawCmd::RawCallback { .. } => {
1182                        // Raw callbacks are not supported in this implementation
1183                        // They would require access to the raw Dear ImGui draw list
1184                        tracing::warn!(
1185                            target: "dear-imgui-wgpu",
1186                            "Warning: Raw callbacks are not supported in WGPU renderer"
1187                        );
1188                    }
1189                }
1190                cmd_i += 1;
1191            }
1192
1193            global_idx_offset += draw_list.idx_buffer().len() as u32;
1194            global_vtx_offset += draw_list.vtx_buffer().len() as i32;
1195            list_i += 1;
1196        }
1197
1198        Ok(())
1199    }
1200
1201    /// Invalidate device objects
1202    ///
1203    /// This corresponds to ImGui_ImplWGPU_InvalidateDeviceObjects in the C++ implementation
1204    pub fn invalidate_device_objects(&mut self) -> RendererResult<()> {
1205        if let Some(ref mut backend_data) = self.backend_data {
1206            backend_data.pipeline_state = None;
1207            backend_data.render_resources = RenderResources::new();
1208
1209            // Clear frame resources
1210            for frame_resources in &mut backend_data.frame_resources {
1211                *frame_resources = FrameResources::new();
1212            }
1213        }
1214
1215        // Clear texture manager
1216        self.texture_manager.clear();
1217        self.default_texture = None;
1218        self.font_texture_id = None;
1219
1220        Ok(())
1221    }
1222
1223    /// Shutdown the renderer
1224    ///
1225    /// This corresponds to ImGui_ImplWGPU_Shutdown in the C++ implementation
1226    pub fn shutdown(&mut self) {
1227        self.invalidate_device_objects().ok();
1228        self.backend_data = None;
1229    }
1230}
1231
1232impl Default for WgpuRenderer {
1233    fn default() -> Self {
1234        Self::empty()
1235    }
1236}
1237
1238/// Multi-viewport support (Renderer_* callbacks and helpers)
1239#[cfg(feature = "multi-viewport")]
1240pub mod multi_viewport {
1241    use super::*;
1242    use dear_imgui_rs::platform_io::Viewport;
1243    use std::ffi::c_void;
1244    use std::sync::OnceLock;
1245    use std::sync::atomic::{AtomicUsize, Ordering};
1246    #[cfg(not(target_arch = "wasm32"))]
1247    use winit::window::Window;
1248
1249    /// Per-viewport WGPU data stored in ImGuiViewport::RendererUserData
1250    pub struct ViewportWgpuData {
1251        pub surface: wgpu::Surface<'static>,
1252        pub config: wgpu::SurfaceConfiguration,
1253        pub pending_frame: Option<wgpu::SurfaceTexture>,
1254    }
1255
1256    static RENDERER_PTR: AtomicUsize = AtomicUsize::new(0);
1257    static GLOBAL: OnceLock<GlobalHandles> = OnceLock::new();
1258
1259    struct GlobalHandles {
1260        instance: Option<wgpu::Instance>,
1261        adapter: Option<wgpu::Adapter>,
1262        device: wgpu::Device,
1263        queue: wgpu::Queue,
1264        render_target_format: wgpu::TextureFormat,
1265    }
1266
1267    /// Enable WGPU multi-viewport: set per-viewport callbacks and capture renderer pointer
1268    pub fn enable(renderer: &mut WgpuRenderer, imgui_context: &mut Context) {
1269        // Expose callbacks through PlatformIO
1270        unsafe {
1271            let platform_io = imgui_context.platform_io_mut();
1272            platform_io.set_renderer_create_window(Some(
1273                renderer_create_window as unsafe extern "C" fn(*mut Viewport),
1274            ));
1275            platform_io.set_renderer_destroy_window(Some(
1276                renderer_destroy_window as unsafe extern "C" fn(*mut Viewport),
1277            ));
1278            platform_io.set_renderer_set_window_size(Some(
1279                renderer_set_window_size
1280                    as unsafe extern "C" fn(*mut Viewport, dear_imgui_rs::sys::ImVec2),
1281            ));
1282            // Route rendering via platform raw callbacks to avoid typed trampolines
1283            platform_io.set_platform_render_window_raw(Some(platform_render_window_sys));
1284            platform_io.set_platform_swap_buffers_raw(Some(platform_swap_buffers_sys));
1285        }
1286
1287        // Store self pointer for callbacks
1288        RENDERER_PTR.store(renderer as *mut _ as usize, Ordering::SeqCst);
1289
1290        // Also store global handles so creation/resizing callbacks don't rely on renderer pointer stability
1291        if let Some(backend) = renderer.backend_data.as_ref() {
1292            let _ = GLOBAL.set(GlobalHandles {
1293                instance: backend.instance.clone(),
1294                adapter: backend.adapter.clone(),
1295                device: backend.device.clone(),
1296                queue: backend.queue.clone(),
1297                render_target_format: backend.render_target_format,
1298            });
1299        }
1300    }
1301
1302    unsafe fn get_renderer<'a>() -> &'a mut WgpuRenderer {
1303        let ptr = RENDERER_PTR.load(Ordering::SeqCst) as *mut WgpuRenderer;
1304        &mut *ptr
1305    }
1306
1307    /// Helper to get or create per-viewport user data
1308    unsafe fn viewport_user_data_mut<'a>(vp: *mut Viewport) -> Option<&'a mut ViewportWgpuData> {
1309        let vpm = &mut *vp;
1310        let data = vpm.renderer_user_data();
1311        if data.is_null() {
1312            None
1313        } else {
1314            Some(&mut *(data as *mut ViewportWgpuData))
1315        }
1316    }
1317
1318    /// Renderer: create per-viewport resources (surface + config)
1319    pub unsafe extern "C" fn renderer_create_window(vp: *mut Viewport) {
1320        if vp.is_null() {
1321            return;
1322        }
1323        mvlog!("[wgpu-mv] Renderer_CreateWindow");
1324
1325        let global = match GLOBAL.get() {
1326            Some(g) => g,
1327            None => return,
1328        };
1329
1330        // Obtain window from platform handle
1331        let vpm = &mut *vp;
1332        let window_ptr = vpm.platform_handle();
1333        if window_ptr.is_null() {
1334            return;
1335        }
1336
1337        #[cfg(not(target_arch = "wasm32"))]
1338        let window: &Window = &*(window_ptr as *const Window);
1339
1340        #[cfg(not(target_arch = "wasm32"))]
1341        let instance = match &global.instance {
1342            Some(i) => i.clone(),
1343            None => return, // cannot create surfaces without instance
1344        };
1345
1346        #[cfg(not(target_arch = "wasm32"))]
1347        let surface = match instance.create_surface(window) {
1348            Ok(s) => s,
1349            Err(e) => {
1350                eprintln!("[wgpu-mv] create_surface error: {:?}", e);
1351                return;
1352            }
1353        };
1354        // Extend surface lifetime to 'static by tying it to backend-owned instance
1355        let surface: wgpu::Surface<'static> = std::mem::transmute(surface);
1356
1357        #[cfg(not(target_arch = "wasm32"))]
1358        let size = window.inner_size();
1359        #[cfg(not(target_arch = "wasm32"))]
1360        let width = size.width.max(1);
1361        #[cfg(not(target_arch = "wasm32"))]
1362        let height = size.height.max(1);
1363
1364        #[cfg(not(target_arch = "wasm32"))]
1365        let mut config = {
1366            // Prefer the renderer's main format if the surface supports it; otherwise, bail out gracefully
1367            if let Some(adapter) = &global.adapter {
1368                let caps = surface.get_capabilities(adapter);
1369                let format = if caps.formats.contains(&global.render_target_format) {
1370                    global.render_target_format
1371                } else {
1372                    // If the main pipeline format isn't supported, we cannot render safely with this pipeline.
1373                    eprintln!(
1374                        "[wgpu-mv] Surface doesn't support pipeline format {:?}; supported: {:?}. Skipping configure.",
1375                        global.render_target_format, caps.formats
1376                    );
1377                    return;
1378                };
1379                let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
1380                    wgpu::PresentMode::Fifo
1381                } else {
1382                    // Fallback to first supported present mode
1383                    caps.present_modes
1384                        .get(0)
1385                        .cloned()
1386                        .unwrap_or(wgpu::PresentMode::Fifo)
1387                };
1388                let alpha_mode = if caps.alpha_modes.contains(&wgpu::CompositeAlphaMode::Opaque) {
1389                    wgpu::CompositeAlphaMode::Opaque
1390                } else if caps.alpha_modes.contains(&wgpu::CompositeAlphaMode::Auto) {
1391                    wgpu::CompositeAlphaMode::Auto
1392                } else {
1393                    caps.alpha_modes
1394                        .get(0)
1395                        .cloned()
1396                        .unwrap_or(wgpu::CompositeAlphaMode::Opaque)
1397                };
1398                wgpu::SurfaceConfiguration {
1399                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1400                    format,
1401                    width,
1402                    height,
1403                    present_mode,
1404                    alpha_mode,
1405                    view_formats: vec![format],
1406                    desired_maximum_frame_latency: 1,
1407                }
1408            } else {
1409                // No adapter available: assume the same format as main and attempt configure (best-effort)
1410                wgpu::SurfaceConfiguration {
1411                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
1412                    format: global.render_target_format,
1413                    width,
1414                    height,
1415                    present_mode: wgpu::PresentMode::Fifo,
1416                    alpha_mode: wgpu::CompositeAlphaMode::Opaque,
1417                    view_formats: vec![global.render_target_format],
1418                    desired_maximum_frame_latency: 1,
1419                }
1420            }
1421        };
1422
1423        #[cfg(not(target_arch = "wasm32"))]
1424        {
1425            // Configure with validated config
1426            surface.configure(&global.device, &config);
1427        }
1428
1429        #[cfg(not(target_arch = "wasm32"))]
1430        {
1431            let data = ViewportWgpuData {
1432                surface,
1433                config,
1434                pending_frame: None,
1435            };
1436            vpm.set_renderer_user_data(Box::into_raw(Box::new(data)) as *mut c_void);
1437        }
1438    }
1439
1440    /// Renderer: destroy per-viewport resources
1441    pub unsafe extern "C" fn renderer_destroy_window(vp: *mut Viewport) {
1442        if vp.is_null() {
1443            return;
1444        }
1445        mvlog!("[wgpu-mv] Renderer_DestroyWindow");
1446        if let Some(data) = viewport_user_data_mut(vp) {
1447            // Drop pending frame if any
1448            data.pending_frame.take();
1449            // Free user data box
1450            let _ = Box::from_raw(data as *mut ViewportWgpuData);
1451            let vpm = &mut *vp;
1452            vpm.set_renderer_user_data(std::ptr::null_mut());
1453        }
1454    }
1455
1456    /// Renderer: notify new size
1457    pub unsafe extern "C" fn renderer_set_window_size(
1458        vp: *mut Viewport,
1459        size: dear_imgui_rs::sys::ImVec2,
1460    ) {
1461        if vp.is_null() {
1462            return;
1463        }
1464        mvlog!(
1465            "[wgpu-mv] Renderer_SetWindowSize to ({}, {})",
1466            size.x,
1467            size.y
1468        );
1469        let global = match GLOBAL.get() {
1470            Some(g) => g,
1471            None => return,
1472        };
1473        if let Some(data) = viewport_user_data_mut(vp) {
1474            // Convert ImGui logical size to physical pixels using framebuffer scale
1475            let vpm_ref = &*vp;
1476            let scale = vpm_ref.framebuffer_scale();
1477            let new_w = (size.x * scale[0]).max(1.0) as u32;
1478            let new_h = (size.y * scale[1]).max(1.0) as u32;
1479            if data.config.width != new_w || data.config.height != new_h {
1480                data.config.width = new_w;
1481                data.config.height = new_h;
1482                data.surface.configure(&global.device, &data.config);
1483            }
1484        }
1485    }
1486
1487    /// Renderer: render viewport draw data into its surface
1488    pub unsafe extern "C" fn renderer_render_window(vp: *mut Viewport, _render_arg: *mut c_void) {
1489        if vp.is_null() {
1490            return;
1491        }
1492        mvlog!("[wgpu-mv] Renderer_RenderWindow");
1493        let renderer = match (get_renderer() as *mut WgpuRenderer).as_mut() {
1494            Some(r) => r,
1495            None => return,
1496        };
1497        // Clone device/queue to avoid borrowing renderer during render
1498        let (device, queue) = match renderer.backend_data.as_ref() {
1499            Some(b) => (b.device.clone(), b.queue.clone()),
1500            None => return,
1501        };
1502        let vpm = &mut *vp;
1503        let raw_dd = vpm.draw_data();
1504        if raw_dd.is_null() {
1505            return;
1506        }
1507        // SAFETY: Dear ImGui provides a valid ImDrawData during RenderPlatformWindowsDefault
1508        let draw_data: &dear_imgui_rs::render::DrawData = std::mem::transmute(&*raw_dd);
1509        mvlog!(
1510            "[wgpu-mv] draw_data: valid={} lists={} fb_scale=({:.2},{:.2}) disp=({:.1},{:.1})",
1511            draw_data.valid(),
1512            draw_data.draw_lists_count(),
1513            draw_data.framebuffer_scale()[0],
1514            draw_data.framebuffer_scale()[1],
1515            draw_data.display_size()[0],
1516            draw_data.display_size()[1]
1517        );
1518
1519        mvlog!("[wgpu-mv] retrieving viewport user data");
1520        if let Some(data) = unsafe { viewport_user_data_mut(vp) } {
1521            mvlog!("[wgpu-mv] have viewport user data; acquiring surface frame");
1522            // Acquire frame with basic recovery on Outdated/Lost/Timeout
1523            let frame = match data.surface.get_current_texture() {
1524                Ok(f) => f,
1525                Err(wgpu::SurfaceError::Outdated) | Err(wgpu::SurfaceError::Lost) => {
1526                    // Reconfigure with current window size and retry next frame
1527                    #[cfg(not(target_arch = "wasm32"))]
1528                    {
1529                        let vpm_ref = &*vp;
1530                        let window_ptr = vpm_ref.platform_handle();
1531                        if !window_ptr.is_null() {
1532                            let window: &winit::window::Window = &*(window_ptr as *const _);
1533                            let size = window.inner_size();
1534                            if size.width > 0 && size.height > 0 {
1535                                data.config.width = size.width;
1536                                data.config.height = size.height;
1537                                data.surface.configure(&device, &data.config);
1538                            }
1539                        }
1540                    }
1541                    return;
1542                }
1543                Err(wgpu::SurfaceError::Timeout) => {
1544                    // Skip this frame silently
1545                    return;
1546                }
1547                Err(e) => {
1548                    eprintln!("[wgpu-mv] get_current_texture error: {:?}", e);
1549                    return;
1550                }
1551            };
1552            mvlog!("[wgpu-mv] acquired frame; creating view");
1553            let view = frame
1554                .texture
1555                .create_view(&wgpu::TextureViewDescriptor::default());
1556            // Encode commands and render (catch panics to avoid crashing the whole app)
1557            mvlog!("[wgpu-mv] creating command encoder");
1558            let render_block = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1559                let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1560                    label: Some("dear-imgui-wgpu::viewport-encoder"),
1561                });
1562                mvlog!("[wgpu-mv] begin_render_pass start");
1563                {
1564                    let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1565                        label: Some("dear-imgui-wgpu::viewport-pass"),
1566                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1567                            view: &view,
1568                            resolve_target: None,
1569                            ops: wgpu::Operations {
1570                                load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1571                                store: wgpu::StoreOp::Store,
1572                            },
1573                            depth_slice: None,
1574                        })],
1575                        depth_stencil_attachment: None,
1576                        occlusion_query_set: None,
1577                        timestamp_writes: None,
1578                    });
1579                    mvlog!("[wgpu-mv] begin_render_pass ok");
1580                    mvlog!("[wgpu-mv] about to render_draw_data for viewport");
1581                    // Reuse existing draw path with explicit framebuffer size to avoid scissor mismatch
1582                    let saved_index_opt = renderer.backend_data.as_ref().map(|b| b.frame_index);
1583                    if let Some(vd) = viewport_user_data_mut(vp) {
1584                        let fb_w = vd.config.width;
1585                        let fb_h = vd.config.height;
1586                        if let Err(e) = renderer.render_draw_data_with_fb_size(
1587                            &draw_data,
1588                            &mut render_pass,
1589                            fb_w,
1590                            fb_h,
1591                        ) {
1592                            eprintln!("[wgpu-mv] render_draw_data(with_fb) error: {:?}", e);
1593                        }
1594                    } else if let Err(e) = renderer.render_draw_data(&draw_data, &mut render_pass) {
1595                        eprintln!("[wgpu-mv] render_draw_data error: {:?}", e);
1596                    }
1597                    if let Some(saved_index) = saved_index_opt {
1598                        if let Some(backend) = renderer.backend_data.as_mut() {
1599                            // Only restore to a meaningful value; avoid resetting to u32::MAX
1600                            if saved_index != u32::MAX {
1601                                backend.frame_index = saved_index;
1602                            }
1603                        }
1604                    }
1605                    mvlog!("[wgpu-mv] finished render_draw_data");
1606                }
1607                mvlog!("[wgpu-mv] submitting queue");
1608                queue.submit(std::iter::once(encoder.finish()));
1609                mvlog!("[wgpu-mv] submit ok");
1610            }));
1611            if render_block.is_err() {
1612                eprintln!(
1613                    "[wgpu-mv] panic during viewport render block; skipping present for this viewport"
1614                );
1615                return;
1616            }
1617            data.pending_frame = Some(frame);
1618            mvlog!("[wgpu-mv] submitted and stored pending frame");
1619        }
1620    }
1621
1622    /// Renderer: present frame for viewport surface
1623    pub unsafe extern "C" fn renderer_swap_buffers(vp: *mut Viewport, _render_arg: *mut c_void) {
1624        if vp.is_null() {
1625            return;
1626        }
1627        mvlog!("[wgpu-mv] Renderer_SwapBuffers");
1628        if let Some(data) = viewport_user_data_mut(vp) {
1629            if let Some(frame) = data.pending_frame.take() {
1630                frame.present();
1631            }
1632        }
1633    }
1634
1635    // Raw sys-platform wrappers to avoid typed trampolines
1636    pub unsafe extern "C" fn platform_render_window_sys(
1637        vp: *mut dear_imgui_rs::sys::ImGuiViewport,
1638        arg: *mut c_void,
1639    ) {
1640        if vp.is_null() {
1641            return;
1642        }
1643        renderer_render_window(vp as *mut Viewport, arg);
1644    }
1645
1646    pub unsafe extern "C" fn platform_swap_buffers_sys(
1647        vp: *mut dear_imgui_rs::sys::ImGuiViewport,
1648        arg: *mut c_void,
1649    ) {
1650        if vp.is_null() {
1651            return;
1652        }
1653        renderer_swap_buffers(vp as *mut Viewport, arg);
1654    }
1655}