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 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 entry.texture.set_tex_id(texture_id);
566 }
567 if retiring {
568 mark_want_destroy(&mut entry.texture);
569 } else {
570 unsafe {
571 entry.texture.set_status(TextureStatus::OK);
574 }
575 }
576 }
577 TextureFeedbackResult::Destroyed => {
578 unsafe {
579 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 texture.set_tex_id(texture_id);
601 }
602 unsafe {
603 texture.set_status(TextureStatus::OK);
606 }
607 }
608 TextureFeedbackResult::Destroyed => {
609 unsafe {
610 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 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 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 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 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 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 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 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}