use std::any::TypeId;
use std::cell::{Cell, RefCell};
use std::fmt;
use std::marker::PhantomData;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::ptr::NonNull;
use std::rc::{Rc, Weak};
use thiserror::Error;
use crate::render::RendererConsumerCapability;
use super::binding::{self, ContextId, ContextLifecycle, ContextState};
use super::core::Context;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ContextAttachmentPhase {
Quiesce,
RendererResources,
PlatformWindows,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ContextAttachmentRole {
Extension,
Renderer,
Platform,
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("{message}")]
pub struct ContextAttachmentTeardownError {
message: String,
}
impl ContextAttachmentTeardownError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum ContextPlatformWindowTeardownError {
#[error("Dear ImGui context teardown is in progress")]
ContextDropping,
#[error("platform-window teardown cannot be reentered")]
Reentrant,
#[error("platform attachment rejected platform-window teardown: {0}")]
AttachmentPreflight(#[source] ContextAttachmentTeardownError),
#[error("platform attachment could not complete platform-window teardown: {0}")]
AttachmentPostflight(#[source] ContextAttachmentTeardownError),
#[error("platform attachment panicked before platform-window teardown")]
BeginPanicked,
#[error("platform attachment panicked after platform-window teardown")]
EndPanicked,
}
pub trait ContextAttachment {
fn begin_platform_window_teardown(
&self,
_context: &ContextPlatformWindowTeardown<'_>,
) -> Result<(), ContextAttachmentTeardownError> {
Ok(())
}
fn end_platform_window_teardown(
&self,
_context: &ContextPlatformWindowTeardown<'_>,
) -> Result<(), ContextAttachmentTeardownError> {
Ok(())
}
fn quiesce(
&self,
_context: &ContextTeardown<'_>,
) -> Result<(), ContextAttachmentTeardownError> {
Ok(())
}
fn release_renderer_resources(
&self,
_context: &ContextTeardown<'_>,
) -> Result<(), ContextAttachmentTeardownError> {
Ok(())
}
fn release_platform_windows(
&self,
_context: &ContextTeardown<'_>,
) -> Result<(), ContextAttachmentTeardownError> {
Ok(())
}
fn context_destroyed(&self, _context: ContextDestroyed) {}
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum ContextAttachmentError {
#[error("an attachment with this marker type is already registered")]
DuplicateAttachment,
#[error("the {0:?} attachment role is already occupied")]
RoleOccupied(ContextAttachmentRole),
#[error("a renderer attachment requires an active platform attachment")]
MissingPlatform,
#[error("Dear ImGui context teardown has already started")]
ContextDropping,
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum ContextAttachmentDetachError {
#[error("platform attachment release is already in progress")]
ReleaseInProgress,
#[error("the platform attachment cannot be detached while a renderer attachment is active")]
RendererActive,
}
#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum ContextPlatformAttachmentReleaseError {
#[error("Dear ImGui context teardown is in progress")]
ContextDropping,
#[error("the platform attachment generation is no longer active")]
AttachmentInactive,
#[error("the supplied attachment does not own the platform role")]
NotPlatform,
#[error("the supplied attachment is not the active platform generation for this Context")]
PlatformGenerationMismatch,
#[error("platform attachment release is already in progress")]
ReleaseInProgress,
#[error("the platform attachment cannot be released while a renderer attachment is active")]
RendererActive,
}
pub struct ContextTeardown<'a> {
owner: NonNull<Context>,
phase: ContextAttachmentPhase,
renderer_texture_reset_active: Cell<bool>,
_exclusive_owner: PhantomData<&'a mut Context>,
}
impl fmt::Debug for ContextTeardown<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ContextTeardown")
.field("id", &self.id())
.field("phase", &self.phase)
.finish_non_exhaustive()
}
}
impl ContextTeardown<'_> {
fn new<'owner>(
owner: &'owner mut Context,
phase: ContextAttachmentPhase,
) -> ContextTeardown<'owner> {
ContextTeardown {
owner: NonNull::from(owner),
phase,
renderer_texture_reset_active: Cell::new(false),
_exclusive_owner: PhantomData,
}
}
fn state(&self) -> &ContextState {
unsafe { self.owner.as_ref().state.as_ref() }
}
pub fn id(&self) -> ContextId {
self.state().id()
}
pub fn phase(&self) -> ContextAttachmentPhase {
self.phase
}
pub fn with_bound_context<R>(&self, f: impl FnOnce() -> R) -> R {
assert_eq!(
self.state().lifecycle(),
ContextLifecycle::Dropping,
"ContextTeardown used outside pre-destroy teardown"
);
let raw = self.state().raw_during_teardown();
assert!(
!raw.is_null(),
"ContextTeardown used after native Context destruction"
);
binding::with_bound_context(raw, f)
}
pub fn with_renderer_texture_reset(
&self,
consumer: &impl RendererConsumerCapability,
release: impl FnOnce() -> Result<(), ContextAttachmentTeardownError>,
) -> Result<(), ContextAttachmentTeardownError> {
if self.phase != ContextAttachmentPhase::RendererResources {
return Err(ContextAttachmentTeardownError::new(format!(
"renderer texture reset requires the RendererResources phase, not {:?}",
self.phase
)));
}
if self.state().lifecycle() != ContextLifecycle::Dropping {
return Err(ContextAttachmentTeardownError::new(
"renderer texture reset requires active Context teardown",
));
}
if self.renderer_texture_reset_active.replace(true) {
return Err(ContextAttachmentTeardownError::new(
"renderer texture reset cannot be reentered",
));
}
let _active = RendererTextureResetInvocation {
active: &self.renderer_texture_reset_active,
};
let watermark = unsafe { &mut *self.owner.as_ptr() }
.prepare_renderer_texture_reset_during_teardown(consumer)
.map_err(|error| {
ContextAttachmentTeardownError::new(format!(
"renderer texture reset preflight failed: {error}"
))
})?;
release()?;
unsafe { &mut *self.owner.as_ptr() }
.commit_renderer_texture_reset_during_teardown(watermark);
Ok(())
}
#[cfg(test)]
pub(super) fn as_raw_for_test(&self) -> *mut crate::sys::ImGuiContext {
self.state().raw_during_teardown()
}
}
pub struct ContextPlatformWindowTeardown<'a> {
state: &'a ContextState,
_exclusive_owner: PhantomData<&'a mut Context>,
}
impl fmt::Debug for ContextPlatformWindowTeardown<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ContextPlatformWindowTeardown")
.field("id", &self.id())
.finish_non_exhaustive()
}
}
impl<'a> ContextPlatformWindowTeardown<'a> {
#[cfg(feature = "multi-viewport")]
pub(super) fn new(state: &'a ContextState) -> Self {
Self {
state,
_exclusive_owner: PhantomData,
}
}
pub fn id(&self) -> ContextId {
self.state.id()
}
pub fn with_bound_context<R>(&self, f: impl FnOnce() -> R) -> R {
assert_eq!(
self.state.lifecycle(),
ContextLifecycle::Alive,
"ContextPlatformWindowTeardown used outside a live Context"
);
let raw = self.state.raw_during_teardown();
assert!(
!raw.is_null(),
"ContextPlatformWindowTeardown used after native Context destruction"
);
binding::with_bound_context(raw, f)
}
}
struct RendererTextureResetInvocation<'a> {
active: &'a Cell<bool>,
}
impl Drop for RendererTextureResetInvocation<'_> {
fn drop(&mut self) {
self.active.set(false);
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ContextDestroyed {
id: ContextId,
}
impl ContextDestroyed {
pub fn id(self) -> ContextId {
self.id
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum AttachmentState {
Active,
ReleasePrepared,
Teardown,
Complete,
Detached,
}
#[derive(Default)]
struct AttachmentRoleState {
renderer_active: Cell<bool>,
}
pub(super) struct AttachmentControl {
marker: TypeId,
role: ContextAttachmentRole,
state: Cell<AttachmentState>,
attachment: RefCell<Option<Rc<dyn ContextAttachment>>>,
roles: Rc<AttachmentRoleState>,
}
impl AttachmentControl {
fn detach(&self) -> Result<bool, ContextAttachmentDetachError> {
match self.state.get() {
AttachmentState::Active => {}
AttachmentState::ReleasePrepared => {
return Err(ContextAttachmentDetachError::ReleaseInProgress);
}
AttachmentState::Teardown | AttachmentState::Complete | AttachmentState::Detached => {
return Ok(false);
}
}
if self.role == ContextAttachmentRole::Platform && self.roles.renderer_active.get() {
return Err(ContextAttachmentDetachError::RendererActive);
}
self.state.set(AttachmentState::Detached);
if self.role == ContextAttachmentRole::Renderer {
self.roles.renderer_active.set(false);
}
let attachment = self.attachment.borrow_mut().take();
drop(attachment);
Ok(true)
}
fn prepare_platform_release(&self) {
debug_assert_eq!(self.role, ContextAttachmentRole::Platform);
debug_assert_eq!(self.state.get(), AttachmentState::Active);
self.state.set(AttachmentState::ReleasePrepared);
}
fn abandon_platform_release(&self) {
if self.state.get() == AttachmentState::ReleasePrepared {
self.state.set(AttachmentState::Active);
}
}
fn commit_platform_release(&self) -> Option<Rc<dyn ContextAttachment>> {
debug_assert_eq!(self.role, ContextAttachmentRole::Platform);
debug_assert_eq!(self.state.get(), AttachmentState::ReleasePrepared);
debug_assert!(!self.roles.renderer_active.get());
self.state.set(AttachmentState::Detached);
self.attachment.borrow_mut().take()
}
}
impl fmt::Debug for AttachmentControl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AttachmentControl")
.field("marker", &self.marker)
.field("role", &self.role)
.field("state", &self.state.get())
.finish_non_exhaustive()
}
}
#[derive(Debug)]
#[must_use = "retain the lease for explicit detach, or defer cleanup to Context teardown"]
pub struct ContextAttachmentLease {
control: Weak<AttachmentControl>,
_not_send_or_sync: PhantomData<Rc<()>>,
}
impl ContextAttachmentLease {
pub fn handle(&self) -> ContextAttachmentHandle {
ContextAttachmentHandle {
control: self.control.clone(),
_not_send_or_sync: PhantomData,
}
}
pub fn detach(&mut self) -> Result<bool, ContextAttachmentDetachError> {
self.control
.upgrade()
.map_or(Ok(false), |control| control.detach())
}
pub fn is_attached(&self) -> bool {
self.control.upgrade().is_some_and(|control| {
matches!(
control.state.get(),
AttachmentState::Active | AttachmentState::ReleasePrepared
)
})
}
pub fn defer_to_context(mut self) {
self.control = Weak::new();
}
}
impl Drop for ContextAttachmentLease {
fn drop(&mut self) {
let _ = self.detach();
}
}
#[derive(Clone, Debug)]
pub struct ContextAttachmentHandle {
control: Weak<AttachmentControl>,
_not_send_or_sync: PhantomData<Rc<()>>,
}
impl ContextAttachmentHandle {
pub fn is_attached(&self) -> bool {
self.control.upgrade().is_some_and(|control| {
matches!(
control.state.get(),
AttachmentState::Active | AttachmentState::ReleasePrepared
)
})
}
pub fn has_active_renderer_dependency(&self) -> bool {
self.control.upgrade().is_some_and(|control| {
control.role == ContextAttachmentRole::Platform
&& matches!(
control.state.get(),
AttachmentState::Active | AttachmentState::ReleasePrepared
)
&& control.roles.renderer_active.get()
})
}
}
#[must_use = "dropping the permit abandons platform detachment and keeps the attachment active"]
pub struct ContextPlatformAttachmentRelease<'a> {
context: &'a mut Context,
control: Rc<AttachmentControl>,
committed: bool,
}
impl fmt::Debug for ContextPlatformAttachmentRelease<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ContextPlatformAttachmentRelease")
.field("attachment", &self.control)
.field("committed", &self.committed)
.finish_non_exhaustive()
}
}
impl<'a> ContextPlatformAttachmentRelease<'a> {
pub(super) fn new(context: &'a mut Context, control: Rc<AttachmentControl>) -> Self {
Self {
context,
control,
committed: false,
}
}
pub fn context_mut(&mut self) -> &mut Context {
self.context
}
pub fn commit(mut self) {
let attachment = self.control.commit_platform_release();
self.committed = true;
drop(attachment);
}
}
impl Drop for ContextPlatformAttachmentRelease<'_> {
fn drop(&mut self) {
if !self.committed {
self.control.abandon_platform_release();
}
}
}
#[derive(Default)]
pub(super) struct AttachmentRegistry {
controls: Vec<Rc<AttachmentControl>>,
roles: Rc<AttachmentRoleState>,
tearing_down: bool,
platform_window_teardown_active: Cell<bool>,
}
impl fmt::Debug for AttachmentRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AttachmentRegistry")
.field("controls", &self.controls)
.field("tearing_down", &self.tearing_down)
.field(
"platform_window_teardown_active",
&self.platform_window_teardown_active.get(),
)
.finish()
}
}
impl AttachmentRegistry {
pub(super) fn preflight_register<Marker: 'static>(
&self,
lifecycle: ContextLifecycle,
role: ContextAttachmentRole,
) -> Result<(), ContextAttachmentError> {
if lifecycle != ContextLifecycle::Alive || self.tearing_down {
return Err(ContextAttachmentError::ContextDropping);
}
let marker = TypeId::of::<Marker>();
if self.controls.iter().any(|control| {
control.marker == marker && control.state.get() != AttachmentState::Detached
}) {
return Err(ContextAttachmentError::DuplicateAttachment);
}
if role == ContextAttachmentRole::Renderer
&& !self.role_is_operational(ContextAttachmentRole::Platform)
{
return Err(ContextAttachmentError::MissingPlatform);
}
if role != ContextAttachmentRole::Extension && self.role_is_active(role) {
return Err(ContextAttachmentError::RoleOccupied(role));
}
Ok(())
}
pub(super) fn register<Marker: 'static>(
&mut self,
lifecycle: ContextLifecycle,
role: ContextAttachmentRole,
attachment: Rc<dyn ContextAttachment>,
) -> Result<ContextAttachmentLease, ContextAttachmentError> {
self.preflight_register::<Marker>(lifecycle, role)?;
self.controls
.retain(|control| control.state.get() != AttachmentState::Detached);
let marker = TypeId::of::<Marker>();
let control = Rc::new(AttachmentControl {
marker,
role,
state: Cell::new(AttachmentState::Active),
attachment: RefCell::new(Some(attachment)),
roles: Rc::clone(&self.roles),
});
if role == ContextAttachmentRole::Renderer {
self.roles.renderer_active.set(true);
}
let lease = ContextAttachmentLease {
control: Rc::downgrade(&control),
_not_send_or_sync: PhantomData,
};
self.controls.push(control);
Ok(lease)
}
fn role_is_active(&self, role: ContextAttachmentRole) -> bool {
self.controls.iter().any(|control| {
control.role == role
&& matches!(
control.state.get(),
AttachmentState::Active | AttachmentState::ReleasePrepared
)
})
}
fn role_is_operational(&self, role: ContextAttachmentRole) -> bool {
self.controls
.iter()
.any(|control| control.role == role && control.state.get() == AttachmentState::Active)
}
pub(super) fn prepare_platform_release(
&self,
handle: &ContextAttachmentHandle,
) -> Result<Rc<AttachmentControl>, ContextPlatformAttachmentReleaseError> {
if self.tearing_down {
return Err(ContextPlatformAttachmentReleaseError::ContextDropping);
}
let control = handle
.control
.upgrade()
.ok_or(ContextPlatformAttachmentReleaseError::AttachmentInactive)?;
if control.role != ContextAttachmentRole::Platform {
return Err(ContextPlatformAttachmentReleaseError::NotPlatform);
}
match control.state.get() {
AttachmentState::Active => {}
AttachmentState::ReleasePrepared => {
return Err(ContextPlatformAttachmentReleaseError::ReleaseInProgress);
}
AttachmentState::Teardown | AttachmentState::Complete | AttachmentState::Detached => {
return Err(ContextPlatformAttachmentReleaseError::AttachmentInactive);
}
}
let owns_active_generation = self.controls.iter().any(|candidate| {
Rc::ptr_eq(candidate, &control)
&& candidate.role == ContextAttachmentRole::Platform
&& candidate.state.get() == AttachmentState::Active
});
if !owns_active_generation {
return Err(ContextPlatformAttachmentReleaseError::PlatformGenerationMismatch);
}
if self.roles.renderer_active.get() {
return Err(ContextPlatformAttachmentReleaseError::RendererActive);
}
control.prepare_platform_release();
Ok(control)
}
#[cfg(feature = "multi-viewport")]
pub(super) fn begin_platform_window_teardown(
&self,
context: &ContextPlatformWindowTeardown<'_>,
) -> Result<PlatformWindowTeardownInvocation<'_>, ContextPlatformWindowTeardownError> {
if self.tearing_down {
return Err(ContextPlatformWindowTeardownError::ContextDropping);
}
if self.platform_window_teardown_active.get() {
return Err(ContextPlatformWindowTeardownError::Reentrant);
}
self.platform_window_teardown_active.set(true);
let invocation = PlatformWindowTeardownInvocation {
attachment: self
.controls
.iter()
.find(|control| {
control.role == ContextAttachmentRole::Platform
&& matches!(
control.state.get(),
AttachmentState::Active | AttachmentState::ReleasePrepared
)
})
.and_then(|control| control.attachment.borrow().clone()),
active: &self.platform_window_teardown_active,
};
invocation.begin(context)?;
Ok(invocation)
}
pub(super) fn begin_teardown(&mut self) -> Vec<Rc<AttachmentControl>> {
self.tearing_down = true;
let controls = std::mem::take(&mut self.controls);
controls
.into_iter()
.filter(|control| {
if !matches!(
control.state.get(),
AttachmentState::Active | AttachmentState::ReleasePrepared
) {
return false;
}
control.state.set(AttachmentState::Teardown);
true
})
.collect()
}
}
#[cfg(feature = "multi-viewport")]
pub(super) struct PlatformWindowTeardownInvocation<'a> {
attachment: Option<Rc<dyn ContextAttachment>>,
active: &'a Cell<bool>,
}
#[cfg(feature = "multi-viewport")]
impl PlatformWindowTeardownInvocation<'_> {
fn begin(
&self,
context: &ContextPlatformWindowTeardown<'_>,
) -> Result<(), ContextPlatformWindowTeardownError> {
let Some(attachment) = &self.attachment else {
return Ok(());
};
match catch_unwind(AssertUnwindSafe(|| {
attachment.begin_platform_window_teardown(context)
})) {
Ok(Ok(())) => Ok(()),
Ok(Err(error)) => Err(ContextPlatformWindowTeardownError::AttachmentPreflight(
error,
)),
Err(payload) => {
std::mem::forget(payload);
Err(ContextPlatformWindowTeardownError::BeginPanicked)
}
}
}
pub(super) fn finish(
self,
context: &ContextPlatformWindowTeardown<'_>,
) -> Result<(), ContextPlatformWindowTeardownError> {
let Some(attachment) = &self.attachment else {
return Ok(());
};
match catch_unwind(AssertUnwindSafe(|| {
attachment.end_platform_window_teardown(context)
})) {
Ok(Ok(())) => Ok(()),
Ok(Err(error)) => Err(ContextPlatformWindowTeardownError::AttachmentPostflight(
error,
)),
Err(payload) => {
std::mem::forget(payload);
Err(ContextPlatformWindowTeardownError::EndPanicked)
}
}
}
}
#[cfg(feature = "multi-viewport")]
impl Drop for PlatformWindowTeardownInvocation<'_> {
fn drop(&mut self) {
self.active.set(false);
}
}
pub(super) fn run_pre_destroy_phase(
controls: &[Rc<AttachmentControl>],
owner: &mut Context,
phase: ContextAttachmentPhase,
) -> bool {
let context = ContextTeardown::new(owner, phase);
let mut completed = true;
for control in controls {
let Some(attachment) = control.attachment.borrow().clone() else {
continue;
};
let result = catch_unwind(AssertUnwindSafe(|| match phase {
ContextAttachmentPhase::Quiesce => attachment.quiesce(&context),
ContextAttachmentPhase::RendererResources => {
attachment.release_renderer_resources(&context)
}
ContextAttachmentPhase::PlatformWindows => {
attachment.release_platform_windows(&context)
}
}));
match result {
Ok(Ok(())) => {}
Ok(Err(error)) => {
completed = false;
std::mem::forget(error);
}
Err(payload) => {
completed = false;
std::mem::forget(payload);
}
}
}
completed
}
pub(super) fn run_post_destroy(
controls: Vec<Rc<AttachmentControl>>,
context_id: ContextId,
) -> bool {
let context = ContextDestroyed { id: context_id };
let mut completed = true;
for control in controls {
if let Some(attachment) = control.attachment.borrow().clone() {
if let Err(payload) =
catch_unwind(AssertUnwindSafe(|| attachment.context_destroyed(context)))
{
completed = false;
std::mem::forget(payload);
}
}
control.state.set(AttachmentState::Complete);
let attachment = control.attachment.borrow_mut().take();
if let Err(payload) = catch_unwind(AssertUnwindSafe(move || drop(attachment))) {
completed = false;
std::mem::forget(payload);
}
}
completed
}