viewport_lib/resources/material/texture_store.rs
1//! Slotted texture storage with generational ids.
2//!
3//! `TextureStore` gives user-uploaded textures the same slot-and-generation
4//! model `MeshStore` gives meshes: a removed texture leaves an empty slot that a
5//! later upload reuses, and each slot carries a generation counter bumped on
6//! removal. Texture ids stay `u64` (materials hold them directly), with the
7//! generation packed into the high 32 bits and the slot index in the low 32.
8//! For a texture that is never freed the generation is 0, so its id equals its
9//! slot index and behaves exactly like the old dense index. Once a slot is freed
10//! and reused, a handle to the old texture carries a stale generation and
11//! resolves to `None`, falling back to the fallback texture instead of aliasing
12//! whatever now occupies the slot.
13
14use crate::resources::GpuTexture;
15
16/// Handle to a user-uploaded texture.
17///
18/// Wraps a packed slot index (low 32 bits) and generation (high 32 bits). A
19/// handle whose texture was freed (its slot reused by a later upload) carries a
20/// stale generation and resolves to `None` on lookup, falling back to the
21/// fallback texture instead of aliasing whatever now occupies the slot. For a
22/// texture that is never freed the generation is 0, so the handle's raw value
23/// equals its dense slot index.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26pub struct TextureId(pub(crate) u64);
27
28impl TextureId {
29 /// A handle that refers to no texture. Store lookups always return `None`
30 /// for it. Use it as the default / placeholder value.
31 pub const INVALID: TextureId = TextureId(u64::MAX);
32
33 /// The raw slot index this handle points at.
34 pub fn index(&self) -> usize {
35 (self.0 as u32) as usize
36 }
37
38 /// The packed raw value, for keying caches and crossing the plugin boundary.
39 pub(crate) fn raw(&self) -> u64 {
40 self.0
41 }
42}
43
44impl crate::resources::handle::ContentHandle for TextureId {
45 const INVALID: Self = TextureId(u64::MAX);
46
47 fn index(&self) -> usize {
48 (self.0 as u32) as usize
49 }
50
51 fn generation(&self) -> u32 {
52 (self.0 >> 32) as u32
53 }
54
55 fn from_parts(index: u32, generation: u32) -> Self {
56 pack_texture_id(index, generation)
57 }
58}
59
60/// Pack a slot index and generation into a texture id.
61///
62/// Generation 0 leaves the high bits clear, so `pack(index, 0) == index` and a
63/// never-freed texture keeps the dense-index id it always had.
64fn pack_texture_id(index: u32, generation: u32) -> TextureId {
65 TextureId(((generation as u64) << 32) | index as u64)
66}
67
68/// Slotted storage for user-uploaded GPU textures with generational ids, a free
69/// list, and maintained byte / count totals. The id packs the slot index and
70/// generation into a `u64`; the generic core keys on it through
71/// [`ContentHandle`](crate::resources::handle::ContentHandle).
72pub(crate) struct TextureStore {
73 store: crate::resources::handle::SlotStore<GpuTexture, TextureId>,
74}
75
76impl TextureStore {
77 /// Create an empty texture store.
78 pub fn new() -> Self {
79 Self {
80 store: crate::resources::handle::SlotStore::default(),
81 }
82 }
83
84 /// Insert a texture charging `bytes` against it, reusing a free slot if one
85 /// is available. Returns the handle carrying the slot's generation.
86 pub fn insert(&mut self, texture: GpuTexture, bytes: u64) -> TextureId {
87 self.store.insert(texture, bytes)
88 }
89
90 /// Look up the live texture for `id`, or `None` if the index is out of
91 /// range, the slot is empty, or the handle's generation is stale.
92 pub fn get(&self, id: TextureId) -> Option<&GpuTexture> {
93 self.store.get(id)
94 }
95
96 /// Remove the texture for `id`, dropping the `GpuTexture`, bumping the slot
97 /// generation, freeing the slot, and decrementing the byte / count totals.
98 ///
99 /// Returns `true` if a texture was actually removed, `false` if the slot was
100 /// empty, out of range, or the handle was stale.
101 pub fn remove(&mut self, id: TextureId) -> bool {
102 self.store.remove(id).is_some()
103 }
104
105 /// Swap the texture in `id`'s slot for `texture`, charging `bytes` in place
106 /// of the old size and keeping the slot generation (so `id` stays valid).
107 ///
108 /// The generation check is the in-flight guard: a stale `id` (its slot freed
109 /// and reused) does not resolve, so it cannot overwrite whatever now occupies
110 /// the slot. Returns the dropped `GpuTexture` on success (the caller drops it
111 /// after the frame that may still sample it), or `None` if `id` did not
112 /// resolve to a live texture.
113 pub fn replace(
114 &mut self,
115 id: TextureId,
116 texture: GpuTexture,
117 bytes: u64,
118 ) -> Option<GpuTexture> {
119 self.store.replace(id, texture, bytes)
120 }
121
122 /// Number of live (occupied) texture slots.
123 pub fn len(&self) -> usize {
124 self.store.len()
125 }
126
127 /// Total bytes charged for the textures currently resident in the store.
128 pub fn allocated_bytes(&self) -> u64 {
129 self.store.allocated_bytes()
130 }
131}