Skip to main content

dear_imgui_rs/context/
attachment.rs

1use std::any::TypeId;
2use std::cell::{Cell, RefCell};
3use std::fmt;
4use std::marker::PhantomData;
5use std::panic::{AssertUnwindSafe, catch_unwind};
6use std::ptr::NonNull;
7use std::rc::{Rc, Weak};
8
9use thiserror::Error;
10
11use crate::render::RendererConsumerCapability;
12
13use super::binding::{self, ContextId, ContextLifecycle, ContextState};
14use super::core::Context;
15
16/// Ordered phase of Context teardown exposed to an attachment hook.
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18#[non_exhaustive]
19pub enum ContextAttachmentPhase {
20    /// Stop new work and make callbacks inert.
21    Quiesce,
22    /// Release renderer-owned resources for secondary viewports.
23    RendererResources,
24    /// Destroy platform-owned secondary windows.
25    PlatformWindows,
26}
27
28/// Exclusive role claimed by a Context attachment.
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30#[non_exhaustive]
31pub enum ContextAttachmentRole {
32    /// An extension without platform or renderer ordering requirements.
33    Extension,
34    /// The Context's renderer runtime.
35    Renderer,
36    /// The Context's platform runtime.
37    Platform,
38}
39
40/// Non-retryable failure reported by an attachment during `Context::drop`.
41///
42/// Backends should expose their concrete error from explicit shutdown APIs. This erased error is
43/// only for the final ownership fallback, where continuing into a later teardown phase would be
44/// unsafe and the process therefore aborts after all peers in the current phase are notified.
45#[derive(Clone, Debug, Eq, Error, PartialEq)]
46#[error("{message}")]
47pub struct ContextAttachmentTeardownError {
48    message: String,
49}
50
51impl ContextAttachmentTeardownError {
52    /// Create a fail-stop attachment error from a backend diagnostic.
53    pub fn new(message: impl Into<String>) -> Self {
54        Self {
55            message: message.into(),
56        }
57    }
58}
59
60/// Failure while entering or leaving an explicit platform-window teardown transaction.
61///
62/// Unlike [`ContextAttachmentTeardownError`], this error is returned to the caller of
63/// [`crate::Context::destroy_platform_windows`] before or after its native operation. It does not
64/// use Context-drop's fail-stop policy because the caller still owns a live Context.
65#[derive(Clone, Debug, Eq, Error, PartialEq)]
66#[non_exhaustive]
67pub enum ContextPlatformWindowTeardownError {
68    /// The Context is already being dropped.
69    #[error("Dear ImGui context teardown is in progress")]
70    ContextDropping,
71    /// A platform-window teardown transaction attempted to re-enter itself.
72    #[error("platform-window teardown cannot be reentered")]
73    Reentrant,
74    /// The active platform attachment rejected the transaction before native teardown began.
75    #[error("platform attachment rejected platform-window teardown: {0}")]
76    AttachmentPreflight(#[source] ContextAttachmentTeardownError),
77    /// The active platform attachment failed after native teardown completed.
78    #[error("platform attachment could not complete platform-window teardown: {0}")]
79    AttachmentPostflight(#[source] ContextAttachmentTeardownError),
80    /// The active platform attachment panicked before native teardown began.
81    #[error("platform attachment panicked before platform-window teardown")]
82    BeginPanicked,
83    /// The active platform attachment panicked after native teardown completed.
84    #[error("platform attachment panicked after platform-window teardown")]
85    EndPanicked,
86}
87
88/// Type-erased lifecycle hooks owned by a Context.
89///
90/// Hooks must be idempotent. If a hook panics, the remaining attachments in that phase are still
91/// notified, then the process aborts before a later destructive phase can violate resource
92/// ordering. Explicit backend shutdown APIs remain responsible for reporting retryable errors;
93/// attachment hooks are the fail-stop fallback used by `Context::drop`.
94pub trait ContextAttachment {
95    /// Validates and prepares a normal [`crate::Context::destroy_platform_windows`] call.
96    ///
97    /// Only the active platform attachment receives this hook. The passed capability may bind the
98    /// target Context for immediate native inspection, but intentionally does not expose a mutable
99    /// `Context` reference. Returning an error prevents native teardown from starting.
100    fn begin_platform_window_teardown(
101        &self,
102        _context: &ContextPlatformWindowTeardown<'_>,
103    ) -> Result<(), ContextAttachmentTeardownError> {
104        Ok(())
105    }
106
107    /// Completes a normal [`crate::Context::destroy_platform_windows`] call.
108    ///
109    /// This runs only when [`Self::begin_platform_window_teardown`] succeeded and native teardown
110    /// returned normally. Implementations should restore any temporary callback state and record
111    /// the new native baseline before returning.
112    fn end_platform_window_teardown(
113        &self,
114        _context: &ContextPlatformWindowTeardown<'_>,
115    ) -> Result<(), ContextAttachmentTeardownError> {
116        Ok(())
117    }
118
119    /// Stops new work before native teardown begins.
120    fn quiesce(
121        &self,
122        _context: &ContextTeardown<'_>,
123    ) -> Result<(), ContextAttachmentTeardownError> {
124        Ok(())
125    }
126
127    /// Releases renderer resources before platform windows are destroyed.
128    fn release_renderer_resources(
129        &self,
130        _context: &ContextTeardown<'_>,
131    ) -> Result<(), ContextAttachmentTeardownError> {
132        Ok(())
133    }
134
135    /// Releases platform windows before the native Context is destroyed.
136    fn release_platform_windows(
137        &self,
138        _context: &ContextTeardown<'_>,
139    ) -> Result<(), ContextAttachmentTeardownError> {
140        Ok(())
141    }
142
143    /// Tombstones Rust state after the native Context has been destroyed.
144    fn context_destroyed(&self, _context: ContextDestroyed) {}
145}
146
147/// Failure to register an attachment with a Context.
148#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
149#[non_exhaustive]
150pub enum ContextAttachmentError {
151    /// An attachment with the same marker type is already active.
152    #[error("an attachment with this marker type is already registered")]
153    DuplicateAttachment,
154    /// The exclusive platform or renderer role is already occupied.
155    #[error("the {0:?} attachment role is already occupied")]
156    RoleOccupied(ContextAttachmentRole),
157    /// A renderer cannot attach until a platform runtime is registered.
158    #[error("a renderer attachment requires an active platform attachment")]
159    MissingPlatform,
160    /// Context teardown has already started.
161    #[error("Dear ImGui context teardown has already started")]
162    ContextDropping,
163}
164
165/// Failure to explicitly detach a live Context attachment lease.
166///
167/// A failed detach leaves both the attachment and lease active. Platform backends must release
168/// renderer dependencies first and use [`Context::prepare_platform_attachment_release`] for
169/// transactional native teardown.
170#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
171#[non_exhaustive]
172pub enum ContextAttachmentDetachError {
173    /// Another transaction has reserved this platform attachment generation for release.
174    #[error("platform attachment release is already in progress")]
175    ReleaseInProgress,
176    /// Renderer resources still depend on platform-owned native handles.
177    #[error("the platform attachment cannot be detached while a renderer attachment is active")]
178    RendererActive,
179}
180
181/// Failure to prepare an explicit platform attachment release.
182///
183/// Platform backends must complete this preflight before closing a frame, destroying native
184/// windows, or clearing callback state. The returned permit keeps the exact attachment generation
185/// reserved until the backend either commits detachment or abandons the transaction.
186#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
187#[non_exhaustive]
188pub enum ContextPlatformAttachmentReleaseError {
189    /// Context-owned teardown has already started.
190    #[error("Dear ImGui context teardown is in progress")]
191    ContextDropping,
192    /// The supplied attachment generation has already detached.
193    #[error("the platform attachment generation is no longer active")]
194    AttachmentInactive,
195    /// The supplied attachment does not own the platform role.
196    #[error("the supplied attachment does not own the platform role")]
197    NotPlatform,
198    /// The supplied attachment is not this Context's active platform generation.
199    #[error("the supplied attachment is not the active platform generation for this Context")]
200    PlatformGenerationMismatch,
201    /// Another release transaction already reserves this platform generation.
202    #[error("platform attachment release is already in progress")]
203    ReleaseInProgress,
204    /// Renderer resources still depend on platform-owned native handles.
205    #[error("the platform attachment cannot be released while a renderer attachment is active")]
206    RendererActive,
207}
208
209/// Phase-limited access passed to pre-destroy attachment hooks.
210pub struct ContextTeardown<'a> {
211    owner: NonNull<Context>,
212    phase: ContextAttachmentPhase,
213    renderer_texture_reset_active: Cell<bool>,
214    _exclusive_owner: PhantomData<&'a mut Context>,
215}
216
217impl fmt::Debug for ContextTeardown<'_> {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        f.debug_struct("ContextTeardown")
220            .field("id", &self.id())
221            .field("phase", &self.phase)
222            .finish_non_exhaustive()
223    }
224}
225
226impl ContextTeardown<'_> {
227    fn new<'owner>(
228        owner: &'owner mut Context,
229        phase: ContextAttachmentPhase,
230    ) -> ContextTeardown<'owner> {
231        ContextTeardown {
232            owner: NonNull::from(owner),
233            phase,
234            renderer_texture_reset_active: Cell::new(false),
235            _exclusive_owner: PhantomData,
236        }
237    }
238
239    fn state(&self) -> &ContextState {
240        // SAFETY: `run_pre_destroy_phase` creates this capability from its exclusive Context
241        // borrow and does not access that Context again until the capability is dropped.
242        unsafe { self.owner.as_ref().state.as_ref() }
243    }
244
245    /// Returns the Context identity being torn down.
246    pub fn id(&self) -> ContextId {
247        self.state().id()
248    }
249
250    /// Returns the currently executing teardown phase.
251    pub fn phase(&self) -> ContextAttachmentPhase {
252        self.phase
253    }
254
255    /// Runs a closure while the dropping Context is current.
256    ///
257    /// This capability is valid only for the duration of the current pre-destroy hook.
258    pub fn with_bound_context<R>(&self, f: impl FnOnce() -> R) -> R {
259        assert_eq!(
260            self.state().lifecycle(),
261            ContextLifecycle::Dropping,
262            "ContextTeardown used outside pre-destroy teardown"
263        );
264        let raw = self.state().raw_during_teardown();
265        assert!(
266            !raw.is_null(),
267            "ContextTeardown used after native Context destruction"
268        );
269
270        binding::with_bound_context(raw, f)
271    }
272
273    /// Release a renderer's complete GPU texture map and reset its native bindings atomically.
274    ///
275    /// This transaction is available only from [`ContextAttachmentPhase::RendererResources`].
276    /// The Context first validates that `consumer` is its matching idle renderer generation. It
277    /// then runs `release`, and resets Context-owned texture bindings only when `release` returns
278    /// `Ok(())`. A failed preflight, a failed release, a panic, or a reentrant call leaves those
279    /// native bindings unchanged. The closure receives no Context access.
280    ///
281    /// Attachment hooks are the fail-stop fallback used by `Context::drop`; concrete renderer
282    /// shutdown APIs should continue to expose their retryable backend errors before deferring
283    /// ownership to the Context.
284    pub fn with_renderer_texture_reset(
285        &self,
286        consumer: &impl RendererConsumerCapability,
287        release: impl FnOnce() -> Result<(), ContextAttachmentTeardownError>,
288    ) -> Result<(), ContextAttachmentTeardownError> {
289        if self.phase != ContextAttachmentPhase::RendererResources {
290            return Err(ContextAttachmentTeardownError::new(format!(
291                "renderer texture reset requires the RendererResources phase, not {:?}",
292                self.phase
293            )));
294        }
295        if self.state().lifecycle() != ContextLifecycle::Dropping {
296            return Err(ContextAttachmentTeardownError::new(
297                "renderer texture reset requires active Context teardown",
298            ));
299        }
300        if self.renderer_texture_reset_active.replace(true) {
301            return Err(ContextAttachmentTeardownError::new(
302                "renderer texture reset cannot be reentered",
303            ));
304        }
305        let _active = RendererTextureResetInvocation {
306            active: &self.renderer_texture_reset_active,
307        };
308
309        // SAFETY: `ContextTeardown` owns the exclusive Context borrow for this entire hook. No
310        // mutable Context reference crosses `release`, and the active flag rejects recursive
311        // attempts to create another reset transaction through this capability.
312        let watermark = unsafe { &mut *self.owner.as_ptr() }
313            .prepare_renderer_texture_reset_during_teardown(consumer)
314            .map_err(|error| {
315                ContextAttachmentTeardownError::new(format!(
316                    "renderer texture reset preflight failed: {error}"
317                ))
318            })?;
319
320        release()?;
321
322        // SAFETY: the preflight's mutable borrow ended before `release` ran. This is the same
323        // exclusive Context owner, the phase and lifecycle were validated above, and reentrancy
324        // remains blocked until the commit completes.
325        unsafe { &mut *self.owner.as_ptr() }
326            .commit_renderer_texture_reset_during_teardown(watermark);
327        Ok(())
328    }
329
330    #[cfg(test)]
331    pub(super) fn as_raw_for_test(&self) -> *mut crate::sys::ImGuiContext {
332        self.state().raw_during_teardown()
333    }
334}
335
336/// Phase-limited capability passed around a normal platform-window teardown transaction.
337///
338/// The Context remains alive throughout this scope. It exists so a platform backend can prepare
339/// callback state for native teardown without receiving unrestricted mutable Context access.
340pub struct ContextPlatformWindowTeardown<'a> {
341    state: &'a ContextState,
342    _exclusive_owner: PhantomData<&'a mut Context>,
343}
344
345impl fmt::Debug for ContextPlatformWindowTeardown<'_> {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        f.debug_struct("ContextPlatformWindowTeardown")
348            .field("id", &self.id())
349            .finish_non_exhaustive()
350    }
351}
352
353impl<'a> ContextPlatformWindowTeardown<'a> {
354    #[cfg(feature = "multi-viewport")]
355    pub(super) fn new(state: &'a ContextState) -> Self {
356        Self {
357            state,
358            _exclusive_owner: PhantomData,
359        }
360    }
361
362    /// Returns the Context identity whose platform windows are being torn down.
363    pub fn id(&self) -> ContextId {
364        self.state.id()
365    }
366
367    /// Runs a closure while the target Context is current.
368    ///
369    /// The capability remains valid only for the observer hook currently executing. It does not
370    /// provide a mutable [`Context`] reference, so backend callbacks cannot recursively enter an
371    /// unrelated Context operation while native window teardown is in progress.
372    pub fn with_bound_context<R>(&self, f: impl FnOnce() -> R) -> R {
373        assert_eq!(
374            self.state.lifecycle(),
375            ContextLifecycle::Alive,
376            "ContextPlatformWindowTeardown used outside a live Context"
377        );
378        let raw = self.state.raw_during_teardown();
379        assert!(
380            !raw.is_null(),
381            "ContextPlatformWindowTeardown used after native Context destruction"
382        );
383        binding::with_bound_context(raw, f)
384    }
385}
386
387struct RendererTextureResetInvocation<'a> {
388    active: &'a Cell<bool>,
389}
390
391impl Drop for RendererTextureResetInvocation<'_> {
392    fn drop(&mut self) {
393        self.active.set(false);
394    }
395}
396
397/// Pointer-free notification passed after native Context destruction.
398#[derive(Clone, Copy, Debug, Eq, PartialEq)]
399pub struct ContextDestroyed {
400    id: ContextId,
401}
402
403impl ContextDestroyed {
404    /// Returns the identity of the destroyed Context.
405    pub fn id(self) -> ContextId {
406        self.id
407    }
408}
409
410#[derive(Clone, Copy, Debug, Eq, PartialEq)]
411enum AttachmentState {
412    Active,
413    ReleasePrepared,
414    Teardown,
415    Complete,
416    Detached,
417}
418
419#[derive(Default)]
420struct AttachmentRoleState {
421    renderer_active: Cell<bool>,
422}
423
424pub(super) struct AttachmentControl {
425    marker: TypeId,
426    role: ContextAttachmentRole,
427    state: Cell<AttachmentState>,
428    attachment: RefCell<Option<Rc<dyn ContextAttachment>>>,
429    roles: Rc<AttachmentRoleState>,
430}
431
432impl AttachmentControl {
433    fn detach(&self) -> Result<bool, ContextAttachmentDetachError> {
434        match self.state.get() {
435            AttachmentState::Active => {}
436            AttachmentState::ReleasePrepared => {
437                return Err(ContextAttachmentDetachError::ReleaseInProgress);
438            }
439            AttachmentState::Teardown | AttachmentState::Complete | AttachmentState::Detached => {
440                return Ok(false);
441            }
442        }
443        if self.role == ContextAttachmentRole::Platform && self.roles.renderer_active.get() {
444            return Err(ContextAttachmentDetachError::RendererActive);
445        }
446        self.state.set(AttachmentState::Detached);
447        if self.role == ContextAttachmentRole::Renderer {
448            self.roles.renderer_active.set(false);
449        }
450        let attachment = self.attachment.borrow_mut().take();
451        drop(attachment);
452        Ok(true)
453    }
454
455    fn prepare_platform_release(&self) {
456        debug_assert_eq!(self.role, ContextAttachmentRole::Platform);
457        debug_assert_eq!(self.state.get(), AttachmentState::Active);
458        self.state.set(AttachmentState::ReleasePrepared);
459    }
460
461    fn abandon_platform_release(&self) {
462        if self.state.get() == AttachmentState::ReleasePrepared {
463            self.state.set(AttachmentState::Active);
464        }
465    }
466
467    fn commit_platform_release(&self) -> Option<Rc<dyn ContextAttachment>> {
468        debug_assert_eq!(self.role, ContextAttachmentRole::Platform);
469        debug_assert_eq!(self.state.get(), AttachmentState::ReleasePrepared);
470        debug_assert!(!self.roles.renderer_active.get());
471        self.state.set(AttachmentState::Detached);
472        self.attachment.borrow_mut().take()
473    }
474}
475
476impl fmt::Debug for AttachmentControl {
477    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478        f.debug_struct("AttachmentControl")
479            .field("marker", &self.marker)
480            .field("role", &self.role)
481            .field("state", &self.state.get())
482            .finish_non_exhaustive()
483    }
484}
485
486/// Lease that unregisters an attachment when explicitly detached or dropped.
487///
488/// A platform attachment with an active renderer dependency remains Context-owned instead of
489/// detaching out of order.
490#[derive(Debug)]
491#[must_use = "retain the lease for explicit detach, or defer cleanup to Context teardown"]
492pub struct ContextAttachmentLease {
493    control: Weak<AttachmentControl>,
494    _not_send_or_sync: PhantomData<Rc<()>>,
495}
496
497impl ContextAttachmentLease {
498    /// Returns a non-owning identity for this exact attachment generation.
499    ///
500    /// Backends may retain the handle in shared runtime state. Unlike the lease, dropping a handle
501    /// never detaches the attachment.
502    pub fn handle(&self) -> ContextAttachmentHandle {
503        ContextAttachmentHandle {
504            control: self.control.clone(),
505            _not_send_or_sync: PhantomData,
506        }
507    }
508
509    /// Detaches the attachment if it is still active and has no dependency blocking release.
510    ///
511    /// `Ok(true)` reports the transition from attached to detached. `Ok(false)` means Context
512    /// teardown or an earlier release already claimed the attachment. An error leaves the lease
513    /// attached: platform backends must not destroy native state after such a failure. Explicit
514    /// platform shutdown should use [`Context::prepare_platform_attachment_release`] so native
515    /// teardown and lease release share one transaction.
516    pub fn detach(&mut self) -> Result<bool, ContextAttachmentDetachError> {
517        self.control
518            .upgrade()
519            .map_or(Ok(false), |control| control.detach())
520    }
521
522    /// Returns whether the attachment is still active.
523    pub fn is_attached(&self) -> bool {
524        self.control.upgrade().is_some_and(|control| {
525            matches!(
526                control.state.get(),
527                AttachmentState::Active | AttachmentState::ReleasePrepared
528            )
529        })
530    }
531
532    /// Leave the attachment under Context ownership until Context teardown.
533    ///
534    /// Backend owners use this when their own `Drop` implementation cannot safely enter native
535    /// teardown without an explicit mutable Context. The Context retains the attachment and runs
536    /// its normal phased teardown before destroying the native Context.
537    pub fn defer_to_context(mut self) {
538        self.control = Weak::new();
539    }
540}
541
542impl Drop for ContextAttachmentLease {
543    fn drop(&mut self) {
544        let _ = self.detach();
545    }
546}
547
548/// Non-owning identity for one exact Context attachment generation.
549///
550/// Handles are cloneable so related runtime objects can prove which platform attachment they use.
551/// They cannot detach the attachment and do not keep it alive after the Context releases it.
552#[derive(Clone, Debug)]
553pub struct ContextAttachmentHandle {
554    control: Weak<AttachmentControl>,
555    _not_send_or_sync: PhantomData<Rc<()>>,
556}
557
558impl ContextAttachmentHandle {
559    /// Returns whether this attachment generation remains active or reserved by a release permit.
560    pub fn is_attached(&self) -> bool {
561        self.control.upgrade().is_some_and(|control| {
562            matches!(
563                control.state.get(),
564                AttachmentState::Active | AttachmentState::ReleasePrepared
565            )
566        })
567    }
568
569    /// Returns whether an active renderer attachment still depends on this platform generation.
570    ///
571    /// This is a conservative Drop-path diagnostic. Explicit platform shutdown must use
572    /// [`Context::prepare_platform_attachment_release`] so the check and native cleanup share one
573    /// exclusive transaction.
574    pub fn has_active_renderer_dependency(&self) -> bool {
575        self.control.upgrade().is_some_and(|control| {
576            control.role == ContextAttachmentRole::Platform
577                && matches!(
578                    control.state.get(),
579                    AttachmentState::Active | AttachmentState::ReleasePrepared
580                )
581                && control.roles.renderer_active.get()
582        })
583    }
584}
585
586/// Exclusive permit for an explicit platform attachment release transaction.
587///
588/// Preparing the permit proves that no renderer attachment still depends on the exact platform
589/// generation. Use [`Self::context_mut`] for any frame normalization or native cleanup, then call
590/// [`Self::commit`] only after the platform attachment has been fully released. Dropping an
591/// uncommitted permit restores the attachment to its active state so shutdown can be retried.
592/// Renderer registration remains unavailable while the permit reserves the platform generation.
593#[must_use = "dropping the permit abandons platform detachment and keeps the attachment active"]
594pub struct ContextPlatformAttachmentRelease<'a> {
595    context: &'a mut Context,
596    control: Rc<AttachmentControl>,
597    committed: bool,
598}
599
600impl fmt::Debug for ContextPlatformAttachmentRelease<'_> {
601    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
602        formatter
603            .debug_struct("ContextPlatformAttachmentRelease")
604            .field("attachment", &self.control)
605            .field("committed", &self.committed)
606            .finish_non_exhaustive()
607    }
608}
609
610impl<'a> ContextPlatformAttachmentRelease<'a> {
611    pub(super) fn new(context: &'a mut Context, control: Rc<AttachmentControl>) -> Self {
612        Self {
613            context,
614            control,
615            committed: false,
616        }
617    }
618
619    /// Returns the exclusively borrowed Context after platform-release preflight succeeded.
620    pub fn context_mut(&mut self) -> &mut Context {
621        self.context
622    }
623
624    /// Commits detachment of the exact platform attachment generation.
625    ///
626    /// The state transition is infallible because the permit exclusively borrows the Context and
627    /// keeps the attachment control reserved against ordinary lease detachment. If the final
628    /// user-owned attachment destructor panics, the committed detached state is retained while
629    /// that panic resumes.
630    pub fn commit(mut self) {
631        let attachment = self.control.commit_platform_release();
632        self.committed = true;
633        drop(attachment);
634    }
635}
636
637impl Drop for ContextPlatformAttachmentRelease<'_> {
638    fn drop(&mut self) {
639        if !self.committed {
640            self.control.abandon_platform_release();
641        }
642    }
643}
644
645#[derive(Default)]
646pub(super) struct AttachmentRegistry {
647    controls: Vec<Rc<AttachmentControl>>,
648    roles: Rc<AttachmentRoleState>,
649    tearing_down: bool,
650    platform_window_teardown_active: Cell<bool>,
651}
652
653impl fmt::Debug for AttachmentRegistry {
654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655        f.debug_struct("AttachmentRegistry")
656            .field("controls", &self.controls)
657            .field("tearing_down", &self.tearing_down)
658            .field(
659                "platform_window_teardown_active",
660                &self.platform_window_teardown_active.get(),
661            )
662            .finish()
663    }
664}
665
666impl AttachmentRegistry {
667    pub(super) fn preflight_register<Marker: 'static>(
668        &self,
669        lifecycle: ContextLifecycle,
670        role: ContextAttachmentRole,
671    ) -> Result<(), ContextAttachmentError> {
672        if lifecycle != ContextLifecycle::Alive || self.tearing_down {
673            return Err(ContextAttachmentError::ContextDropping);
674        }
675
676        let marker = TypeId::of::<Marker>();
677        if self.controls.iter().any(|control| {
678            control.marker == marker && control.state.get() != AttachmentState::Detached
679        }) {
680            return Err(ContextAttachmentError::DuplicateAttachment);
681        }
682        if role == ContextAttachmentRole::Renderer
683            && !self.role_is_operational(ContextAttachmentRole::Platform)
684        {
685            return Err(ContextAttachmentError::MissingPlatform);
686        }
687        if role != ContextAttachmentRole::Extension && self.role_is_active(role) {
688            return Err(ContextAttachmentError::RoleOccupied(role));
689        }
690        Ok(())
691    }
692
693    pub(super) fn register<Marker: 'static>(
694        &mut self,
695        lifecycle: ContextLifecycle,
696        role: ContextAttachmentRole,
697        attachment: Rc<dyn ContextAttachment>,
698    ) -> Result<ContextAttachmentLease, ContextAttachmentError> {
699        self.preflight_register::<Marker>(lifecycle, role)?;
700        self.controls
701            .retain(|control| control.state.get() != AttachmentState::Detached);
702        let marker = TypeId::of::<Marker>();
703
704        let control = Rc::new(AttachmentControl {
705            marker,
706            role,
707            state: Cell::new(AttachmentState::Active),
708            attachment: RefCell::new(Some(attachment)),
709            roles: Rc::clone(&self.roles),
710        });
711        if role == ContextAttachmentRole::Renderer {
712            self.roles.renderer_active.set(true);
713        }
714        let lease = ContextAttachmentLease {
715            control: Rc::downgrade(&control),
716            _not_send_or_sync: PhantomData,
717        };
718        self.controls.push(control);
719        Ok(lease)
720    }
721
722    fn role_is_active(&self, role: ContextAttachmentRole) -> bool {
723        self.controls.iter().any(|control| {
724            control.role == role
725                && matches!(
726                    control.state.get(),
727                    AttachmentState::Active | AttachmentState::ReleasePrepared
728                )
729        })
730    }
731
732    fn role_is_operational(&self, role: ContextAttachmentRole) -> bool {
733        self.controls
734            .iter()
735            .any(|control| control.role == role && control.state.get() == AttachmentState::Active)
736    }
737
738    pub(super) fn prepare_platform_release(
739        &self,
740        handle: &ContextAttachmentHandle,
741    ) -> Result<Rc<AttachmentControl>, ContextPlatformAttachmentReleaseError> {
742        if self.tearing_down {
743            return Err(ContextPlatformAttachmentReleaseError::ContextDropping);
744        }
745        let control = handle
746            .control
747            .upgrade()
748            .ok_or(ContextPlatformAttachmentReleaseError::AttachmentInactive)?;
749        if control.role != ContextAttachmentRole::Platform {
750            return Err(ContextPlatformAttachmentReleaseError::NotPlatform);
751        }
752        match control.state.get() {
753            AttachmentState::Active => {}
754            AttachmentState::ReleasePrepared => {
755                return Err(ContextPlatformAttachmentReleaseError::ReleaseInProgress);
756            }
757            AttachmentState::Teardown | AttachmentState::Complete | AttachmentState::Detached => {
758                return Err(ContextPlatformAttachmentReleaseError::AttachmentInactive);
759            }
760        }
761        let owns_active_generation = self.controls.iter().any(|candidate| {
762            Rc::ptr_eq(candidate, &control)
763                && candidate.role == ContextAttachmentRole::Platform
764                && candidate.state.get() == AttachmentState::Active
765        });
766        if !owns_active_generation {
767            return Err(ContextPlatformAttachmentReleaseError::PlatformGenerationMismatch);
768        }
769        if self.roles.renderer_active.get() {
770            return Err(ContextPlatformAttachmentReleaseError::RendererActive);
771        }
772        control.prepare_platform_release();
773        Ok(control)
774    }
775
776    #[cfg(feature = "multi-viewport")]
777    pub(super) fn begin_platform_window_teardown(
778        &self,
779        context: &ContextPlatformWindowTeardown<'_>,
780    ) -> Result<PlatformWindowTeardownInvocation<'_>, ContextPlatformWindowTeardownError> {
781        if self.tearing_down {
782            return Err(ContextPlatformWindowTeardownError::ContextDropping);
783        }
784        if self.platform_window_teardown_active.get() {
785            return Err(ContextPlatformWindowTeardownError::Reentrant);
786        }
787        self.platform_window_teardown_active.set(true);
788        let invocation = PlatformWindowTeardownInvocation {
789            attachment: self
790                .controls
791                .iter()
792                .find(|control| {
793                    control.role == ContextAttachmentRole::Platform
794                        && matches!(
795                            control.state.get(),
796                            AttachmentState::Active | AttachmentState::ReleasePrepared
797                        )
798                })
799                .and_then(|control| control.attachment.borrow().clone()),
800            active: &self.platform_window_teardown_active,
801        };
802        invocation.begin(context)?;
803        Ok(invocation)
804    }
805
806    pub(super) fn begin_teardown(&mut self) -> Vec<Rc<AttachmentControl>> {
807        self.tearing_down = true;
808        let controls = std::mem::take(&mut self.controls);
809        controls
810            .into_iter()
811            .filter(|control| {
812                if !matches!(
813                    control.state.get(),
814                    AttachmentState::Active | AttachmentState::ReleasePrepared
815                ) {
816                    return false;
817                }
818                control.state.set(AttachmentState::Teardown);
819                true
820            })
821            .collect()
822    }
823}
824
825#[cfg(feature = "multi-viewport")]
826pub(super) struct PlatformWindowTeardownInvocation<'a> {
827    attachment: Option<Rc<dyn ContextAttachment>>,
828    active: &'a Cell<bool>,
829}
830
831#[cfg(feature = "multi-viewport")]
832impl PlatformWindowTeardownInvocation<'_> {
833    fn begin(
834        &self,
835        context: &ContextPlatformWindowTeardown<'_>,
836    ) -> Result<(), ContextPlatformWindowTeardownError> {
837        let Some(attachment) = &self.attachment else {
838            return Ok(());
839        };
840        match catch_unwind(AssertUnwindSafe(|| {
841            attachment.begin_platform_window_teardown(context)
842        })) {
843            Ok(Ok(())) => Ok(()),
844            Ok(Err(error)) => Err(ContextPlatformWindowTeardownError::AttachmentPreflight(
845                error,
846            )),
847            Err(payload) => {
848                // A panic payload may panic when dropped. The transaction is rejected before any
849                // native teardown begins, so retaining it is preferable to a nested unwind.
850                std::mem::forget(payload);
851                Err(ContextPlatformWindowTeardownError::BeginPanicked)
852            }
853        }
854    }
855
856    pub(super) fn finish(
857        self,
858        context: &ContextPlatformWindowTeardown<'_>,
859    ) -> Result<(), ContextPlatformWindowTeardownError> {
860        let Some(attachment) = &self.attachment else {
861            return Ok(());
862        };
863        match catch_unwind(AssertUnwindSafe(|| {
864            attachment.end_platform_window_teardown(context)
865        })) {
866            Ok(Ok(())) => Ok(()),
867            Ok(Err(error)) => Err(ContextPlatformWindowTeardownError::AttachmentPostflight(
868                error,
869            )),
870            Err(payload) => {
871                // Native teardown completed, but the caller still receives a recoverable Rust
872                // error rather than unwinding through an FFI boundary.
873                std::mem::forget(payload);
874                Err(ContextPlatformWindowTeardownError::EndPanicked)
875            }
876        }
877    }
878}
879
880#[cfg(feature = "multi-viewport")]
881impl Drop for PlatformWindowTeardownInvocation<'_> {
882    fn drop(&mut self) {
883        self.active.set(false);
884    }
885}
886
887pub(super) fn run_pre_destroy_phase(
888    controls: &[Rc<AttachmentControl>],
889    owner: &mut Context,
890    phase: ContextAttachmentPhase,
891) -> bool {
892    let context = ContextTeardown::new(owner, phase);
893    let mut completed = true;
894    for control in controls {
895        let Some(attachment) = control.attachment.borrow().clone() else {
896            continue;
897        };
898        let result = catch_unwind(AssertUnwindSafe(|| match phase {
899            ContextAttachmentPhase::Quiesce => attachment.quiesce(&context),
900            ContextAttachmentPhase::RendererResources => {
901                attachment.release_renderer_resources(&context)
902            }
903            ContextAttachmentPhase::PlatformWindows => {
904                attachment.release_platform_windows(&context)
905            }
906        }));
907        match result {
908            Ok(Ok(())) => {}
909            Ok(Err(error)) => {
910                completed = false;
911                std::mem::forget(error);
912            }
913            Err(payload) => {
914                completed = false;
915                // A panic payload may itself panic when dropped. Context teardown is now
916                // fail-stop, so retain it until the caller aborts instead of risking a nested
917                // unwind mid-phase.
918                std::mem::forget(payload);
919            }
920        }
921    }
922    completed
923}
924
925pub(super) fn run_post_destroy(
926    controls: Vec<Rc<AttachmentControl>>,
927    context_id: ContextId,
928) -> bool {
929    let context = ContextDestroyed { id: context_id };
930    let mut completed = true;
931    for control in controls {
932        if let Some(attachment) = control.attachment.borrow().clone() {
933            if let Err(payload) =
934                catch_unwind(AssertUnwindSafe(|| attachment.context_destroyed(context)))
935            {
936                completed = false;
937                std::mem::forget(payload);
938            }
939        }
940        control.state.set(AttachmentState::Complete);
941        let attachment = control.attachment.borrow_mut().take();
942        if let Err(payload) = catch_unwind(AssertUnwindSafe(move || drop(attachment))) {
943            completed = false;
944            std::mem::forget(payload);
945        }
946    }
947    completed
948}