Skip to main content

dear_imgui_wgpu/renderer/
lifecycle.rs

1use std::{
2    cell::{Cell, RefCell},
3    rc::Rc,
4};
5
6use super::{
7    WgpuRenderer,
8    callbacks::{
9        draw_callback_matches, draw_callback_reset_render_state, draw_callback_set_sampler_linear,
10        draw_callback_set_sampler_nearest,
11    },
12    core::{RendererContextState, RendererPublication},
13};
14use crate::{FrameResourceArena, RenderResources, RendererError, RendererResult, ShaderManager};
15use dear_imgui_rs::{
16    Context, ContextAttachment, ContextAttachmentError, ContextAttachmentLease,
17    ContextAttachmentRole, ContextAttachmentTeardownError, ContextBindingError, ContextDestroyed,
18    ContextTeardown, render::SynchronousRendererConsumer,
19};
20
21struct WgpuRendererDropAttachmentMarker;
22
23/// GPU and consumer state transferred out of a renderer wrapper that was dropped without an
24/// explicit `shutdown`. The Context attachment releases GPU resources in its terminal renderer
25/// phase; a Context that is already dropping retains the raw-publication token until native
26/// destruction has made it inert.
27struct DeferredRendererResources {
28    context_state: Option<RendererContextState>,
29    backend_data: Option<crate::WgpuBackendData>,
30    shader_manager: Option<ShaderManager>,
31    texture_manager: Option<crate::WgpuTextureManager>,
32    default_texture: Option<wgpu::TextureView>,
33    renderer_consumer: Option<SynchronousRendererConsumer>,
34}
35
36impl DeferredRendererResources {
37    fn release_renderer_resources(
38        &mut self,
39        context: &ContextTeardown<'_>,
40    ) -> Result<(), ContextAttachmentTeardownError> {
41        let consumer = self.renderer_consumer.take().ok_or_else(|| {
42            ContextAttachmentTeardownError::new(
43                "deferred WGPU renderer resources lost their renderer consumer",
44            )
45        })?;
46
47        // The Context's renderer-resource phase is the only Drop fallback that can perform the
48        // same prepare -> release -> commit transaction as explicit shutdown. If preflight
49        // rejects an outstanding epoch, the closure does not run and the consumer is restored so
50        // no renderer state is silently released out of order.
51        let result = context.with_renderer_texture_reset(&consumer, || {
52            self.backend_data.take();
53            self.shader_manager.take();
54            self.texture_manager.take();
55            self.default_texture.take();
56            Ok(())
57        });
58        if result.is_err() {
59            self.renderer_consumer = Some(consumer);
60        }
61        result
62    }
63
64    fn release_after_context_destroyed(mut self) {
65        // Reading and dropping the retained token explicitly documents why it survives the
66        // renderer-resource phase when wrapper Drop races Context teardown.
67        drop(self.context_state.take());
68    }
69}
70
71struct RendererDropControl {
72    deferred: RefCell<Option<DeferredRendererResources>>,
73    publication: RefCell<Option<RendererPublication>>,
74    context_destroyed: Cell<bool>,
75    #[cfg(test)]
76    renderer_release_count: Cell<u32>,
77}
78
79struct RendererDropAttachment {
80    control: Rc<RendererDropControl>,
81}
82
83impl ContextAttachment for RendererDropAttachment {
84    fn quiesce(
85        &self,
86        context: &ContextTeardown<'_>,
87    ) -> Result<(), dear_imgui_rs::ContextAttachmentTeardownError> {
88        if let Some(publication) = self.control.publication.borrow().as_ref() {
89            context.with_bound_context(|| unsafe {
90                publication.clear_owned_raw_state_bound();
91            });
92        }
93        Ok(())
94    }
95
96    fn release_renderer_resources(
97        &self,
98        context: &ContextTeardown<'_>,
99    ) -> Result<(), ContextAttachmentTeardownError> {
100        if let Some(resources) = self.control.deferred.borrow_mut().as_mut() {
101            resources.release_renderer_resources(context)?;
102            #[cfg(test)]
103            self.control
104                .renderer_release_count
105                .set(self.control.renderer_release_count.get() + 1);
106        }
107        Ok(())
108    }
109
110    fn context_destroyed(&self, _context: ContextDestroyed) {
111        // A raw Context pointer can remain published only when this renderer was dropped during
112        // Context teardown. At this point the core has tombstoned that pointer, so releasing the
113        // final token and any late resources cannot re-enter native Dear ImGui.
114        self.control.context_destroyed.set(true);
115        self.control.publication.borrow_mut().take();
116        if let Some(resources) = self.control.deferred.borrow_mut().take() {
117            resources.release_after_context_destroyed();
118        }
119    }
120}
121
122/// Lease and storage that make unreported renderer Drop defer to Context teardown.
123pub(super) struct RendererDropDeferral {
124    control: Rc<RendererDropControl>,
125    attachment: ContextAttachmentLease,
126}
127
128impl RendererDropDeferral {
129    pub(super) fn register(context: &mut Context) -> Result<Self, ContextAttachmentError> {
130        let control = Rc::new(RendererDropControl {
131            deferred: RefCell::new(None),
132            publication: RefCell::new(None),
133            context_destroyed: Cell::new(false),
134            #[cfg(test)]
135            renderer_release_count: Cell::new(0),
136        });
137        let attachment = context.register_attachment::<WgpuRendererDropAttachmentMarker>(
138            ContextAttachmentRole::Extension,
139            Rc::new(RendererDropAttachment {
140                control: Rc::clone(&control),
141            }),
142        )?;
143        Ok(Self {
144            control,
145            attachment,
146        })
147    }
148
149    pub(super) fn set_publication(&self, publication: RendererPublication) {
150        debug_assert!(self.control.publication.borrow().is_none());
151        self.control.publication.borrow_mut().replace(publication);
152    }
153
154    fn defer_to_context(self, resources: DeferredRendererResources) {
155        debug_assert!(self.control.deferred.borrow().is_none());
156        self.control.deferred.borrow_mut().replace(resources);
157        self.attachment.defer_to_context();
158    }
159
160    #[cfg(test)]
161    fn control_for_test(&self) -> Rc<RendererDropControl> {
162        Rc::clone(&self.control)
163    }
164}
165
166#[cfg(test)]
167impl RendererDropControl {
168    fn has_deferred_resources(&self) -> bool {
169        self.deferred.borrow().is_some()
170    }
171
172    fn renderer_release_count(&self) -> u32 {
173        self.renderer_release_count.get()
174    }
175
176    fn context_was_destroyed(&self) -> bool {
177        self.context_destroyed.get()
178    }
179}
180
181impl WgpuRenderer {
182    pub(super) fn prepare_frame_epoch(
183        &mut self,
184        epoch: u64,
185        native_frame_count: i32,
186    ) -> RendererResult<()> {
187        let needs_recreation = match &self.backend_data {
188            Some(backend_data) => {
189                self.ensure_context_alive()?;
190                self.ensure_renderer_contract()?;
191                !backend_data.is_initialized()
192            }
193            None => {
194                return Err(RendererError::InvalidRenderState(
195                    "renderer is not initialized".to_owned(),
196                ));
197            }
198        };
199
200        if needs_recreation {
201            let mut backend_data = self
202                .backend_data
203                .take()
204                .expect("prepare_frame_epoch() already verified backend data");
205            let result = self.create_device_objects(&mut backend_data);
206            self.backend_data = Some(backend_data);
207            result?;
208        }
209        self.backend_data
210            .as_mut()
211            .expect("prepare_frame_epoch() already verified backend data")
212            .begin_frame(epoch, native_frame_count)
213    }
214
215    /// Invalidate renderer-owned device objects and reset managed texture bindings.
216    ///
217    /// This corresponds to `ImGui_ImplWGPU_InvalidateDeviceObjects`. Passing the context makes
218    /// destroying renderer-owned GPU textures and requeueing Context-owned uploads one operation.
219    /// Application-owned external texture handles remain registered; after device loss, replace
220    /// their views through [`Self::update_external_texture`] before drawing them again.
221    pub fn invalidate_device_objects(&mut self, imgui_context: &mut Context) -> RendererResult<()> {
222        self.ensure_context_matches(imgui_context)?;
223        self.ensure_renderer_contract()?;
224        let consumer = self
225            .renderer_consumer
226            .take()
227            .ok_or(RendererError::ContextNotBound)?;
228        let reset = match imgui_context.prepare_renderer_texture_reset(&consumer) {
229            Ok(reset) => reset,
230            Err(error) => {
231                self.renderer_consumer = Some(consumer);
232                return Err(error.into());
233            }
234        };
235
236        self.invalidate_device_objects_only();
237        reset.commit();
238        self.texture_manager.clear_destroyed_managed_textures();
239        self.renderer_consumer = Some(consumer);
240
241        Ok(())
242    }
243
244    pub(super) fn invalidate_device_objects_only(&mut self) {
245        if let Some(ref mut backend_data) = self.backend_data {
246            backend_data.pipeline_state = None;
247            backend_data.render_resources = RenderResources::new();
248
249            // Clear frame resources
250            backend_data.frame_resources = FrameResourceArena::new();
251            backend_data.frame_cursor = Default::default();
252        }
253
254        self.texture_manager.clear_managed_textures();
255        self.default_texture = None;
256        self.shader_manager = ShaderManager::new();
257    }
258
259    fn release_all_device_objects_only(&mut self) {
260        self.invalidate_device_objects_only();
261        self.texture_manager.clear_external_views();
262    }
263
264    /// Shutdown the renderer and detach its Dear ImGui state.
265    ///
266    /// This corresponds to ImGui_ImplWGPU_Shutdown in the C++ implementation.
267    ///
268    /// The matching context is required so managed texture IDs, backend flags, the renderer name,
269    /// and standard draw callbacks cannot outlive the GPU resources they describe. An initialized
270    /// renderer consumed by a multi-viewport runtime is intentionally unavailable through this
271    /// method until that owning runtime completes teardown.
272    pub fn shutdown(&mut self, imgui_context: &mut Context) -> RendererResult<()> {
273        self.ensure_context_matches(imgui_context)?;
274        // Reset is the transactional preflight for teardown. Outstanding epochs leave
275        // the renderer, GPU resources, token, and raw Context contract intact for a later retry.
276        let consumer = self
277            .renderer_consumer
278            .take()
279            .ok_or(RendererError::ContextNotBound)?;
280        let reset = match imgui_context.prepare_renderer_texture_reset(&consumer) {
281            Ok(reset) => reset,
282            Err(error) => {
283                self.renderer_consumer = Some(consumer);
284                return Err(error.into());
285            }
286        };
287
288        self.release_all_device_objects_only();
289        reset.commit();
290        self.texture_manager.clear_destroyed_managed_textures();
291        self.backend_data = None;
292        drop(consumer);
293        self.clear_bound_imgui_context(imgui_context);
294        Ok(())
295    }
296
297    /// Validates that shutdown can acquire its renderer reset permit without mutating renderer
298    /// state. Multi-viewport teardown calls this before it destroys surfaces or releases callback
299    /// slots, so an outstanding renderer epoch leaves the whole runtime retryable.
300    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
301    pub(super) fn preflight_shutdown(&self, imgui_context: &mut Context) -> RendererResult<()> {
302        self.ensure_context_matches(imgui_context)?;
303        let consumer = self
304            .renderer_consumer
305            .as_ref()
306            .ok_or(RendererError::ContextNotBound)?;
307        // Dropping an uncommitted permit is intentionally inert. This validates the exact
308        // consumer generation now; `shutdown` prepares it again immediately before commit.
309        let _reset = imgui_context.prepare_renderer_texture_reset(consumer)?;
310        Ok(())
311    }
312
313    pub(super) fn owned_draw_callbacks_match(
314        platform_io: &dear_imgui_rs::platform_io::PlatformIo,
315    ) -> bool {
316        draw_callback_matches(
317            platform_io.draw_callback_reset_render_state_raw(),
318            draw_callback_reset_render_state,
319        ) && draw_callback_matches(
320            platform_io.draw_callback_set_sampler_linear_raw(),
321            draw_callback_set_sampler_linear,
322        ) && draw_callback_matches(
323            platform_io.draw_callback_set_sampler_nearest_raw(),
324            draw_callback_set_sampler_nearest,
325        )
326    }
327
328    pub(super) fn owns_any_standard_draw_callback(
329        platform_io: &dear_imgui_rs::platform_io::PlatformIo,
330    ) -> bool {
331        draw_callback_matches(
332            platform_io.draw_callback_reset_render_state_raw(),
333            draw_callback_reset_render_state,
334        ) || draw_callback_matches(
335            platform_io.draw_callback_set_sampler_linear_raw(),
336            draw_callback_set_sampler_linear,
337        ) || draw_callback_matches(
338            platform_io.draw_callback_set_sampler_nearest_raw(),
339            draw_callback_set_sampler_nearest,
340        )
341    }
342
343    pub(super) fn clear_owned_draw_callbacks(
344        platform_io: &mut dear_imgui_rs::platform_io::PlatformIo,
345    ) {
346        if draw_callback_matches(
347            platform_io.draw_callback_reset_render_state_raw(),
348            draw_callback_reset_render_state,
349        ) {
350            unsafe { platform_io.set_draw_callback_reset_render_state_raw(None) };
351        }
352        if draw_callback_matches(
353            platform_io.draw_callback_set_sampler_linear_raw(),
354            draw_callback_set_sampler_linear,
355        ) {
356            unsafe { platform_io.set_draw_callback_set_sampler_linear_raw(None) };
357        }
358        if draw_callback_matches(
359            platform_io.draw_callback_set_sampler_nearest_raw(),
360            draw_callback_set_sampler_nearest,
361        ) {
362            unsafe { platform_io.set_draw_callback_set_sampler_nearest_raw(None) };
363        }
364        // Renderer_RenderState is a transient draw-scope field and is never owned by the core
365        // WGPU renderer between calls. Do not clear a foreign value during teardown.
366    }
367
368    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
369    pub(super) fn shutdown_during_context_teardown(
370        &mut self,
371        context: &ContextTeardown<'_>,
372        release_viewports: impl FnOnce() -> Result<(), ContextAttachmentTeardownError>,
373    ) -> Result<(), ContextAttachmentTeardownError> {
374        let consumer = self.renderer_consumer.take().ok_or_else(|| {
375            ContextAttachmentTeardownError::new(
376                "WGPU viewport renderer lost its renderer consumer before Context teardown",
377            )
378        })?;
379
380        // Context-owned teardown has no retry channel, but it still follows the normal reset
381        // contract exactly: validate the idle consumer, release sidecars and every GPU texture,
382        // then commit native binding invalidation. A failed preflight or sidecar release restores
383        // the consumer and leaves the renderer fields intact for the fail-stop Context owner.
384        let result = context.with_renderer_texture_reset(&consumer, || {
385            release_viewports()?;
386            self.release_all_device_objects_only();
387            self.backend_data = None;
388            Ok(())
389        });
390        match result {
391            Ok(()) => {
392                self.texture_manager.clear_destroyed_managed_textures();
393                drop(consumer);
394                self.clear_context_state();
395                Ok(())
396            }
397            Err(error) => {
398                self.renderer_consumer = Some(consumer);
399                Err(error)
400            }
401        }
402    }
403
404    #[cfg(any(feature = "multi-viewport-winit", feature = "multi-viewport-sdl3"))]
405    pub(super) fn shutdown_after_context_destroyed(&mut self) {
406        self.release_all_device_objects_only();
407        self.backend_data = None;
408        self.renderer_consumer = None;
409        self.clear_context_state();
410    }
411
412    fn defer_resources_to_context(&mut self, context_state: Option<RendererContextState>) {
413        // `bind_context` installs this attachment before it publishes `context_state`. Keep the
414        // relationship fail-stop: silently dropping or forgetting the texture map here would
415        // leave live Context bindings pointing at renderer resources that no longer exist.
416        let deferral = self.drop_deferral.take().unwrap_or_else(|| {
417            debug_assert!(
418                false,
419                "a bound WGPU renderer must retain its Context drop attachment"
420            );
421            std::process::abort();
422        });
423        let resources = DeferredRendererResources {
424            context_state,
425            backend_data: self.backend_data.take(),
426            shader_manager: Some(std::mem::take(&mut self.shader_manager)),
427            texture_manager: Some(std::mem::take(&mut self.texture_manager)),
428            default_texture: self.default_texture.take(),
429            renderer_consumer: self.renderer_consumer.take(),
430        };
431        deferral.defer_to_context(resources);
432    }
433}
434
435impl Drop for WgpuRenderer {
436    fn drop(&mut self) {
437        let clear_result = self.context_state.as_ref().map(|state| {
438            let binding = state.context();
439            binding.try_with_bound_context(|| unsafe { state.clear_owned_raw_state_bound() })
440        });
441        match clear_result {
442            Some(Ok(_)) => {
443                // Exact raw publication is gone, so the token can be released immediately. The
444                // GPU map and consumer remain Context-owned through the deferred attachment.
445                drop(self.context_state.take());
446                self.defer_resources_to_context(None);
447            }
448            Some(Err(error)) => {
449                if matches!(error, ContextBindingError::NativeDestroyed) {
450                    // Native teardown has already invalidated every Context binding. It is now
451                    // safe for normal field Drop to release backend resources without entering
452                    // Dear ImGui.
453                    self.drop_deferral = None;
454                    drop(self.context_state.take());
455                } else {
456                    // The raw Context still exists, so retain the publication token until
457                    // `context_destroyed`. The attachment owns terminal GPU cleanup meanwhile.
458                    let context_state = self.context_state.take();
459                    self.defer_resources_to_context(context_state);
460                }
461            }
462            None => {
463                self.drop_deferral = None;
464            }
465        }
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use dear_imgui_rs::{
472        BackendFlags, Context, sys,
473        texture::{OwnedTextureData, TextureFormat, TextureStatus},
474    };
475
476    use super::WgpuRenderer;
477    use crate::{
478        RendererError,
479        renderer::callbacks::{
480            draw_callback_reset_render_state, draw_callback_set_sampler_linear,
481            draw_callback_set_sampler_nearest,
482        },
483    };
484
485    fn configured_test_renderer(context: &mut Context) -> WgpuRenderer {
486        let (flags, _) = WgpuRenderer::configure_imgui_context(context)
487            .expect("fresh Context should accept WGPU renderer state");
488        let mut renderer = WgpuRenderer::empty();
489        renderer
490            .bind_context(context, flags)
491            .expect("configured Context should bind once");
492        renderer.renderer_consumer = Some(
493            context
494                .create_synchronous_renderer_consumer()
495                .expect("test Context should create a renderer consumer"),
496        );
497        renderer
498    }
499
500    unsafe extern "C" fn foreign_draw_callback(
501        _parent_list: *const sys::ImDrawList,
502        _cmd: *const sys::ImDrawCmd,
503    ) {
504    }
505
506    fn renderer_flags() -> BackendFlags {
507        BackendFlags::RENDERER_HAS_TEXTURES | BackendFlags::RENDERER_HAS_VTX_OFFSET
508    }
509
510    fn install_complete_foreign_renderer_takeover(context: &mut Context) {
511        context
512            .set_renderer_name(Some("foreign-renderer".to_owned()))
513            .unwrap();
514        unsafe {
515            context
516                .io_mut()
517                .set_backend_renderer_user_data(std::ptr::dangling_mut::<u8>().cast());
518            let platform_io = context.platform_io_mut();
519            platform_io.set_draw_callback_reset_render_state_raw(Some(foreign_draw_callback));
520            platform_io.set_draw_callback_set_sampler_linear_raw(Some(foreign_draw_callback));
521            platform_io.set_draw_callback_set_sampler_nearest_raw(Some(foreign_draw_callback));
522        }
523    }
524
525    fn assert_complete_foreign_takeover_is_preserved(context: &Context) {
526        assert_eq!(
527            context.io().backend_renderer_name().unwrap().to_bytes(),
528            b"foreign-renderer"
529        );
530        assert!(!context.io().backend_renderer_user_data().is_null());
531        assert!(context.io().backend_flags().contains(renderer_flags()));
532        let platform_io = context.platform_io();
533        for callback in [
534            platform_io.draw_callback_reset_render_state_raw(),
535            platform_io.draw_callback_set_sampler_linear_raw(),
536            platform_io.draw_callback_set_sampler_nearest_raw(),
537        ] {
538            assert!(callback.is_some_and(|callback| {
539                std::ptr::fn_addr_eq(
540                    callback,
541                    foreign_draw_callback
542                        as unsafe extern "C" fn(*const sys::ImDrawList, *const sys::ImDrawCmd),
543                )
544            }));
545        }
546    }
547
548    fn clear_complete_foreign_renderer_takeover(context: &mut Context) {
549        context.set_renderer_name::<String>(None).unwrap();
550        unsafe {
551            context
552                .io_mut()
553                .set_backend_renderer_user_data(std::ptr::null_mut());
554            let mut flags = context.io().backend_flags();
555            flags.remove(renderer_flags());
556            context.io_mut().set_backend_flags(flags);
557            let platform_io = context.platform_io_mut();
558            platform_io.set_draw_callback_reset_render_state_raw(None);
559            platform_io.set_draw_callback_set_sampler_linear_raw(None);
560            platform_io.set_draw_callback_set_sampler_nearest_raw(None);
561        }
562    }
563
564    #[test]
565    fn unbound_device_invalidation_preserves_managed_texture_bindings() {
566        let mut context = Context::create();
567        let texture = OwnedTextureData::from_pixels(TextureFormat::RGBA32, 1, 1, &[0; 4]).unwrap();
568        let texture = context.register_texture(texture);
569
570        let mut renderer = WgpuRenderer::empty();
571        let result = renderer.invalidate_device_objects(&mut context);
572
573        assert!(matches!(result, Err(RendererError::ContextNotBound)));
574        context
575            .with_texture(texture, |texture| {
576                assert_eq!(texture.status(), TextureStatus::WantCreate);
577                assert!(texture.texture_id().is_null());
578            })
579            .expect("registered texture should remain active");
580    }
581
582    #[test]
583    fn renderer_owns_the_context_synchronous_consumer() {
584        let mut context = Context::create();
585        let mut renderer = configured_test_renderer(&mut context);
586
587        let consumer = renderer
588            .renderer_consumer()
589            .expect("configured renderer should own a consumer");
590        assert_eq!(consumer.context_id(), context.id());
591        assert!(matches!(
592            context.create_detached_renderer_consumer(),
593            Err(dear_imgui_rs::render::RendererConsumerError::ConsumerAlreadyActive)
594        ));
595
596        renderer.shutdown(&mut context).unwrap();
597    }
598
599    #[test]
600    fn dropping_renderer_defers_resources_to_context_teardown() {
601        let mut context = Context::create();
602        let renderer = configured_test_renderer(&mut context);
603        let control = renderer
604            .drop_deferral
605            .as_ref()
606            .expect("bound renderer must install a deferred-drop attachment")
607            .control_for_test();
608        drop(renderer);
609
610        assert!(control.has_deferred_resources());
611        assert_eq!(control.renderer_release_count(), 0);
612        assert!(context.io().backend_renderer_user_data().is_null());
613        assert!(context.io().backend_renderer_name().is_none());
614        assert!(!context.io().backend_flags().intersects(renderer_flags()));
615        assert!(matches!(
616            WgpuRenderer::empty().bind_context(&mut context, BackendFlags::empty()),
617            Err(RendererError::ContextAttachment(
618                dear_imgui_rs::ContextAttachmentError::DuplicateAttachment
619            ))
620        ));
621
622        assert!(control.has_deferred_resources());
623        assert_eq!(control.renderer_release_count(), 0);
624
625        drop(context);
626
627        assert_eq!(control.renderer_release_count(), 1);
628        assert!(!control.has_deferred_resources());
629        assert!(control.context_was_destroyed());
630    }
631
632    #[test]
633    fn context_first_renderer_drop_uses_the_native_destroyed_path() {
634        let mut context = Context::create();
635        let renderer = configured_test_renderer(&mut context);
636        let control = renderer
637            .drop_deferral
638            .as_ref()
639            .expect("bound renderer must install a deferred-drop attachment")
640            .control_for_test();
641
642        drop(context);
643
644        assert!(control.context_was_destroyed());
645        assert!(!control.has_deferred_resources());
646        assert_eq!(control.renderer_release_count(), 0);
647        drop(renderer);
648    }
649
650    #[test]
651    fn foreign_context_lifecycle_calls_are_transactional() {
652        let mut owner = Context::create();
653        let mut renderer = configured_test_renderer(&mut owner);
654
655        let suspended_owner = owner.suspend_or_panic();
656        let mut foreign = Context::create();
657        let foreign_flags = foreign.io().backend_flags();
658
659        assert!(matches!(
660            renderer.invalidate_device_objects(&mut foreign),
661            Err(RendererError::ContextMismatch)
662        ));
663        assert!(matches!(
664            renderer.shutdown(&mut foreign),
665            Err(RendererError::ContextMismatch)
666        ));
667        assert_eq!(foreign.io().backend_flags(), foreign_flags);
668        assert!(renderer.context_state.is_some());
669
670        let suspended_foreign = foreign.suspend_or_panic();
671        let mut owner = suspended_owner
672            .activate()
673            .expect("owner context should reactivate");
674        renderer
675            .shutdown(&mut owner)
676            .expect("matching context should shut down the test renderer");
677        assert!(renderer.context_state.is_none());
678        drop(suspended_foreign);
679    }
680
681    #[test]
682    fn moving_renderer_preserves_published_token_and_name_identity() {
683        let mut context = Context::create();
684        let renderer = configured_test_renderer(&mut context);
685        let token = context.io().backend_renderer_user_data();
686        let name = context
687            .io()
688            .backend_renderer_name()
689            .expect("renderer name should be published")
690            .as_ptr();
691
692        let mut moved = renderer;
693        assert_eq!(context.io().backend_renderer_user_data(), token);
694        assert_eq!(context.io().backend_renderer_name().unwrap().as_ptr(), name);
695        moved
696            .ensure_renderer_contract()
697            .expect("moving WgpuRenderer must preserve its raw identities");
698        moved.shutdown(&mut context).unwrap();
699    }
700
701    #[test]
702    fn same_bytes_at_a_foreign_name_pointer_are_drift_and_survive_teardown() {
703        let mut context = Context::create();
704        let mut renderer = configured_test_renderer(&mut context);
705        let owned_name = context.io().backend_renderer_name().unwrap().as_ptr();
706        context
707            .set_renderer_name(Some(format!(
708                "dear-imgui-wgpu {}",
709                env!("CARGO_PKG_VERSION")
710            )))
711            .unwrap();
712        let foreign_name = context.io().backend_renderer_name().unwrap().as_ptr();
713        assert_ne!(foreign_name, owned_name);
714
715        assert!(matches!(
716            renderer.ensure_renderer_contract(),
717            Err(RendererError::RendererStateDrift {
718                field: "BackendRendererName"
719            })
720        ));
721        assert_eq!(
722            context.io().backend_renderer_name().unwrap().as_ptr(),
723            foreign_name
724        );
725
726        renderer.shutdown(&mut context).unwrap();
727        assert_eq!(
728            context.io().backend_renderer_name().unwrap().as_ptr(),
729            foreign_name
730        );
731        context.set_renderer_name::<String>(None).unwrap();
732    }
733
734    #[test]
735    fn foreign_user_data_is_drift_and_survives_teardown() {
736        let mut context = Context::create();
737        let mut renderer = configured_test_renderer(&mut context);
738        let foreign = std::ptr::dangling_mut::<u8>().cast();
739        unsafe { context.io_mut().set_backend_renderer_user_data(foreign) };
740
741        assert!(matches!(
742            renderer.ensure_renderer_contract(),
743            Err(RendererError::RendererStateDrift {
744                field: "BackendRendererUserData"
745            })
746        ));
747        assert_eq!(context.io().backend_renderer_user_data(), foreign);
748
749        renderer.shutdown(&mut context).unwrap();
750        assert_eq!(context.io().backend_renderer_user_data(), foreign);
751        unsafe {
752            context
753                .io_mut()
754                .set_backend_renderer_user_data(std::ptr::null_mut())
755        };
756    }
757
758    #[test]
759    fn shutdown_preserves_flags_after_complete_foreign_renderer_takeover() {
760        let mut context = Context::create();
761        let mut renderer = configured_test_renderer(&mut context);
762        install_complete_foreign_renderer_takeover(&mut context);
763
764        assert!(matches!(
765            renderer.ensure_renderer_contract(),
766            Err(RendererError::RendererStateDrift {
767                field: "BackendRendererUserData"
768            })
769        ));
770        assert_complete_foreign_takeover_is_preserved(&context);
771
772        renderer.shutdown(&mut context).unwrap();
773        assert_complete_foreign_takeover_is_preserved(&context);
774        clear_complete_foreign_renderer_takeover(&mut context);
775    }
776
777    #[test]
778    fn drop_preserves_flags_after_complete_foreign_renderer_takeover() {
779        let mut context = Context::create();
780        let renderer = configured_test_renderer(&mut context);
781        install_complete_foreign_renderer_takeover(&mut context);
782
783        assert!(matches!(
784            renderer.ensure_renderer_contract(),
785            Err(RendererError::RendererStateDrift {
786                field: "BackendRendererUserData"
787            })
788        ));
789        drop(renderer);
790
791        assert_complete_foreign_takeover_is_preserved(&context);
792        clear_complete_foreign_renderer_takeover(&mut context);
793    }
794
795    #[test]
796    fn renderer_contract_fault_is_sticky_across_raw_aba_restoration() {
797        let mut context = Context::create();
798        let mut renderer = configured_test_renderer(&mut context);
799        let token = context.io().backend_renderer_user_data();
800        let name = context.io().backend_renderer_name().unwrap().as_ptr();
801        let mut flags = context.io().backend_flags();
802        flags.remove(BackendFlags::RENDERER_HAS_TEXTURES);
803        context.io_mut().set_backend_flags(flags);
804
805        assert!(matches!(
806            renderer.ensure_renderer_contract(),
807            Err(RendererError::RendererStateDrift {
808                field: "RENDERER_HAS_TEXTURES"
809            })
810        ));
811
812        let io = unsafe { sys::igGetIO_ContextPtr(context.as_raw()) };
813        unsafe {
814            (*io).BackendRendererUserData = token;
815            (*io).BackendRendererName = name;
816            (*io).BackendFlags |= (BackendFlags::RENDERER_HAS_VTX_OFFSET
817                | BackendFlags::RENDERER_HAS_TEXTURES)
818                .bits();
819            let platform_io = context.platform_io_mut();
820            platform_io
821                .set_draw_callback_reset_render_state_raw(Some(draw_callback_reset_render_state));
822            platform_io
823                .set_draw_callback_set_sampler_linear_raw(Some(draw_callback_set_sampler_linear));
824            platform_io
825                .set_draw_callback_set_sampler_nearest_raw(Some(draw_callback_set_sampler_nearest));
826        }
827
828        assert!(matches!(
829            renderer.ensure_renderer_contract(),
830            Err(RendererError::RendererStateDrift {
831                field: "RENDERER_HAS_TEXTURES"
832            })
833        ));
834        assert!(context.io().backend_renderer_user_data().is_null());
835        assert!(context.io().backend_renderer_name().is_none());
836        assert!(!context.io().backend_flags().intersects(
837            BackendFlags::RENDERER_HAS_VTX_OFFSET | BackendFlags::RENDERER_HAS_TEXTURES
838        ));
839
840        renderer.shutdown(&mut context).unwrap();
841    }
842
843    #[test]
844    fn explicit_shutdown_revokes_context_state_and_allows_replacement() {
845        let mut context = Context::create();
846        let mut renderer = configured_test_renderer(&mut context);
847        renderer.shutdown(&mut context).unwrap();
848
849        assert!(context.io().backend_renderer_user_data().is_null());
850        assert!(context.io().backend_renderer_name().is_none());
851        assert!(!context.io().backend_flags().intersects(
852            BackendFlags::RENDERER_HAS_VTX_OFFSET | BackendFlags::RENDERER_HAS_TEXTURES
853        ));
854        assert!(
855            context
856                .platform_io()
857                .draw_callback_reset_render_state_raw()
858                .is_none()
859        );
860        assert!(
861            context
862                .platform_io()
863                .draw_callback_set_sampler_linear_raw()
864                .is_none()
865        );
866        assert!(
867            context
868                .platform_io()
869                .draw_callback_set_sampler_nearest_raw()
870                .is_none()
871        );
872
873        let mut replacement = configured_test_renderer(&mut context);
874        replacement.shutdown(&mut context).unwrap();
875    }
876}