Skip to main content

dear_imgui_wgpu/renderer/
init.rs

1use super::{
2    WgpuRenderer,
3    callbacks::{
4        draw_callback_reset_render_state, draw_callback_set_sampler_linear,
5        draw_callback_set_sampler_nearest,
6    },
7};
8use crate::wgpu;
9use crate::{
10    GammaMode, RendererError, RendererResult, ShaderManager, WgpuBackendData, WgpuInitInfo,
11    WgpuTextureManager,
12};
13use dear_imgui_rs::{BackendFlags, Context};
14use wgpu::*;
15
16impl WgpuRenderer {
17    /// Create a WGPU renderer bound to one Dear ImGui context (recommended)
18    ///
19    /// This is the preferred way to create a WGPU renderer as it ensures proper
20    /// initialization order and is consistent with other backends.
21    ///
22    /// # Arguments
23    /// * `init_info` - WGPU initialization information (device, queue, format)
24    /// * `imgui_ctx` - Dear ImGui context to configure
25    ///
26    /// # Example
27    /// ```rust,no_run
28    /// use dear_imgui_rs::Context;
29    /// use dear_imgui_wgpu::{WgpuRenderer, WgpuInitInfo, wgpu};
30    ///
31    /// # fn main() -> Result<(), dear_imgui_wgpu::RendererError> {
32    /// # let (device, queue) = todo!("initialize a WGPU Device/Queue");
33    /// # let surface_format = wgpu::TextureFormat::Bgra8UnormSrgb;
34    /// # let mut imgui_context = Context::create();
35    /// let init_info = WgpuInitInfo::new(device, queue, surface_format);
36    /// let mut renderer = WgpuRenderer::new(init_info, &mut imgui_context)?;
37    /// # Ok(()) }
38    /// ```
39    pub fn new(init_info: WgpuInitInfo, imgui_ctx: &mut Context) -> RendererResult<Self> {
40        let mut renderer = Self::empty();
41        renderer.initialize_for_context(init_info, imgui_ctx)?;
42        Ok(renderer)
43    }
44
45    pub(super) fn empty() -> Self {
46        Self {
47            context_state: None,
48            backend_data: None,
49            shader_manager: ShaderManager::new(),
50            texture_manager: WgpuTextureManager::new(),
51            default_texture: None,
52            gamma_mode: GammaMode::Auto,
53            #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
54            viewport_clear_color: Color::BLACK,
55            renderer_consumer: None,
56            drop_deferral: None,
57        }
58    }
59
60    /// Initialize renderer-owned GPU state.
61    ///
62    /// Initialization is private so renderer resources and Dear ImGui bindings cannot be replaced
63    /// independently.
64    fn initialize_device(&mut self, init_info: WgpuInitInfo) -> RendererResult<()> {
65        self.ensure_uninitialized()?;
66
67        // Create backend data
68        let mut backend_data = WgpuBackendData::new(init_info);
69
70        // Preflight: ensure the render target format is render-attachable and blendable.
71        // The ImGui pipeline always uses alpha blending; non-blendable formats will
72        // fail validation later with less actionable errors.
73        let fmt = backend_data.render_target_format;
74        if let Some(adapter) = backend_data.init_info.adapter.as_ref() {
75            let fmt_features = adapter.get_texture_format_features(fmt);
76            if !fmt_features
77                .allowed_usages
78                .contains(wgpu::TextureUsages::RENDER_ATTACHMENT)
79                || !fmt_features
80                    .flags
81                    .contains(wgpu::TextureFormatFeatureFlags::BLENDABLE)
82            {
83                return Err(RendererError::InvalidRenderState(format!(
84                    "Render target format {:?} is not suitable for ImGui WGPU renderer (requires RENDER_ATTACHMENT + BLENDABLE). allowed_usages={:?} flags={:?}",
85                    fmt, fmt_features.allowed_usages, fmt_features.flags
86                )));
87            }
88        }
89
90        if let Err(error) = self.create_device_objects(&mut backend_data) {
91            self.shader_manager = ShaderManager::new();
92            self.default_texture = None;
93            return Err(error);
94        }
95
96        self.backend_data = Some(backend_data);
97        Ok(())
98    }
99
100    fn ensure_uninitialized(&self) -> RendererResult<()> {
101        if self.backend_data.is_some()
102            || self.context_state.is_some()
103            || self.renderer_consumer.is_some()
104        {
105            return Err(RendererError::InvalidRenderState(
106                "renderer is already initialized; call shutdown() with its ImGui context before reinitializing"
107                    .to_owned(),
108            ));
109        }
110        Ok(())
111    }
112
113    fn initialize_for_context(
114        &mut self,
115        init_info: WgpuInitInfo,
116        imgui_ctx: &mut Context,
117    ) -> RendererResult<()> {
118        self.ensure_uninitialized()?;
119        Self::ensure_context_available(imgui_ctx)?;
120        self.initialize_device(init_info)?;
121
122        if let Err(error) = self.attach_context(imgui_ctx) {
123            self.invalidate_device_objects_only();
124            self.backend_data = None;
125            return Err(error);
126        }
127
128        Ok(())
129    }
130
131    fn attach_context(&mut self, imgui_ctx: &mut Context) -> RendererResult<()> {
132        let (renderer_flags_added, renderer_name_ptr) = Self::configure_imgui_context(imgui_ctx)?;
133        if let Err(error) = self.bind_context(imgui_ctx, renderer_flags_added) {
134            Self::clear_unbound_imgui_context(imgui_ctx, renderer_flags_added, renderer_name_ptr);
135            return Err(error);
136        }
137        let consumer = match imgui_ctx.create_synchronous_renderer_consumer() {
138            Ok(consumer) => consumer,
139            Err(error) => {
140                self.clear_bound_imgui_context(imgui_ctx);
141                return Err(error.into());
142            }
143        };
144        self.renderer_consumer = Some(consumer);
145
146        // A newly attached renderer cannot inherit GPU bindings from a previous renderer/device.
147        // There is no local map yet, but use the same explicit permit/commit transaction as every
148        // destructive path so the Context validates the consumer generation before publication
149        // changes.
150        let consumer = self
151            .renderer_consumer
152            .take()
153            .ok_or(RendererError::ContextNotBound)?;
154        let reset = match imgui_ctx.prepare_renderer_texture_reset(&consumer) {
155            Ok(reset) => reset,
156            Err(error) => {
157                drop(consumer);
158                self.clear_bound_imgui_context(imgui_ctx);
159                return Err(error.into());
160            }
161        };
162        reset.commit();
163        self.texture_manager.clear_destroyed_managed_textures();
164        self.renderer_consumer = Some(consumer);
165
166        Ok(())
167    }
168
169    fn ensure_context_available(imgui_context: &Context) -> RendererResult<()> {
170        if !imgui_context.io().backend_renderer_user_data().is_null() {
171            return Err(RendererError::ContextAlreadyHasRenderer);
172        }
173        if imgui_context.io().backend_renderer_name().is_some() {
174            return Err(RendererError::ContextAlreadyHasRenderer);
175        }
176        let reserved_flags = BackendFlags::RENDERER_HAS_VTX_OFFSET
177            | BackendFlags::RENDERER_HAS_TEXTURES
178            | BackendFlags::from_bits_retain(
179                dear_imgui_rs::sys::ImGuiBackendFlags_RendererHasViewports,
180            );
181        if !(imgui_context.io().backend_flags() & reserved_flags).is_empty() {
182            return Err(RendererError::ContextAlreadyHasRenderer);
183        }
184
185        let platform_io = imgui_context.platform_io();
186        // SAFETY: PlatformIO belongs to this Context and is immutably borrowed for the check.
187        let raw = unsafe { &*platform_io.as_raw() };
188        if unsafe { !platform_io.renderer_render_state().is_null() }
189            || raw.Renderer_TextureMaxWidth != 0
190            || raw.Renderer_TextureMaxHeight != 0
191            || raw.Renderer_CreateWindow.is_some()
192            || raw.Renderer_DestroyWindow.is_some()
193            || raw.Renderer_SetWindowSize.is_some()
194            || raw.Renderer_RenderWindow.is_some()
195            || raw.Renderer_SwapBuffers.is_some()
196        {
197            return Err(RendererError::ContextAlreadyHasRenderer);
198        }
199        let draw_callbacks_occupied = platform_io.draw_callback_reset_render_state_raw().is_some()
200            || platform_io.draw_callback_set_sampler_linear_raw().is_some()
201            || platform_io
202                .draw_callback_set_sampler_nearest_raw()
203                .is_some();
204
205        if imgui_context.io().backend_renderer_name().is_some() || draw_callbacks_occupied {
206            Err(RendererError::ContextAlreadyHasRenderer)
207        } else {
208            Ok(())
209        }
210    }
211
212    /// Set gamma mode
213    pub fn set_gamma_mode(&mut self, mode: GammaMode) {
214        self.gamma_mode = mode;
215    }
216
217    /// Set clear color for secondary viewports (multi-viewport mode).
218    ///
219    /// This color is used as the load/clear color when rendering ImGui-created
220    /// platform windows via `RenderPlatformWindowsDefault`. It is independent
221    /// from whatever clear color your main swapchain uses.
222    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
223    pub fn set_viewport_clear_color(&mut self, color: Color) {
224        self.viewport_clear_color = color;
225    }
226
227    /// Get current clear color for secondary viewports.
228    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
229    pub fn viewport_clear_color(&self) -> Color {
230        self.viewport_clear_color
231    }
232
233    pub(super) fn configure_imgui_context(
234        imgui_context: &mut Context,
235    ) -> RendererResult<(BackendFlags, *const std::ffi::c_char)> {
236        // Keep this function transactional even when called directly by an internal recovery
237        // path: no renderer field may be overwritten after the preflight has passed.
238        Self::ensure_context_available(imgui_context)?;
239        imgui_context
240            .set_renderer_name(Some(format!(
241                "dear-imgui-wgpu {}",
242                env!("CARGO_PKG_VERSION")
243            )))
244            .map_err(|error| {
245                RendererError::InvalidRenderState(format!(
246                    "failed to configure Dear ImGui renderer name: {error}"
247                ))
248            })?;
249        let renderer_name_ptr = imgui_context
250            .io()
251            .backend_renderer_name()
252            .expect("WGPU just published BackendRendererName")
253            .as_ptr();
254
255        let io = imgui_context.io_mut();
256        let previous_flags = io.backend_flags();
257        let renderer_flags =
258            BackendFlags::RENDERER_HAS_VTX_OFFSET | BackendFlags::RENDERER_HAS_TEXTURES;
259
260        // Set WGPU renderer capabilities
261        // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes.
262        // We can also honor ImGuiPlatformIO::Textures[] requests during render.
263        io.set_backend_flags(previous_flags | renderer_flags);
264
265        let platform_io = imgui_context.platform_io_mut();
266        // SAFETY: these static callbacks use Dear ImGui's exact draw-callback ABI and stay valid
267        // for the renderer lifetime.
268        unsafe {
269            platform_io
270                .set_draw_callback_reset_render_state_raw(Some(draw_callback_reset_render_state));
271            platform_io
272                .set_draw_callback_set_sampler_linear_raw(Some(draw_callback_set_sampler_linear));
273            platform_io
274                .set_draw_callback_set_sampler_nearest_raw(Some(draw_callback_set_sampler_nearest));
275        }
276
277        Ok((renderer_flags & !previous_flags, renderer_name_ptr))
278    }
279
280    fn clear_unbound_imgui_context(
281        imgui_context: &mut Context,
282        renderer_flags_added: BackendFlags,
283        renderer_name_ptr: *const std::ffi::c_char,
284    ) {
285        let owned_name = imgui_context
286            .io()
287            .backend_renderer_name()
288            .is_some_and(|name| name.as_ptr() == renderer_name_ptr);
289        if owned_name {
290            imgui_context
291                .set_renderer_name::<String>(None)
292                .expect("clearing WGPU BackendRendererName must not fail");
293        }
294        let io = imgui_context.io_mut();
295        io.set_backend_flags(io.backend_flags() & !renderer_flags_added);
296        Self::clear_owned_draw_callbacks(imgui_context.platform_io_mut());
297    }
298
299    pub(super) fn clear_bound_imgui_context(&mut self, imgui_context: &mut Context) {
300        if let Some(state) = self.context_state.as_ref() {
301            state.clear_with_context(imgui_context);
302        }
303        self.clear_context_state();
304    }
305
306    /// Recreate every resource discarded by `invalidate_device_objects()`.
307    pub(super) fn create_device_objects(
308        &mut self,
309        backend_data: &mut WgpuBackendData,
310    ) -> RendererResult<()> {
311        backend_data
312            .render_resources
313            .initialize(&backend_data.device)?;
314        self.shader_manager.initialize(&backend_data.device)?;
315        self.default_texture =
316            Some(self.create_default_texture(&backend_data.device, &backend_data.queue)?);
317        self.create_render_pipeline(backend_data)
318    }
319
320    /// Create a default 1x1 white texture
321    fn create_default_texture(
322        &self,
323        device: &Device,
324        queue: &Queue,
325    ) -> RendererResult<TextureView> {
326        let texture = device.create_texture(&TextureDescriptor {
327            label: Some("Dear ImGui Default Texture"),
328            size: Extent3d {
329                width: 1,
330                height: 1,
331                depth_or_array_layers: 1,
332            },
333            mip_level_count: 1,
334            sample_count: 1,
335            dimension: TextureDimension::D2,
336            format: TextureFormat::Rgba8Unorm,
337            usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
338            view_formats: &[],
339        });
340
341        // Upload white pixel
342        queue.write_texture(
343            wgpu::TexelCopyTextureInfo {
344                texture: &texture,
345                mip_level: 0,
346                origin: wgpu::Origin3d::ZERO,
347                aspect: wgpu::TextureAspect::All,
348            },
349            &[255u8, 255u8, 255u8, 255u8], // RGBA white
350            wgpu::TexelCopyBufferLayout {
351                offset: 0,
352                bytes_per_row: Some(4),
353                rows_per_image: Some(1),
354            },
355            Extent3d {
356                width: 1,
357                height: 1,
358                depth_or_array_layers: 1,
359            },
360        );
361
362        Ok(texture.create_view(&TextureViewDescriptor::default()))
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn context_preflight_rejects_an_existing_renderer_name() {
372        let mut context = Context::create();
373        context
374            .set_renderer_name(Some("foreign-renderer"))
375            .expect("test renderer name should be valid");
376
377        assert!(matches!(
378            WgpuRenderer::ensure_context_available(&context),
379            Err(RendererError::ContextAlreadyHasRenderer)
380        ));
381    }
382
383    #[test]
384    fn context_preflight_rejects_reserved_renderer_flags() {
385        let mut context = Context::create();
386        context
387            .io_mut()
388            .set_backend_flags(BackendFlags::RENDERER_HAS_TEXTURES);
389
390        assert!(matches!(
391            WgpuRenderer::ensure_context_available(&context),
392            Err(RendererError::ContextAlreadyHasRenderer)
393        ));
394        context.io_mut().set_backend_flags(BackendFlags::empty());
395    }
396
397    #[test]
398    fn context_preflight_always_rejects_the_viewport_renderer_capability() {
399        let context = Context::create();
400        let viewport_flag = BackendFlags::from_bits_retain(
401            dear_imgui_rs::sys::ImGuiBackendFlags_RendererHasViewports,
402        );
403        let raw_io = unsafe { dear_imgui_rs::sys::igGetIO_ContextPtr(context.as_raw()) };
404        unsafe { (*raw_io).BackendFlags = viewport_flag.bits() };
405        let flags_before = context.io().backend_flags();
406
407        assert!(matches!(
408            WgpuRenderer::ensure_context_available(&context),
409            Err(RendererError::ContextAlreadyHasRenderer)
410        ));
411        assert_eq!(context.io().backend_flags(), flags_before);
412        assert!(context.io().backend_renderer_user_data().is_null());
413        assert!(context.io().backend_renderer_name().is_none());
414
415        unsafe { (*raw_io).BackendFlags = 0 };
416    }
417
418    #[test]
419    fn unconfigure_removes_the_exclusive_wgpu_claim() {
420        let mut context = Context::create();
421        let (added, _) = WgpuRenderer::configure_imgui_context(&mut context)
422            .expect("fresh context should accept WGPU renderer state");
423        assert_eq!(
424            added,
425            BackendFlags::RENDERER_HAS_VTX_OFFSET | BackendFlags::RENDERER_HAS_TEXTURES
426        );
427        let mut renderer = WgpuRenderer::empty();
428        renderer
429            .bind_context(&mut context, added)
430            .expect("configured Context should bind once");
431
432        renderer.clear_bound_imgui_context(&mut context);
433        assert_eq!(context.io().backend_flags(), BackendFlags::empty());
434        assert!(context.io().backend_renderer_user_data().is_null());
435        assert!(context.io().backend_renderer_name().is_none());
436        assert!(
437            context
438                .platform_io()
439                .draw_callback_reset_render_state_raw()
440                .is_none()
441        );
442    }
443}