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