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