Skip to main content

dear_imgui_rs/context/
texture_registry.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::fmt;
4use std::num::NonZeroU64;
5use std::rc::Rc;
6use std::sync::Arc;
7
8use crate::render::snapshot::{
9    CapturedTextureOperation, PendingTextureRequest, RendererConsumerError,
10    ResolvedSnapshotTexture, SnapshotError, SnapshotTextureId, TextureFeedback,
11    TextureFeedbackResult, TextureOp, TextureRequestKind,
12};
13use crate::sys;
14use crate::texture::{
15    ManagedTextureError, ManagedTextureId, ManagedTextureMut, ManagedTextureMutationError,
16    ManagedTextureRef, OwnedTextureData, TextureData, TextureDataError, TextureStatus,
17};
18
19use super::binding::CTX_MUTEX;
20use super::texture_sync::{TextureSyncQueue, cumulative_update_rect, update_pending_create};
21use super::{Context, ContextId};
22
23pub(crate) type SharedTextureRegistry = Rc<RefCell<ManagedTextureRegistry>>;
24
25#[derive(Debug)]
26pub(crate) struct FontAtlasSnapshotTarget {
27    atlas: *mut sys::ImFontAtlas,
28    context: ContextId,
29    textures: Vec<FontAtlasTextureTarget>,
30}
31
32#[derive(Copy, Clone, Debug)]
33pub(crate) struct FontAtlasTextureTarget {
34    id: SnapshotTextureId,
35    texture: *mut sys::ImTextureData,
36}
37
38impl FontAtlasSnapshotTarget {
39    pub(crate) fn new(
40        atlas: *mut sys::ImFontAtlas,
41        context: ContextId,
42        textures: Vec<FontAtlasTextureTarget>,
43    ) -> Self {
44        Self {
45            atlas,
46            context,
47            textures,
48        }
49    }
50
51    pub(crate) fn resolve(
52        &self,
53        native: *const sys::ImTextureData,
54    ) -> Option<ResolvedSnapshotTexture> {
55        self.textures
56            .iter()
57            .find(|target| std::ptr::eq(native, target.texture.cast_const()))
58            .map(|target| ResolvedSnapshotTexture { id: target.id })
59    }
60
61    fn find(&self, id: SnapshotTextureId) -> Option<FontAtlasTextureTarget> {
62        self.textures.iter().find(|target| target.id == id).copied()
63    }
64
65    pub(crate) fn record_request_reference(&self, id: SnapshotTextureId, epoch: u64) {
66        crate::fonts::record_font_atlas_texture_reference(self.atlas, id, epoch);
67    }
68
69    fn identity_is_known(&self, id: SnapshotTextureId) -> bool {
70        matches!(id, SnapshotTextureId::FontAtlas { context, .. } if context == self.context)
71            && crate::fonts::font_atlas_texture_identity_is_known(self.atlas, id)
72    }
73
74    pub(crate) fn prune_tombstones(&self, watermark: u64) {
75        crate::fonts::prune_font_atlas_texture_tombstones(self.atlas, self.context, watermark);
76    }
77
78    pub(crate) fn reset_renderer_bindings(&self) {
79        for target in &self.textures {
80            let texture = unsafe { TextureData::from_raw(target.texture) };
81            if texture.ref_count() != 1 || texture.status() == TextureStatus::Destroyed {
82                continue;
83            }
84            unsafe {
85                // The pointer came from this transaction's current atlas list. The renderer has
86                // already released the resource represented by its binding.
87                texture.set_status(TextureStatus::Destroyed);
88            }
89        }
90        crate::fonts::mark_font_atlas_renderer_reset(self.atlas);
91    }
92}
93
94impl FontAtlasTextureTarget {
95    pub(crate) fn new(id: SnapshotTextureId, texture: *mut sys::ImTextureData) -> Self {
96        Self { id, texture }
97    }
98}
99
100pub(crate) struct ManagedTextureRegistry {
101    context: ContextId,
102    slots: Vec<TextureSlot>,
103    reusable: Vec<u32>,
104    by_native: HashMap<usize, ManagedTextureId>,
105    native_refresh_generation: u64,
106    sync: TextureSyncQueue,
107}
108
109enum TextureSlot {
110    Active(TextureEntry),
111    Retiring(TextureEntry),
112    NativeExposed {
113        entry: TextureEntry,
114        release_after_refresh: u64,
115    },
116    Retired {
117        generation: NonZeroU64,
118    },
119    Exhausted,
120}
121
122struct TextureEntry {
123    generation: NonZeroU64,
124    last_reference_epoch: u64,
125    destroy_ack_epoch: Option<u64>,
126    texture: OwnedTextureData,
127}
128
129impl fmt::Debug for ManagedTextureRegistry {
130    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131        let active = self
132            .slots
133            .iter()
134            .filter(|slot| matches!(slot, TextureSlot::Active(_)))
135            .count();
136        let retiring = self
137            .slots
138            .iter()
139            .filter(|slot| matches!(slot, TextureSlot::Retiring(_)))
140            .count();
141        let native_exposed = self
142            .slots
143            .iter()
144            .filter(|slot| matches!(slot, TextureSlot::NativeExposed { .. }))
145            .count();
146        formatter
147            .debug_struct("ManagedTextureRegistry")
148            .field("context", &self.context)
149            .field("active", &active)
150            .field("retiring", &retiring)
151            .field("native_exposed", &native_exposed)
152            .field("slot_count", &self.slots.len())
153            .finish()
154    }
155}
156
157impl ManagedTextureRegistry {
158    pub(crate) fn new(context: ContextId) -> SharedTextureRegistry {
159        Rc::new(RefCell::new(Self {
160            context,
161            slots: Vec::new(),
162            reusable: Vec::new(),
163            by_native: HashMap::new(),
164            native_refresh_generation: 0,
165            sync: TextureSyncQueue::default(),
166        }))
167    }
168
169    fn validate_context(&self, id: ManagedTextureId) -> Result<(), ManagedTextureError> {
170        if id.context_id() == self.context {
171            Ok(())
172        } else {
173            Err(ManagedTextureError::ForeignContext {
174                expected: self.context,
175                actual: id.context_id(),
176            })
177        }
178    }
179
180    fn slot(&self, id: ManagedTextureId) -> Result<&TextureSlot, ManagedTextureError> {
181        self.validate_context(id)?;
182        let slot = self
183            .slots
184            .get(id.slot() as usize)
185            .ok_or(ManagedTextureError::UnknownSlot(id))?;
186        let generation = match slot {
187            TextureSlot::Active(entry)
188            | TextureSlot::Retiring(entry)
189            | TextureSlot::NativeExposed { entry, .. } => entry.generation,
190            TextureSlot::Retired { generation } => *generation,
191            TextureSlot::Exhausted => return Err(ManagedTextureError::AlreadyRemoved(id)),
192        };
193        if generation != id.generation() {
194            return Err(ManagedTextureError::StaleGeneration(id));
195        }
196        Ok(slot)
197    }
198
199    fn active_entry(&self, id: ManagedTextureId) -> Result<&TextureEntry, ManagedTextureError> {
200        match self.slot(id)? {
201            TextureSlot::Active(entry) => Ok(entry),
202            TextureSlot::Retiring(_) => Err(ManagedTextureError::Retiring(id)),
203            TextureSlot::NativeExposed { .. }
204            | TextureSlot::Retired { .. }
205            | TextureSlot::Exhausted => Err(ManagedTextureError::AlreadyRemoved(id)),
206        }
207    }
208
209    fn active_entry_mut(
210        &mut self,
211        id: ManagedTextureId,
212    ) -> Result<&mut TextureEntry, ManagedTextureError> {
213        self.validate_context(id)?;
214        let slot = self
215            .slots
216            .get_mut(id.slot() as usize)
217            .ok_or(ManagedTextureError::UnknownSlot(id))?;
218        let generation = match slot {
219            TextureSlot::Active(entry)
220            | TextureSlot::Retiring(entry)
221            | TextureSlot::NativeExposed { entry, .. } => entry.generation,
222            TextureSlot::Retired { generation } => *generation,
223            TextureSlot::Exhausted => return Err(ManagedTextureError::AlreadyRemoved(id)),
224        };
225        if generation != id.generation() {
226            return Err(ManagedTextureError::StaleGeneration(id));
227        }
228        match slot {
229            TextureSlot::Active(entry) => Ok(entry),
230            TextureSlot::Retiring(_) => Err(ManagedTextureError::Retiring(id)),
231            TextureSlot::NativeExposed { .. }
232            | TextureSlot::Retired { .. }
233            | TextureSlot::Exhausted => Err(ManagedTextureError::AlreadyRemoved(id)),
234        }
235    }
236
237    fn allocate_slot(&mut self) -> (u32, NonZeroU64) {
238        while let Some(slot_index) = self.reusable.pop() {
239            let slot = &mut self.slots[slot_index as usize];
240            let TextureSlot::Retired { generation } = slot else {
241                debug_assert!(false, "reusable texture slot was not retired");
242                continue;
243            };
244            let Some(next) = generation.get().checked_add(1).and_then(NonZeroU64::new) else {
245                *slot = TextureSlot::Exhausted;
246                continue;
247            };
248            return (slot_index, next);
249        }
250
251        let slot_index = u32::try_from(self.slots.len())
252            .expect("managed texture registry exhausted its slot identity space");
253        (slot_index, NonZeroU64::MIN)
254    }
255
256    fn register(&mut self, mut texture: OwnedTextureData, watermark: u64) -> ManagedTextureId {
257        self.reap_destroyed(watermark);
258        let (slot_index, generation) = self.allocate_slot();
259        let id = ManagedTextureId::new(self.context, slot_index, generation);
260        let native = texture.as_mut().as_raw_mut();
261        assert_eq!(
262            texture.ref_count(),
263            0,
264            "Context::register_texture() received texture data already owned by native state"
265        );
266        unsafe {
267            sys::igRegisterUserTexture(native);
268        }
269        let entry = TextureEntry {
270            generation,
271            last_reference_epoch: 0,
272            destroy_ack_epoch: None,
273            texture,
274        };
275        if slot_index as usize == self.slots.len() {
276            self.slots.push(TextureSlot::Active(entry));
277        } else {
278            self.slots[slot_index as usize] = TextureSlot::Active(entry);
279        }
280        let previous = self.by_native.insert(native as usize, id);
281        assert!(
282            previous.is_none(),
283            "native texture allocation was registered twice"
284        );
285        id
286    }
287
288    pub(crate) fn resolve(
289        &self,
290        id: ManagedTextureId,
291    ) -> Result<sys::ImTextureRef, ManagedTextureError> {
292        let entry = self.active_entry(id)?;
293        Ok(sys::ImTextureRef {
294            _TexData: entry.texture.as_raw() as *mut sys::ImTextureData,
295            _TexID: 0 as sys::ImTextureID,
296        })
297    }
298
299    pub(crate) fn id_for_native(
300        &self,
301        native: *const sys::ImTextureData,
302    ) -> Option<ManagedTextureId> {
303        self.by_native.get(&(native as usize)).copied()
304    }
305
306    pub(crate) fn resolve_snapshot_texture(
307        &self,
308        native: *const sys::ImTextureData,
309        atlas: &FontAtlasSnapshotTarget,
310    ) -> Result<ResolvedSnapshotTexture, SnapshotError> {
311        if let Some(id) = self.id_for_native(native) {
312            match self.slot(id)? {
313                TextureSlot::Active(_) | TextureSlot::Retiring(_) => {}
314                TextureSlot::NativeExposed { .. }
315                | TextureSlot::Retired { .. }
316                | TextureSlot::Exhausted => {
317                    return Err(ManagedTextureError::AlreadyRemoved(id).into());
318                }
319            }
320            return Ok(ResolvedSnapshotTexture {
321                id: SnapshotTextureId::User(id),
322            });
323        }
324        if let Some(resolved) = atlas.resolve(native) {
325            return Ok(resolved);
326        }
327        Err(SnapshotError::UnknownManagedTexture)
328    }
329
330    pub(crate) fn record_snapshot_references(
331        &mut self,
332        ids: &std::collections::HashSet<ManagedTextureId>,
333        epoch: u64,
334    ) -> Result<(), ManagedTextureError> {
335        for id in ids {
336            match self.slot(*id)? {
337                TextureSlot::Active(_) | TextureSlot::Retiring(_) => {}
338                TextureSlot::NativeExposed { .. }
339                | TextureSlot::Retired { .. }
340                | TextureSlot::Exhausted => {
341                    return Err(ManagedTextureError::AlreadyRemoved(*id));
342                }
343            }
344        }
345        for id in ids {
346            let slot = &mut self.slots[id.slot() as usize];
347            let entry = match slot {
348                TextureSlot::Active(entry) | TextureSlot::Retiring(entry) => entry,
349                TextureSlot::NativeExposed { .. }
350                | TextureSlot::Retired { .. }
351                | TextureSlot::Exhausted => unreachable!(),
352            };
353            entry.last_reference_epoch = entry.last_reference_epoch.max(epoch);
354        }
355        Ok(())
356    }
357
358    fn with_texture<R>(
359        &self,
360        id: ManagedTextureId,
361        f: impl for<'texture> FnOnce(ManagedTextureRef<'texture>) -> R,
362    ) -> Result<R, ManagedTextureError> {
363        let entry = self.active_entry(id)?;
364        Ok(f(ManagedTextureRef::new(&entry.texture)))
365    }
366
367    fn with_texture_mut<R>(
368        &mut self,
369        id: ManagedTextureId,
370        f: impl for<'texture> FnOnce(ManagedTextureMut<'texture>) -> R,
371    ) -> Result<R, ManagedTextureError> {
372        let entry = self.active_entry_mut(id)?;
373        Ok(f(ManagedTextureMut::new(&mut entry.texture)))
374    }
375
376    pub(crate) fn stage_snapshot_operations(
377        &mut self,
378        requests: &[CapturedTextureOperation],
379        atlas: &FontAtlasSnapshotTarget,
380    ) -> Result<Vec<PendingTextureRequest>, SnapshotError> {
381        // Capture validated bytes first. Native OK means the persistent queue owns them,
382        // not that the renderer has uploaded them.
383        let mut staged = Vec::with_capacity(requests.len());
384        for request in requests.iter() {
385            let native = match request.texture {
386                SnapshotTextureId::User(id) => match self.slot(id)? {
387                    TextureSlot::Active(entry) | TextureSlot::Retiring(entry) => {
388                        entry.texture.as_raw()
389                    }
390                    _ => return Err(ManagedTextureError::AlreadyRemoved(id).into()),
391                },
392                SnapshotTextureId::FontAtlas { .. } => atlas
393                    .find(request.texture)
394                    .ok_or(SnapshotError::UnknownManagedTexture)?
395                    .texture
396                    .cast_const(),
397            };
398            let texture = unsafe { TextureData::from_raw(native.cast_mut()) };
399            let mut operation = Arc::clone(&request.op);
400            if let Some(create) = self
401                .sync
402                .operation(request.texture)
403                .and_then(|previous| update_pending_create(previous, operation.as_ref()))
404            {
405                operation = Arc::new(create);
406            } else if let TextureOp::Update {
407                format,
408                width,
409                height,
410                rects,
411            } = operation.as_ref()
412            {
413                let previous_update = self
414                    .sync
415                    .operation(request.texture)
416                    .filter(|previous| matches!(previous, TextureOp::Update { .. }));
417                if previous_update.is_none() && rects.len() == 1 {
418                    staged.push((request.texture, native, operation));
419                    continue;
420                }
421                let previous = previous_update.unwrap_or(operation.as_ref());
422                let rect = cumulative_update_rect(previous, operation.as_ref()).ok_or(
423                    SnapshotError::TextureFullUpdateOutOfRange {
424                        id: request.texture,
425                        width: *width,
426                        height: *height,
427                    },
428                )?;
429                let pixels = texture
430                    .pixels()
431                    .ok_or(SnapshotError::TexturePixelsMissing {
432                        id: request.texture,
433                        status: texture.status(),
434                    })?;
435                let upload = crate::render::snapshot::copy_upload_rect(
436                    pixels,
437                    *width,
438                    *height,
439                    texture.raw_bytes_per_pixel_i32() as usize,
440                    rect,
441                )
442                .ok_or(SnapshotError::TextureInvalidLayout {
443                    id: request.texture,
444                    width: texture.raw_width_i32(),
445                    height: texture.raw_height_i32(),
446                    bpp: texture.raw_bytes_per_pixel_i32(),
447                })?;
448                operation = Arc::new(TextureOp::Update {
449                    format: *format,
450                    width: *width,
451                    height: *height,
452                    rects: vec![upload],
453                });
454            }
455            staged.push((request.texture, native, operation));
456        }
457        for (id, native, operation) in staged {
458            let destroy = matches!(operation.as_ref(), TextureOp::Destroy);
459            self.sync.stage(id, operation);
460            let texture = unsafe { TextureData::from_raw(native.cast_mut()) };
461            texture.claim_managed_queue();
462            if !destroy {
463                unsafe {
464                    texture.set_status(TextureStatus::OK);
465                }
466            }
467            if matches!(id, SnapshotTextureId::FontAtlas { .. }) {
468                crate::fonts::mark_font_atlas_texture_staged(atlas.atlas);
469            }
470        }
471        Ok(self.sync.requests())
472    }
473
474    fn remove(&mut self, id: ManagedTextureId, watermark: u64) -> Result<(), ManagedTextureError> {
475        self.reap_destroyed(watermark);
476        self.validate_context(id)?;
477        let slot_index = id.slot() as usize;
478        let slot = self
479            .slots
480            .get(slot_index)
481            .ok_or(ManagedTextureError::UnknownSlot(id))?;
482        match slot {
483            TextureSlot::Active(entry) if entry.generation == id.generation() => {}
484            TextureSlot::Active(_) => return Err(ManagedTextureError::StaleGeneration(id)),
485            TextureSlot::Retiring(entry) if entry.generation == id.generation() => {
486                return Err(ManagedTextureError::AlreadyRetiring(id));
487            }
488            TextureSlot::Retiring(_) => return Err(ManagedTextureError::StaleGeneration(id)),
489            TextureSlot::NativeExposed { entry, .. } if entry.generation == id.generation() => {
490                return Err(ManagedTextureError::AlreadyRemoved(id));
491            }
492            TextureSlot::NativeExposed { .. } => {
493                return Err(ManagedTextureError::StaleGeneration(id));
494            }
495            TextureSlot::Retired { generation } if *generation == id.generation() => {
496                return Err(ManagedTextureError::AlreadyRemoved(id));
497            }
498            TextureSlot::Retired { .. } => return Err(ManagedTextureError::StaleGeneration(id)),
499            TextureSlot::Exhausted => return Err(ManagedTextureError::AlreadyRemoved(id)),
500        }
501
502        let placeholder = TextureSlot::Retired {
503            generation: id.generation(),
504        };
505        let TextureSlot::Active(mut entry) =
506            std::mem::replace(&mut self.slots[slot_index], placeholder)
507        else {
508            unreachable!("validated active texture slot changed without a mutable alias")
509        };
510        let has_renderer_binding =
511            !entry.texture.tex_id().is_null() || !entry.texture.backend_user_data().is_null();
512        let has_outstanding_reference = entry.last_reference_epoch > watermark;
513        let queue_owned = unsafe { !(*entry.texture.as_raw()).QueueUserData.is_null() };
514        if has_renderer_binding || has_outstanding_reference || queue_owned {
515            mark_want_destroy(&mut entry.texture);
516            self.slots[slot_index] = TextureSlot::Retiring(entry);
517        } else {
518            self.unregister_and_expose(slot_index, entry);
519        }
520        Ok(())
521    }
522
523    pub(crate) fn apply_snapshot_feedback(
524        &mut self,
525        feedback: &[TextureFeedback],
526        atlas: &FontAtlasSnapshotTarget,
527        epoch: u64,
528    ) -> Result<usize, RendererConsumerError> {
529        let mut applied = 0;
530        for item in feedback {
531            let key = item.key();
532            if matches!(
533                item.result(),
534                TextureFeedbackResult::Superseded | TextureFeedbackResult::Retry
535            ) {
536                continue;
537            }
538            match key.texture {
539                SnapshotTextureId::User(id) => match self.slot(id)? {
540                    TextureSlot::Active(_) => {
541                        if item.result() == TextureFeedbackResult::Destroyed {
542                            return Err(RendererConsumerError::InvalidFeedbackTransition {
543                                texture: key.texture,
544                            });
545                        }
546                    }
547                    TextureSlot::Retiring(_) => {}
548                    TextureSlot::NativeExposed { .. }
549                    | TextureSlot::Retired { .. }
550                    | TextureSlot::Exhausted => {
551                        return Err(ManagedTextureError::AlreadyRemoved(id).into());
552                    }
553                },
554                SnapshotTextureId::FontAtlas { .. } => {
555                    if !atlas.identity_is_known(key.texture) {
556                        return Err(RendererConsumerError::StaleFontAtlas);
557                    }
558                }
559            }
560            match (key.kind, item.result()) {
561                (
562                    TextureRequestKind::Create | TextureRequestKind::Update,
563                    TextureFeedbackResult::Uploaded { texture_id },
564                ) if !texture_id.is_null() => {}
565                (TextureRequestKind::Destroy, TextureFeedbackResult::Destroyed) => {}
566                _ => {
567                    return Err(RendererConsumerError::InvalidFeedbackTransition {
568                        texture: key.texture,
569                    });
570                }
571            }
572        }
573
574        for item in feedback {
575            let key = item.key();
576            if matches!(
577                item.result(),
578                TextureFeedbackResult::Superseded | TextureFeedbackResult::Retry
579            ) {
580                continue;
581            }
582            let native = match key.texture {
583                SnapshotTextureId::User(id) => match &self.slots[id.slot() as usize] {
584                    TextureSlot::Active(entry) | TextureSlot::Retiring(entry) => {
585                        entry.texture.as_raw()
586                    }
587                    _ => unreachable!("feedback identities were validated"),
588                },
589                SnapshotTextureId::FontAtlas { .. } => {
590                    let Some(target) = atlas.find(key.texture) else {
591                        continue;
592                    };
593                    target.texture.cast_const()
594                }
595            };
596            let texture = unsafe { TextureData::from_raw(native.cast_mut()) };
597            // Native changes may have happened since the last capture. In particular a
598            // pending Create must stay self-contained until that newer content is staged.
599            let expected_status = if key.kind == TextureRequestKind::Destroy {
600                TextureStatus::WantDestroy
601            } else {
602                TextureStatus::OK
603            };
604            if texture.status() != expected_status
605                || !self.sync.acknowledge(key.texture, key.revision)
606            {
607                continue;
608            }
609            match item.result() {
610                TextureFeedbackResult::Uploaded { texture_id } => unsafe {
611                    texture.set_tex_id(texture_id);
612                },
613                TextureFeedbackResult::Destroyed => {
614                    unsafe {
615                        texture.set_status(TextureStatus::Destroyed);
616                    }
617                    if let SnapshotTextureId::User(id) = key.texture {
618                        if let TextureSlot::Retiring(entry) = &mut self.slots[id.slot() as usize] {
619                            entry.destroy_ack_epoch = Some(epoch);
620                        }
621                    }
622                    self.sync.forget(key.texture);
623                }
624                TextureFeedbackResult::Superseded | TextureFeedbackResult::Retry => unreachable!(),
625            }
626            applied += 1;
627        }
628        Ok(applied)
629    }
630
631    pub(crate) fn reap_destroyed(&mut self, watermark: u64) {
632        let ready = self
633            .slots
634            .iter()
635            .enumerate()
636            .filter_map(|(index, slot)| match slot {
637                TextureSlot::Retiring(entry)
638                    if entry.texture.status() == TextureStatus::Destroyed
639                        && entry.texture.tex_id().is_null()
640                        && entry.texture.backend_user_data().is_null()
641                        && entry
642                            .destroy_ack_epoch
643                            .is_some_and(|epoch| epoch <= watermark)
644                        && entry.last_reference_epoch <= watermark =>
645                {
646                    Some(index)
647                }
648                _ => None,
649            })
650            .collect::<Vec<_>>();
651
652        for slot_index in ready {
653            let generation = match &self.slots[slot_index] {
654                TextureSlot::Retiring(entry) => entry.generation,
655                _ => continue,
656            };
657            let placeholder = TextureSlot::Retired { generation };
658            let TextureSlot::Retiring(entry) =
659                std::mem::replace(&mut self.slots[slot_index], placeholder)
660            else {
661                continue;
662            };
663            self.unregister_and_expose(slot_index, entry);
664        }
665    }
666
667    pub(crate) fn reset_renderer_bindings(&mut self, watermark: u64) {
668        self.sync.clear();
669        for slot in &mut self.slots {
670            let (entry, retiring) = match slot {
671                TextureSlot::Active(entry) => (entry, false),
672                TextureSlot::Retiring(entry) => (entry, true),
673                TextureSlot::NativeExposed { .. }
674                | TextureSlot::Retired { .. }
675                | TextureSlot::Exhausted => continue,
676            };
677            if entry.texture.status() == TextureStatus::Destroyed {
678                if retiring {
679                    entry.destroy_ack_epoch = Some(watermark);
680                }
681                continue;
682            }
683            unsafe {
684                // The renderer has already released the resource. Resetting through the
685                // Context-owned allocation avoids stale pointers cached by PlatformIO.Textures.
686                entry.texture.set_status(TextureStatus::Destroyed);
687            }
688            if retiring {
689                entry.destroy_ack_epoch = Some(watermark);
690            }
691        }
692        self.reap_destroyed(watermark);
693    }
694
695    fn unregister_and_expose(&mut self, slot_index: usize, mut entry: TextureEntry) {
696        let native = entry.texture.as_mut().as_raw_mut();
697        unsafe {
698            sys::igUnregisterUserTexture(native);
699        }
700        self.by_native.remove(&(native as usize));
701        let release_after_refresh = self
702            .native_refresh_generation
703            .checked_add(1)
704            .expect("native texture-list refresh generation exhausted");
705        self.slots[slot_index] = TextureSlot::NativeExposed {
706            entry,
707            release_after_refresh,
708        };
709    }
710
711    pub(crate) fn observe_native_texture_list_refresh(&mut self) {
712        self.native_refresh_generation = self
713            .native_refresh_generation
714            .checked_add(1)
715            .expect("native texture-list refresh generation exhausted");
716        let ready = self
717            .slots
718            .iter()
719            .enumerate()
720            .filter_map(|(index, slot)| match slot {
721                TextureSlot::NativeExposed {
722                    release_after_refresh,
723                    ..
724                } if *release_after_refresh <= self.native_refresh_generation => Some(index),
725                _ => None,
726            })
727            .collect::<Vec<_>>();
728        for slot_index in ready {
729            let generation = match &self.slots[slot_index] {
730                TextureSlot::NativeExposed { entry, .. } => entry.generation,
731                _ => continue,
732            };
733            let previous = std::mem::replace(
734                &mut self.slots[slot_index],
735                TextureSlot::Retired { generation },
736            );
737            let TextureSlot::NativeExposed { entry, .. } = previous else {
738                unreachable!("native-exposed texture changed without a mutable alias")
739            };
740            drop(entry);
741            self.reusable
742                .push(u32::try_from(slot_index).expect("texture slot index must fit u32"));
743        }
744    }
745
746    pub(super) fn prepare_teardown(&mut self) {
747        let registered = self
748            .slots
749            .iter()
750            .enumerate()
751            .filter_map(|(index, slot)| {
752                matches!(slot, TextureSlot::Active(_) | TextureSlot::Retiring(_)).then_some(index)
753            })
754            .collect::<Vec<_>>();
755        for slot_index in registered {
756            let generation = match &self.slots[slot_index] {
757                TextureSlot::Active(entry) | TextureSlot::Retiring(entry) => entry.generation,
758                _ => continue,
759            };
760            let previous = std::mem::replace(
761                &mut self.slots[slot_index],
762                TextureSlot::Retired { generation },
763            );
764            let entry = match previous {
765                TextureSlot::Active(entry) | TextureSlot::Retiring(entry) => entry,
766                _ => unreachable!("registered texture changed without a mutable alias"),
767            };
768            self.unregister_and_expose(slot_index, entry);
769        }
770        self.by_native.clear();
771        self.reusable.clear();
772    }
773
774    pub(super) fn release_after_native_destroy(&mut self) {
775        self.by_native.clear();
776        self.reusable.clear();
777        self.slots.clear();
778    }
779}
780
781fn mark_want_destroy(texture: &mut TextureData) {
782    unsafe {
783        (*texture.as_raw_mut()).WantDestroyNextFrame = true;
784        sys::ImTextureData_SetStatus(texture.as_raw_mut(), sys::ImTextureStatus_WantDestroy);
785    }
786}
787
788impl Context {
789    /// Transfer an owned user texture into this Context's managed registry.
790    pub fn register_texture(&mut self, texture: OwnedTextureData) -> ManagedTextureId {
791        let _guard = CTX_MUTEX.lock();
792        self.assert_current_context("Context::register_texture()");
793        let watermark = self.snapshot_hub.completion_watermark();
794        self.texture_registry
795            .borrow_mut()
796            .register(texture, watermark)
797    }
798
799    /// Read an active managed texture inside a non-escaping closure.
800    ///
801    /// The facade deliberately has no raw-pointer accessor.
802    ///
803    /// ```compile_fail
804    /// use dear_imgui_rs::{Context, ManagedTextureId, sys};
805    ///
806    /// fn leak_native(context: &Context, id: ManagedTextureId) -> *const sys::ImTextureData {
807    ///     context.with_texture(id, |texture| texture.as_raw()).unwrap()
808    /// }
809    /// ```
810    pub fn with_texture<R>(
811        &self,
812        id: ManagedTextureId,
813        f: impl for<'texture> FnOnce(ManagedTextureRef<'texture>) -> R,
814    ) -> Result<R, ManagedTextureError> {
815        let _guard = CTX_MUTEX.lock();
816        self.assert_current_context("Context::with_texture()");
817        self.texture_registry.borrow().with_texture(id, f)
818    }
819
820    /// Mutate an active managed texture inside a non-escaping closure.
821    ///
822    /// Renderer-owned state can only be changed by request-bound feedback.
823    ///
824    /// ```compile_fail
825    /// use dear_imgui_rs::{Context, ManagedTextureId, TextureStatus};
826    ///
827    /// fn bypass_renderer(context: &mut Context, id: ManagedTextureId) {
828    ///     context
829    ///         .with_texture_mut(id, |mut texture| texture.set_status(TextureStatus::OK))
830    ///         .unwrap();
831    /// }
832    /// ```
833    pub fn with_texture_mut<R>(
834        &mut self,
835        id: ManagedTextureId,
836        f: impl for<'texture> FnOnce(ManagedTextureMut<'texture>) -> R,
837    ) -> Result<R, ManagedTextureError> {
838        let _guard = CTX_MUTEX.lock();
839        self.assert_current_context("Context::with_texture_mut()");
840        self.texture_registry.borrow_mut().with_texture_mut(id, f)
841    }
842
843    /// Mutate an active managed texture with flattened access and pixel-validation errors.
844    ///
845    /// # Errors
846    ///
847    /// Returns [`ManagedTextureMutationError::Access`] when `id` is foreign, stale, unknown, or
848    /// retiring. Returns [`ManagedTextureMutationError::Data`] when the closure returns a pixel
849    /// validation error. Each [`ManagedTextureMut`] operation is transactional, but the closure is
850    /// not: successful operations performed before a later error remain applied and immediately
851    /// invalidate older renderer feedback.
852    ///
853    /// ```
854    /// use dear_imgui_rs::{
855    ///     Context, ManagedTextureMutationError, OwnedTextureData, TextureDataError, TextureFormat,
856    /// };
857    ///
858    /// let mut context = Context::create();
859    /// let texture = OwnedTextureData::from_pixels(TextureFormat::RGBA32, 1, 1, &[0; 4])?;
860    /// let id = context.register_texture(texture);
861    /// let error = context
862    ///     .try_with_texture_mut(id, |mut texture| texture.replace_pixels(&[0; 3]))
863    ///     .unwrap_err();
864    /// assert!(matches!(
865    ///     error,
866    ///     ManagedTextureMutationError::Data(TextureDataError::ByteLengthMismatch {
867    ///         expected: 4,
868    ///         actual: 3,
869    ///     })
870    /// ));
871    /// # Ok::<(), Box<dyn std::error::Error>>(())
872    /// ```
873    pub fn try_with_texture_mut<R>(
874        &mut self,
875        id: ManagedTextureId,
876        f: impl for<'texture> FnOnce(ManagedTextureMut<'texture>) -> Result<R, TextureDataError>,
877    ) -> Result<R, ManagedTextureMutationError> {
878        self.with_texture_mut(id, f)
879            .map_err(ManagedTextureMutationError::Access)?
880            .map_err(ManagedTextureMutationError::Data)
881    }
882
883    /// Stop accepting new draw references and retire a managed texture.
884    pub fn remove_texture(&mut self, id: ManagedTextureId) -> Result<(), ManagedTextureError> {
885        let _guard = CTX_MUTEX.lock();
886        self.assert_current_context("Context::remove_texture()");
887        let watermark = self.snapshot_hub.completion_watermark();
888        self.texture_registry.borrow_mut().remove(id, watermark)
889    }
890
891    pub(crate) fn collect_retired_textures(&mut self) {
892        let watermark = self.snapshot_hub.completion_watermark();
893        self.texture_registry.borrow_mut().reap_destroyed(watermark);
894    }
895}
896
897#[cfg(test)]
898mod tests {
899    use super::*;
900    use crate::context::snapshot_hub::SnapshotHub;
901    use crate::texture::{TextureFormat, TextureId};
902
903    fn texture() -> OwnedTextureData {
904        OwnedTextureData::from_pixels(TextureFormat::RGBA32, 1, 1, &[1, 2, 3, 4]).unwrap()
905    }
906
907    #[test]
908    fn native_debug_ids_do_not_participate_in_managed_identity() {
909        let mut context = Context::create();
910        let first = texture();
911        let second = texture();
912        assert_eq!(first.native_unique_id(), 0);
913        assert_eq!(second.native_unique_id(), 0);
914        let first_id = context.register_texture(first);
915        let second_id = context.register_texture(second);
916        assert_ne!(first_id, second_id);
917        assert_eq!(first_id.context_id(), context.id());
918        assert_eq!(second_id.context_id(), context.id());
919    }
920
921    #[test]
922    fn foreign_and_reused_handles_fail_before_native_access() {
923        let mut context_a = Context::create();
924        let first_id = context_a.register_texture(texture());
925        let suspended_a = context_a.suspend_or_panic();
926        let mut context_b = Context::create();
927        assert!(matches!(
928            context_b.with_texture(first_id, |_| ()),
929            Err(ManagedTextureError::ForeignContext { .. })
930        ));
931        assert!(matches!(
932            context_b.try_with_texture_mut(first_id, |mut texture| {
933                texture.replace_pixels(&[4, 3, 2, 1])
934            }),
935            Err(ManagedTextureMutationError::Access(
936                ManagedTextureError::ForeignContext { .. }
937            ))
938        ));
939        drop(context_b);
940        let mut context_a = suspended_a.activate().expect("Context A should reactivate");
941        context_a.remove_texture(first_id).expect("unused texture");
942        assert_eq!(
943            context_a.try_with_texture_mut(first_id, |mut texture| {
944                texture.replace_pixels(&[4, 3, 2, 1])
945            }),
946            Err(ManagedTextureMutationError::Access(
947                ManagedTextureError::AlreadyRemoved(first_id)
948            ))
949        );
950        let before_refresh_id = context_a.register_texture(texture());
951        assert_ne!(
952            before_refresh_id.slot(),
953            first_id.slot(),
954            "a native-exposed allocation must not be reused before the texture list refreshes"
955        );
956        context_a
957            .texture_registry
958            .borrow_mut()
959            .observe_native_texture_list_refresh();
960        let replacement_id = context_a.register_texture(texture());
961        assert_eq!(replacement_id.slot(), first_id.slot());
962        assert_ne!(replacement_id.generation(), first_id.generation());
963        assert_eq!(
964            context_a.with_texture(first_id, |_| ()),
965            Err(ManagedTextureError::StaleGeneration(first_id))
966        );
967    }
968
969    #[test]
970    fn managed_queue_marker_is_idempotent_and_cleared_by_destroy_feedback() {
971        let mut context = Context::create();
972        let id = context.register_texture(texture());
973        let native = context
974            .texture_registry
975            .borrow()
976            .active_entry(id)
977            .expect("registered texture")
978            .texture
979            .as_raw()
980            .cast_mut();
981        let atlas = FontAtlasSnapshotTarget::new(std::ptr::null_mut(), context.id(), Vec::new());
982        let create = Arc::new(TextureOp::Create {
983            format: TextureFormat::RGBA32,
984            width: 1,
985            height: 1,
986            row_pitch: 4,
987            pixels: vec![1, 2, 3, 4],
988        });
989        let captured = vec![CapturedTextureOperation {
990            texture: SnapshotTextureId::User(id),
991            op: create,
992        }];
993
994        {
995            let mut registry = context.texture_registry.borrow_mut();
996            let pending = registry
997                .stage_snapshot_operations(&captured, &atlas)
998                .expect("create request should claim the managed queue");
999            let first_revision = pending[0].revision;
1000            let pending = registry
1001                .stage_snapshot_operations(&[], &atlas)
1002                .expect("claiming the same managed queue must be idempotent");
1003            assert_eq!(pending[0].revision, first_revision);
1004        }
1005        unsafe {
1006            assert_eq!((*native).QueueUserData, native.cast());
1007        }
1008
1009        {
1010            let mut registry = context.texture_registry.borrow_mut();
1011            let entry = registry.active_entry_mut(id).expect("active texture");
1012            unsafe {
1013                entry.texture.set_tex_id(TextureId::new(73));
1014                entry.texture.set_status(TextureStatus::OK);
1015            }
1016            registry.remove(id, 0).expect("bound texture should retire");
1017        }
1018
1019        let captured = vec![CapturedTextureOperation {
1020            texture: SnapshotTextureId::User(id),
1021            op: Arc::new(TextureOp::Destroy),
1022        }];
1023        let destroy = context
1024            .texture_registry
1025            .borrow_mut()
1026            .stage_snapshot_operations(&captured, &atlas)
1027            .expect("destroy request should remain queue-owned");
1028
1029        let mut hub = SnapshotHub::new(context.id());
1030        let generation = hub
1031            .validate_consumer_admission()
1032            .expect("fresh snapshot hub should admit a renderer");
1033        let consumer = hub.commit_synchronous_consumer_admission(generation);
1034        let (epoch, requests) = hub
1035            .begin_synchronous(&consumer, destroy, &atlas)
1036            .expect("destroy request should enter a synchronous epoch");
1037        let feedback = requests[0]
1038            .destroyed()
1039            .expect("destroy request should produce matching feedback");
1040        context
1041            .texture_registry
1042            .borrow_mut()
1043            .apply_snapshot_feedback(&[feedback], &atlas, epoch.sequence())
1044            .expect("matching destroy feedback should reconcile");
1045
1046        unsafe {
1047            assert!((*native).QueueUserData.is_null());
1048        }
1049    }
1050
1051    #[test]
1052    fn destroy_status_without_request_bound_ack_cannot_reap_allocation() {
1053        let mut context = Context::create();
1054        let id = context.register_texture(texture());
1055        let native = {
1056            let mut registry = context.texture_registry.borrow_mut();
1057            let entry = registry.active_entry_mut(id).expect("active texture");
1058            let texture = &mut entry.texture;
1059            unsafe {
1060                // This test seeds renderer-owned state before exercising retirement validation.
1061                texture.set_tex_id(TextureId::new(91));
1062                texture.set_status(TextureStatus::OK);
1063            }
1064            texture.as_raw_mut()
1065        };
1066        context.remove_texture(id).expect("begin retirement");
1067        assert_eq!(
1068            context.remove_texture(id),
1069            Err(ManagedTextureError::AlreadyRetiring(id))
1070        );
1071        assert_eq!(
1072            context.texture_registry.borrow().id_for_native(native),
1073            Some(id)
1074        );
1075        unsafe {
1076            // Deliberately bypass feedback to prove that native status alone cannot retire data.
1077            TextureData::from_raw(native).set_status(TextureStatus::Destroyed);
1078        }
1079        context.collect_retired_textures();
1080        assert_eq!(
1081            context.texture_registry.borrow().id_for_native(native),
1082            Some(id),
1083            "native status alone must not impersonate request-bound feedback"
1084        );
1085        {
1086            let mut registry = context.texture_registry.borrow_mut();
1087            let TextureSlot::Retiring(entry) = &mut registry.slots[id.slot() as usize] else {
1088                panic!("texture should still be retiring");
1089            };
1090            entry.destroy_ack_epoch = Some(0);
1091        }
1092        context.collect_retired_textures();
1093        assert_eq!(
1094            context.texture_registry.borrow().id_for_native(native),
1095            None
1096        );
1097    }
1098
1099    #[test]
1100    fn removed_storage_outlives_the_cached_native_texture_list() {
1101        let mut context = Context::create();
1102        context.io_mut().set_display_size([128.0, 128.0]);
1103        context.io_mut().set_delta_time(1.0 / 60.0);
1104        context
1105            .font_atlas()
1106            .try_claim_legacy_renderer()
1107            .expect("legacy renderer font atlas should be available")
1108            .build();
1109        let id = context.register_texture(texture());
1110        let native = context
1111            .texture_registry
1112            .borrow()
1113            .active_entry(id)
1114            .expect("registered texture")
1115            .texture
1116            .as_raw();
1117
1118        drop(context.begin_frame());
1119        let platform_io = unsafe { &*context.platform_io_ptr("test") };
1120        assert!((0..platform_io.Textures.Size).any(|index| unsafe {
1121            std::ptr::eq(
1122                *platform_io.Textures.Data.add(index as usize),
1123                native.cast_mut(),
1124            )
1125        }));
1126
1127        context.remove_texture(id).expect("unused texture");
1128        assert!(matches!(
1129            &context.texture_registry.borrow().slots[id.slot() as usize],
1130            TextureSlot::NativeExposed { entry, .. }
1131                if std::ptr::eq(entry.texture.as_raw(), native)
1132        ));
1133
1134        drop(context.begin_frame());
1135        assert!(matches!(
1136            &context.texture_registry.borrow().slots[id.slot() as usize],
1137            TextureSlot::Retired { generation } if *generation == id.generation()
1138        ));
1139    }
1140}