Skip to main content

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}