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