Skip to main content

pebble/assets/
storage.rs

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