Skip to main content

pebble/assets/
storage.rs

1use slotmap::{Key as _, SecondaryMap, SlotMap, new_key_type};
2use std::collections::HashMap;
3
4new_key_type! {
5    /// Untyped slot-map key for an asset entry.
6    ///
7    /// Prefer the typed [`Handle<T>`](crate::assets::handle::Handle) over this
8    /// in most code. `RawAssetHandle` is used internally by the storage and
9    /// sync systems.
10    pub struct RawAssetHandle;
11}
12
13/// Storage for raw CPU-side assets of type `T`.
14///
15/// Assets are inserted by name and looked up by either name or
16/// [`RawAssetHandle`]. When an asset is inserted or updated its handle is
17/// pushed onto the *dirty queue*, which the sync system drains each tick to
18/// upload changed assets to the GPU.
19pub struct Assets<T: 'static + Send + Sync> {
20    storage: SlotMap<RawAssetHandle, T>,
21    handles: HashMap<String, RawAssetHandle>,
22    queue: Vec<RawAssetHandle>,
23    removed: Vec<RawAssetHandle>,
24}
25
26impl<T: 'static + Send + Sync> Assets<T> {
27    pub fn new() -> Self {
28        Self {
29            storage: SlotMap::with_key(),
30            handles: HashMap::new(),
31            queue: Vec::new(),
32            removed: Vec::new(),
33        }
34    }
35
36    /// Insert `asset` under `name`, returning its handle.
37    ///
38    /// If an asset with the same name already exists, its data is replaced
39    /// **in-place**: the same slot-map entry (and therefore the same handle)
40    /// is reused, so any code that already holds a [`Handle`](crate::assets::handle::Handle)
41    /// or [`RawAssetHandle`] for this asset continues to work correctly.
42    /// The handle is re-queued so the sync system re-uploads the new data.
43    pub fn insert(&mut self, name: &str, asset: T) -> RawAssetHandle {
44        if let Some(&existing) = self.handles.get(name) {
45            // Replace data in-place so existing handles stay valid.
46            if let Some(slot) = self.storage.get_mut(existing) {
47                *slot = asset;
48                // Only queue once — don't duplicate if already dirty.
49                if !self.queue.contains(&existing) {
50                    self.queue.push(existing);
51                }
52                tracing::debug!(
53                    "Assets<{}>: replaced data for {:?} ({name}) in-place",
54                    std::any::type_name::<T>(),
55                    existing
56                );
57                return existing;
58            }
59        }
60
61        let handle = self.storage.insert(asset);
62        self.handles.insert(name.to_string(), handle);
63        self.queue.push(handle);
64        handle
65    }
66
67    /// Look up an asset by its raw handle.
68    pub fn get(&self, handle: RawAssetHandle) -> Option<&T> {
69        if handle.is_null() {
70            tracing::warn!(
71                "Assets<{}>: get() called with a null/default handle — \
72                 did you forget to insert the asset and store the returned handle?",
73                std::any::type_name::<T>()
74            );
75            return None;
76        }
77        let result = self.storage.get(handle);
78        if result.is_none() {
79            tracing::warn!(
80                "Assets<{}>: get() called with a stale handle {:?} — \
81                 the asset was likely removed or replaced since this handle was obtained",
82                std::any::type_name::<T>(),
83                handle
84            );
85        }
86        result
87    }
88
89    /// Mutably look up an asset by its raw handle.
90    pub fn get_mut(&mut self, handle: RawAssetHandle) -> Option<&mut T> {
91        if handle.is_null() {
92            tracing::warn!(
93                "Assets<{}>: get_mut() called with a null/default handle — \
94                 did you forget to insert the asset and store the returned handle?",
95                std::any::type_name::<T>()
96            );
97            return None;
98        }
99        let result = self.storage.get_mut(handle);
100        if result.is_none() {
101            tracing::warn!(
102                "Assets<{}>: get_mut() called with a stale handle {:?} — \
103                 the asset was likely removed or replaced since this handle was obtained",
104                std::any::type_name::<T>(),
105                handle
106            );
107        }
108        result
109    }
110
111    /// Look up an asset by its name.
112    pub fn get_by_name(&self, name: &str) -> Option<&T> {
113        let result = self.handles
114            .get(name)
115            .and_then(|&handle| self.storage.get(handle));
116        if result.is_none() {
117            tracing::debug!(
118                "Assets<{}>: get_by_name({:?}) found no asset — \
119                 the name may not have been inserted yet",
120                std::any::type_name::<T>(),
121                name
122            );
123        }
124        result
125    }
126
127    /// Mutably look up an asset by its name.
128    pub fn get_mut_by_name(&mut self, name: &str) -> Option<&mut T> {
129        let handle = self.handles.get(name).copied();
130        if handle.is_none() {
131            tracing::debug!(
132                "Assets<{}>: get_mut_by_name({:?}) found no asset — \
133                 the name may not have been inserted yet",
134                std::any::type_name::<T>(),
135                name
136            );
137            return None;
138        }
139        self.storage.get_mut(handle.unwrap())
140    }
141
142    /// Look up an asset handle by its name.
143    pub fn get_handle_by_name(&self, name: &str) -> Option<RawAssetHandle> {
144        self.handles.get(name).copied()
145    }
146
147    /// Drain and return all handles currently in the dirty queue.
148    ///
149    /// Called by the asset sync system each tick.
150    pub fn take_dirty(&mut self) -> Vec<RawAssetHandle> {
151        std::mem::take(&mut self.queue)
152    }
153
154    /// Remove an asset by handle, returning the value if it existed.
155    pub fn remove(&mut self, handle: RawAssetHandle) -> Option<T> {
156        if handle.is_null() {
157            tracing::warn!(
158                "Assets<{}>: remove() called with a null/default handle — no-op",
159                std::any::type_name::<T>()
160            );
161            return None;
162        }
163        let value = self.storage.remove(handle)?;
164
165        self.handles.retain(|_, h| *h != handle);
166        self.queue.retain(|h| *h != handle);
167        self.removed.push(handle);
168
169        Some(value)
170    }
171
172    /// Remove an asset by name, returning the value if it existed.
173    pub fn remove_by_name(&mut self, name: &str) -> Option<T> {
174        let handle = self.handles.remove(name)?;
175        self.queue.retain(|h| *h != handle);
176        self.removed.push(handle);
177        self.storage.remove(handle)
178    }
179
180    /// Drain and return all handles removed since the last call.
181    ///
182    /// Called by the asset sync system each tick to evict stale processed assets.
183    pub fn take_removed(&mut self) -> Vec<RawAssetHandle> {
184        std::mem::take(&mut self.removed)
185    }
186
187    /// Returns `true` if the dirty queue is empty.
188    pub fn dirty_is_empty(&self) -> bool {
189        self.queue.is_empty()
190    }
191
192    /// Returns the number of handles currently in the dirty queue.
193    pub fn dirty_len(&self) -> usize {
194        self.queue.len()
195    }
196
197    /// Push `handles` back onto the dirty queue so they are retried next tick.
198    pub fn requeue(&mut self, handles: Vec<RawAssetHandle>) {
199        self.queue.extend(handles);
200    }
201
202    /// Replace the data for an existing asset by handle, re-queuing it for
203    /// upload. Returns `false` if the handle is null or not present.
204    ///
205    /// This is the handle-based counterpart to [`insert`](Self::insert): use it
206    /// when you already have the handle and don't need to go through a name
207    /// lookup. The handle stays valid — only the underlying data changes.
208    pub fn replace(&mut self, handle: RawAssetHandle, asset: T) -> bool {
209        if handle.is_null() {
210            tracing::warn!(
211                "Assets<{}>: replace() called with a null/default handle — no-op",
212                std::any::type_name::<T>()
213            );
214            return false;
215        }
216        let Some(slot) = self.storage.get_mut(handle) else {
217            tracing::warn!(
218                "Assets<{}>: replace() called with a stale handle {:?} — no-op",
219                std::any::type_name::<T>(),
220                handle
221            );
222            return false;
223        };
224        *slot = asset;
225        if !self.queue.contains(&handle) {
226            self.queue.push(handle);
227        }
228        tracing::debug!(
229            "Assets<{}>: replaced data for {:?}{} via handle",
230            std::any::type_name::<T>(),
231            handle,
232            self.name_for_handle(handle)
233                .map(|n| format!(" ({n})"))
234                .unwrap_or_default()
235        );
236        true
237    }
238
239    /// Mark a single asset as dirty so the sync system re-uploads it next tick,
240    /// even though its source data has not changed.
241    ///
242    /// Use this when a resource the asset *depends on* has been recreated (e.g.
243    /// a texture that a material instance's bind group references was resized),
244    /// requiring the processed asset to be rebuilt with the new version.
245    ///
246    /// Does nothing if `handle` is null or not present in this store.
247    pub fn mark_dirty(&mut self, handle: RawAssetHandle) {
248        if handle.is_null() {
249            tracing::warn!(
250                "Assets<{}>: mark_dirty() called with a null/default handle — no-op",
251                std::any::type_name::<T>()
252            );
253            return;
254        }
255        if self.storage.contains_key(handle) && !self.queue.contains(&handle) {
256            self.queue.push(handle);
257        }
258    }
259
260    /// Iterate over all assets by handle.
261    pub fn iter(&self) -> impl Iterator<Item = (RawAssetHandle, &T)> {
262        self.storage.iter()
263    }
264
265    /// Mutably iterate over all assets by handle.
266    pub fn iter_mut(&mut self) -> impl Iterator<Item = (RawAssetHandle, &mut T)> {
267        self.storage.iter_mut()
268    }
269
270    /// Iterate over `(name, handle)` pairs for every named asset.
271    pub fn names(&self) -> impl Iterator<Item = (&str, RawAssetHandle)> {
272        self.handles
273            .iter()
274            .map(|(name, &handle)| (name.as_str(), handle))
275    }
276
277    pub fn name_for_handle(&self, handle: RawAssetHandle) -> Option<&str> {
278        self.handles
279            .iter()
280            .find(|(_, h)| **h == handle)
281            .map(|(name, _)| name.as_str())
282    }
283}
284
285impl<'a, T: 'static + Send + Sync> IntoIterator for &'a Assets<T> {
286    type Item = (RawAssetHandle, &'a T);
287    type IntoIter = slotmap::basic::Iter<'a, RawAssetHandle, T>;
288
289    fn into_iter(self) -> Self::IntoIter {
290        self.storage.iter()
291    }
292}
293
294impl<'a, T: 'static + Send + Sync> IntoIterator for &'a mut Assets<T> {
295    type Item = (RawAssetHandle, &'a mut T);
296    type IntoIter = slotmap::basic::IterMut<'a, RawAssetHandle, T>;
297
298    fn into_iter(self) -> Self::IntoIter {
299        self.storage.iter_mut()
300    }
301}
302
303/// Storage for backend-processed (GPU) assets indexed by the same
304/// [`RawAssetHandle`] as their source in [`Assets`].
305///
306/// Populated by the asset sync system after a successful [`Asset::upload`](crate::assets::upload::Asset::upload).
307pub struct ProcessedAssets<T: 'static + Send + Sync> {
308    storage: SecondaryMap<RawAssetHandle, T>,
309    pub(crate) names: HashMap<String, RawAssetHandle>,
310}
311
312impl<T: 'static + Send + Sync> ProcessedAssets<T> {
313    pub fn new() -> Self {
314        Self {
315            storage: SecondaryMap::new(),
316            names: HashMap::new(),
317        }
318    }
319
320    /// Store a processed asset, returning the previous value if one existed.
321    pub fn insert(&mut self, handle: RawAssetHandle, asset: T) -> Option<T> {
322        self.storage.insert(handle, asset)
323    }
324
325    /// Look up a processed asset by handle.
326    pub fn get(&self, handle: RawAssetHandle) -> Option<&T> {
327        if handle.is_null() {
328            tracing::warn!(
329                "ProcessedAssets<{}>: get() called with a null/default handle — \
330                 did you forget to insert the source asset and store the returned handle?",
331                std::any::type_name::<T>()
332            );
333            return None;
334        }
335        let result = self.storage.get(handle);
336        if result.is_none() {
337            tracing::debug!(
338                "ProcessedAssets<{}>: get() for handle {:?} returned nothing — \
339                 the asset may still be pending upload or was removed",
340                std::any::type_name::<T>(),
341                handle
342            );
343        }
344        result
345    }
346
347    /// Mutably look up a processed asset by handle.
348    pub fn get_mut(&mut self, handle: RawAssetHandle) -> Option<&mut T> {
349        if handle.is_null() {
350            tracing::warn!(
351                "ProcessedAssets<{}>: get_mut() called with a null/default handle — \
352                 did you forget to insert the source asset and store the returned handle?",
353                std::any::type_name::<T>()
354            );
355            return None;
356        }
357        let result = self.storage.get_mut(handle);
358        if result.is_none() {
359            tracing::debug!(
360                "ProcessedAssets<{}>: get_mut() for handle {:?} returned nothing — \
361                 the asset may still be pending upload or was removed",
362                std::any::type_name::<T>(),
363                handle
364            );
365        }
366        result
367    }
368
369    /// Remove a processed asset by handle, returning the value if it existed.
370    pub fn remove(&mut self, handle: RawAssetHandle) -> Option<T> {
371        self.names.retain(|_, h| *h != handle);
372        self.storage.remove(handle)
373    }
374
375    /// Returns `true` if a processed asset exists for `handle`.
376    pub fn contains(&self, handle: RawAssetHandle) -> bool {
377        self.storage.contains_key(handle)
378    }
379
380    /// Iterate over all processed assets by handle.
381    pub fn iter(&self) -> impl Iterator<Item = (RawAssetHandle, &T)> {
382        self.storage.iter()
383    }
384
385    /// Mutably iterate over all processed assets by handle.
386    pub fn iter_mut(&mut self) -> impl Iterator<Item = (RawAssetHandle, &mut T)> {
387        self.storage.iter_mut()
388    }
389
390    pub fn get_by_name(&self, name: &str) -> Option<&T> {
391        let handle = self.names.get(name)?;
392        self.storage.get(*handle)
393    }
394}
395
396impl<'a, T: 'static + Send + Sync> IntoIterator for &'a ProcessedAssets<T> {
397    type Item = (RawAssetHandle, &'a T);
398    type IntoIter = slotmap::secondary::Iter<'a, RawAssetHandle, T>;
399
400    fn into_iter(self) -> Self::IntoIter {
401        self.storage.iter()
402    }
403}
404
405impl<'a, T: 'static + Send + Sync> IntoIterator for &'a mut ProcessedAssets<T> {
406    type Item = (RawAssetHandle, &'a mut T);
407    type IntoIter = slotmap::secondary::IterMut<'a, RawAssetHandle, T>;
408
409    fn into_iter(self) -> Self::IntoIter {
410        self.storage.iter_mut()
411    }
412}