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