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 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 entry.texture.set_tex_id(texture_id);
614 }
615 if retiring {
616 mark_want_destroy(&mut entry.texture);
617 } else {
618 unsafe {
619 entry.texture.set_status(TextureStatus::OK);
622 }
623 }
624 }
625 TextureFeedbackResult::Destroyed => {
626 unsafe {
627 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 texture.set_tex_id(texture_id);
652 }
653 unsafe {
654 texture.set_status(TextureStatus::OK);
657 }
658 }
659 TextureFeedbackResult::Destroyed => {
660 unsafe {
661 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 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 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 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 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 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 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 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 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}