Skip to main content

dear_imgui_wgpu/renderer/
core.rs

1use crate::{
2    GammaMode, RendererError, RendererResult, ShaderManager, WgpuBackendData, WgpuTextureManager,
3};
4use dear_imgui_rs::{
5    BackendFlags, Context, ContextBinding,
6    render::{PendingFrame, ReconciledFrame, SynchronousRendererConsumer},
7    sys,
8};
9use std::{
10    cell::Cell,
11    ffi::{c_char, c_void},
12};
13use wgpu::TextureView;
14
15#[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
16use wgpu::Color;
17
18/// Stable, non-zero-sized marker published through `BackendRendererUserData`.
19#[derive(Debug)]
20struct RendererBackendToken {
21    _marker: u8,
22}
23
24/// Pointer-identity publication data shared with the Context-owned deferred-drop attachment.
25///
26/// The boxed token remains owned by [`RendererContextState`], while this copy is sufficient to
27/// clear only exact raw publications during Context teardown. It never dereferences `token_ptr`.
28#[derive(Clone, Debug)]
29pub(super) struct RendererPublication {
30    context: ContextBinding,
31    token_ptr: *mut c_void,
32    renderer_name_ptr: *const c_char,
33    renderer_flags_added: BackendFlags,
34}
35
36impl RendererPublication {
37    pub(super) fn context(&self) -> ContextBinding {
38        self.context.clone()
39    }
40
41    /// Clears only raw fields that still carry this renderer's exact identity.
42    ///
43    /// Returns whether the Context-owned renderer name pointer was still ours.
44    pub(super) unsafe fn clear_owned_raw_state_bound(&self) -> bool {
45        let io = unsafe { sys::igGetIO_Nil() };
46        if io.is_null() {
47            return false;
48        }
49        // SAFETY: the owner Context is bound and its native state is still alive.
50        let io = unsafe { &mut *io };
51        let owned_name =
52            !self.renderer_name_ptr.is_null() && io.BackendRendererName == self.renderer_name_ptr;
53        let owned_token = io.BackendRendererUserData == self.token_ptr;
54
55        let platform_io = unsafe { sys::igGetPlatformIO_Nil() };
56        let owns_draw_callback = if platform_io.is_null() {
57            false
58        } else {
59            // SAFETY: PlatformIO belongs to the bound owner Context.
60            WgpuRenderer::owns_any_standard_draw_callback(unsafe {
61                dear_imgui_rs::platform_io::PlatformIo::from_raw(platform_io)
62            })
63        };
64
65        if owned_name {
66            io.BackendRendererName = std::ptr::null();
67        }
68        if owned_token {
69            io.BackendRendererUserData = std::ptr::null_mut();
70        }
71
72        // Capability bits have no independent identity. Revoke them only while at least one
73        // core publication still proves this renderer owns the lease; a complete foreign
74        // takeover must retain the foreign backend's advertised capabilities.
75        if owned_name || owned_token || owns_draw_callback {
76            io.BackendFlags &= !self.renderer_flags_added.bits();
77        }
78
79        if !platform_io.is_null() {
80            WgpuRenderer::clear_owned_draw_callbacks(unsafe {
81                dear_imgui_rs::platform_io::PlatformIo::from_raw_mut(platform_io)
82            });
83        }
84        owned_name
85    }
86
87    /// Returns whether the bound Context still contains at least one publication owned by this
88    /// renderer. Capability bits deliberately do not participate because they may already
89    /// describe a complete foreign renderer takeover.
90    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
91    pub(super) unsafe fn owns_any_raw_publication_bound(&self) -> bool {
92        let io = unsafe { sys::igGetIO_Nil() };
93        if io.is_null() {
94            return false;
95        }
96        // SAFETY: the caller has bound this state owner's live Context.
97        let io = unsafe { &*io };
98        if (!self.renderer_name_ptr.is_null() && io.BackendRendererName == self.renderer_name_ptr)
99            || io.BackendRendererUserData == self.token_ptr
100        {
101            return true;
102        }
103
104        let platform_io = unsafe { sys::igGetPlatformIO_Nil() };
105        !platform_io.is_null()
106            // SAFETY: PlatformIO belongs to the currently bound owner Context.
107            && WgpuRenderer::owns_any_standard_draw_callback(unsafe {
108                dear_imgui_rs::platform_io::PlatformIo::from_raw(platform_io)
109            })
110    }
111}
112
113/// Exclusive ownership of renderer fields published into one Dear ImGui Context.
114///
115/// This value is intentionally not cloneable. Its boxed token and Context-owned renderer name
116/// retain stable addresses when `WgpuRenderer` moves, while one owner remains responsible for
117/// clearing the raw pointers before the token is released.
118#[derive(Debug)]
119pub(super) struct RendererContextState {
120    publication: RendererPublication,
121    token: Box<RendererBackendToken>,
122    fault: Cell<Option<&'static str>>,
123}
124
125impl RendererContextState {
126    pub(super) fn publish(
127        context: &mut Context,
128        renderer_flags_added: BackendFlags,
129    ) -> RendererResult<Self> {
130        let renderer_name_ptr = context
131            .io()
132            .backend_renderer_name()
133            .ok_or_else(|| {
134                RendererError::InvalidRenderState(
135                    "WGPU renderer name was not published before binding".to_owned(),
136                )
137            })?
138            .as_ptr();
139        if !context.io().backend_renderer_user_data().is_null() {
140            return Err(RendererError::ContextAlreadyHasRenderer);
141        }
142
143        let token = Box::new(RendererBackendToken { _marker: 0 });
144        let publication = RendererPublication {
145            context: context.binding(),
146            token_ptr: std::ptr::from_ref(token.as_ref()).cast_mut().cast(),
147            renderer_name_ptr,
148            renderer_flags_added,
149        };
150        let state = Self {
151            publication,
152            token,
153            fault: Cell::new(None),
154        };
155        // SAFETY: the boxed token has a stable address and remains alive until this state clears
156        // the raw pointer or until the native Context has already been destroyed.
157        unsafe {
158            context
159                .io_mut()
160                .set_backend_renderer_user_data(state.publication.token_ptr);
161        }
162        Ok(state)
163    }
164
165    fn ensure_alive(&self) -> RendererResult<()> {
166        if self.publication.context.is_alive() {
167            Ok(())
168        } else {
169            Err(RendererError::ContextDropped)
170        }
171    }
172
173    pub(super) fn ensure_matches(&self, context: &Context) -> RendererResult<()> {
174        self.ensure_alive()?;
175        if self.publication.context.id() == context.id() {
176            Ok(())
177        } else {
178            Err(RendererError::ContextMismatch)
179        }
180    }
181
182    pub(super) fn ensure_renderer_contract(&self) -> RendererResult<()> {
183        self.ensure_alive()?;
184        if let Some(field) = self.fault.get() {
185            let _ = self.publication.context.try_with_bound_context(|| unsafe {
186                self.clear_owned_raw_state_bound();
187            });
188            return Err(RendererError::RendererStateDrift { field });
189        }
190
191        let fault = self
192            .publication
193            .context
194            .try_with_bound_context(|| {
195                let fault = self.current_renderer_state_fault_bound();
196                if fault.is_some() {
197                    // SAFETY: the owner Context is bound for this validation and cleanup.
198                    unsafe { self.clear_owned_raw_state_bound() };
199                }
200                fault
201            })
202            .map_err(|_| RendererError::ContextDropped)?;
203        if let Some(field) = fault {
204            self.fault.set(Some(field));
205            Err(RendererError::RendererStateDrift { field })
206        } else {
207            Ok(())
208        }
209    }
210
211    pub(super) fn context(&self) -> ContextBinding {
212        self.publication.context()
213    }
214
215    pub(super) fn publication(&self) -> RendererPublication {
216        debug_assert_eq!(self.publication.token_ptr, self.token_ptr());
217        self.publication.clone()
218    }
219
220    pub(super) fn clear_with_context(&self, context: &mut Context) {
221        // SAFETY: callers have already verified the matching live Context.
222        let owned_name = unsafe { self.publication.clear_owned_raw_state_bound() };
223        if owned_name {
224            context
225                .set_renderer_name::<String>(None)
226                .expect("clearing WGPU BackendRendererName must not fail");
227        }
228    }
229
230    pub(super) unsafe fn clear_owned_raw_state_bound(&self) -> bool {
231        unsafe { self.publication.clear_owned_raw_state_bound() }
232    }
233
234    /// Returns whether the bound Context still contains at least one publication owned by this
235    /// renderer.
236    ///
237    /// Capability bits deliberately do not participate: they have no per-renderer identity and
238    /// may already describe a complete foreign renderer takeover.
239    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
240    pub(super) unsafe fn owns_any_raw_publication_bound(&self) -> bool {
241        unsafe { self.publication.owns_any_raw_publication_bound() }
242    }
243
244    fn current_renderer_state_fault_bound(&self) -> Option<&'static str> {
245        let io = unsafe { sys::igGetIO_Nil() };
246        if io.is_null() {
247            return Some("ImGuiIO");
248        }
249        // SAFETY: the caller has bound the renderer's Context for the whole check.
250        let io = unsafe { &*io };
251        if io.BackendRendererUserData != self.publication.token_ptr {
252            return Some("BackendRendererUserData");
253        }
254        if io.BackendRendererName != self.publication.renderer_name_ptr {
255            return Some("BackendRendererName");
256        }
257        let flags = BackendFlags::from_bits_retain(io.BackendFlags);
258        for (present, field) in [
259            (
260                flags.contains(BackendFlags::RENDERER_HAS_VTX_OFFSET),
261                "RENDERER_HAS_VTX_OFFSET",
262            ),
263            (
264                flags.contains(BackendFlags::RENDERER_HAS_TEXTURES),
265                "RENDERER_HAS_TEXTURES",
266            ),
267        ] {
268            if !present {
269                return Some(field);
270            }
271        }
272
273        let platform_io = unsafe { sys::igGetPlatformIO_Nil() };
274        if platform_io.is_null() {
275            return Some("PlatformIO");
276        }
277        // SAFETY: PlatformIO belongs to the currently bound Context.
278        let platform_io = unsafe { dear_imgui_rs::platform_io::PlatformIo::from_raw(platform_io) };
279        if !unsafe { platform_io.renderer_render_state() }.is_null() {
280            return Some("Renderer_RenderState");
281        }
282        // WGPU publishes no texture limit. A non-zero value is therefore foreign state.
283        let raw = unsafe { &*platform_io.as_raw() };
284        if raw.Renderer_TextureMaxWidth != 0 {
285            return Some("Renderer_TextureMaxWidth");
286        }
287        if raw.Renderer_TextureMaxHeight != 0 {
288            return Some("Renderer_TextureMaxHeight");
289        }
290        if !WgpuRenderer::owned_draw_callbacks_match(platform_io) {
291            return Some("DrawCallback_*");
292        }
293        None
294    }
295
296    fn token_ptr(&self) -> *mut c_void {
297        std::ptr::from_ref(self.token.as_ref()).cast_mut().cast()
298    }
299}
300
301impl Drop for RendererContextState {
302    fn drop(&mut self) {
303        // Drop runs before `token`, so a live Context cannot retain a pointer to freed storage.
304        let _ = self.publication.context.try_with_bound_context(|| unsafe {
305            self.clear_owned_raw_state_bound();
306        });
307    }
308}
309
310/// Main WGPU renderer for Dear ImGui
311///
312/// This corresponds to the main renderer functionality in imgui_impl_wgpu.cpp
313///
314/// An initialized renderer owns the renderer state of exactly one [`Context`]. Create a separate
315/// renderer for every Dear ImGui context. Call [`Self::shutdown`](WgpuRenderer::shutdown) with
316/// the matching context when GPU resources must be released before Context teardown or before
317/// that live Context can accept a replacement renderer. Dropping a bound renderer without that
318/// call is safe but defers its renderer resources and consumer to Context teardown, so it does
319/// not make the Context available for reuse. The retained context binding makes this renderer
320/// UI-thread-bound.
321pub struct WgpuRenderer {
322    /// Dear ImGui context whose renderer state this instance owns.
323    pub(super) context_state: Option<RendererContextState>,
324    /// Backend data
325    pub(super) backend_data: Option<WgpuBackendData>,
326    /// Shader manager
327    pub(super) shader_manager: ShaderManager,
328    /// Texture manager
329    pub(super) texture_manager: WgpuTextureManager,
330    /// Default texture for fallback
331    pub(super) default_texture: Option<TextureView>,
332    /// Gamma mode: automatic (by format), force linear (1.0), or force 2.2
333    pub(super) gamma_mode: GammaMode,
334    /// Clear color used for secondary viewports (multi-viewport mode)
335    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
336    pub(super) viewport_clear_color: Color,
337    /// Sole managed-texture consumer generation owned by this renderer.
338    pub(super) renderer_consumer: Option<SynchronousRendererConsumer>,
339    /// Context-owned fallback for a renderer wrapper dropped without explicit shutdown.
340    pub(super) drop_deferral: Option<super::lifecycle::RendererDropDeferral>,
341}
342
343impl WgpuRenderer {
344    pub(super) fn bind_context(
345        &mut self,
346        context: &mut Context,
347        renderer_flags_added: BackendFlags,
348    ) -> RendererResult<()> {
349        if self.context_state.is_some() {
350            return Err(RendererError::InvalidRenderState(
351                "renderer is already bound to a Dear ImGui context".to_owned(),
352            ));
353        }
354        let drop_deferral = super::lifecycle::RendererDropDeferral::register(context)?;
355        let state = match RendererContextState::publish(context, renderer_flags_added) {
356            Ok(state) => state,
357            Err(error) => {
358                drop(drop_deferral);
359                return Err(error);
360            }
361        };
362        drop_deferral.set_publication(state.publication());
363        self.context_state = Some(state);
364        self.drop_deferral = Some(drop_deferral);
365        Ok(())
366    }
367
368    pub(super) fn ensure_context_alive(&self) -> RendererResult<()> {
369        self.context_state
370            .as_ref()
371            .ok_or(RendererError::ContextNotBound)?
372            .ensure_alive()
373    }
374
375    pub(super) fn ensure_renderer_contract(&self) -> RendererResult<()> {
376        self.context_state
377            .as_ref()
378            .ok_or(RendererError::ContextNotBound)?
379            .ensure_renderer_contract()
380    }
381
382    /// Reports whether the currently bound Context still exposes an exact core WGPU
383    /// publication. Callers use this before revoking untagged renderer capability bits.
384    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
385    pub(super) fn owns_context_publication_bound(&self) -> bool {
386        self.context_state.as_ref().is_some_and(|state| {
387            // SAFETY: callers invoke this only while the renderer's Context is bound.
388            unsafe { state.owns_any_raw_publication_bound() }
389        })
390    }
391
392    pub(super) fn ensure_context_matches(&self, context: &Context) -> RendererResult<()> {
393        self.context_state
394            .as_ref()
395            .ok_or(RendererError::ContextNotBound)?
396            .ensure_matches(context)
397    }
398
399    pub(super) fn bound_context(&self) -> RendererResult<ContextBinding> {
400        Ok(self
401            .context_state
402            .as_ref()
403            .ok_or(RendererError::ContextNotBound)?
404            .context())
405    }
406
407    pub(super) fn clear_context_state(&mut self) {
408        self.context_state = None;
409        self.drop_deferral = None;
410    }
411
412    /// Returns the synchronous consumer capability owned by this renderer.
413    ///
414    /// Use it with [`Context::render`] when application code needs to reconcile a frame before
415    /// platform-window callbacks or other renderer-managed work.
416    pub fn renderer_consumer(&self) -> RendererResult<&SynchronousRendererConsumer> {
417        self.renderer_consumer
418            .as_ref()
419            .ok_or(RendererError::ContextNotBound)
420    }
421
422    pub(super) fn ensure_pending_frame_matches(
423        &self,
424        frame: &PendingFrame<'_>,
425    ) -> RendererResult<()> {
426        let consumer = self.renderer_consumer()?;
427        if frame.context_id() != consumer.context_id() {
428            return Err(RendererError::ContextMismatch);
429        }
430        let epoch = frame.epoch();
431        if epoch.consumer_generation() != consumer.generation() {
432            return Err(RendererError::InvalidRenderState(format!(
433                "pending frame uses consumer generation {}, WGPU owns generation {}",
434                epoch.consumer_generation(),
435                consumer.generation()
436            )));
437        }
438        Ok(())
439    }
440
441    pub(super) fn ensure_reconciled_frame_matches(
442        &self,
443        frame: &ReconciledFrame<'_>,
444    ) -> RendererResult<()> {
445        let consumer = self.renderer_consumer()?;
446        if frame.context_id() != consumer.context_id() {
447            return Err(RendererError::ContextMismatch);
448        }
449        let epoch = frame.epoch().ok_or_else(|| {
450            RendererError::InvalidRenderState(
451                "WGPU requires a managed-texture renderer epoch".to_owned(),
452            )
453        })?;
454        if epoch.consumer_generation() != consumer.generation() {
455            return Err(RendererError::InvalidRenderState(format!(
456                "reconciled frame uses consumer generation {}, WGPU owns generation {}",
457                epoch.consumer_generation(),
458                consumer.generation()
459            )));
460        }
461        Ok(())
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn dropped_owner_is_not_confused_with_a_reused_context_address() {
471        let owner = Context::create();
472        let owner_id = owner.id();
473        let binding = owner.binding();
474        drop(owner);
475
476        let replacement = Context::create();
477        assert!(!binding.is_alive());
478        assert_ne!(owner_id, replacement.id());
479    }
480}