Skip to main content

pebble/assets/
storage.rs

1use slotmap::{Key as _, SlotMap, new_key_type};
2use std::collections::HashMap;
3
4use crate::assets::{handle::Handle, upload::AssetSource};
5
6new_key_type! {
7    /// Untyped slot-map key for an asset entry.
8    ///
9    /// Prefer the typed [`Handle<T>`](crate::assets::handle::Handle) in
10    /// most code. `RawAssetHandle` is used internally by the storage and
11    /// sync systems.
12    pub struct RawAssetHandle;
13}
14
15fn warn_if_null<T>(handle: RawAssetHandle, method: &str, on_null: &str) -> bool {
16    if handle.is_null() {
17        tracing::warn!(
18            "Assets<{}>: {method}() called with a null/default handle — {on_null}",
19            std::any::type_name::<T>()
20        );
21        true
22    } else {
23        false
24    }
25}
26
27struct AssetEntry<T: AssetSource> {
28    source: T,
29    processed: Option<T::Processed>,
30}
31
32/// Unified storage for source and processed assets of type `T`.
33///
34/// Each entry holds both the raw source data (`T`) and the uploaded result
35/// (`T::Processed`), keyed by the same [`Handle<T>`].
36/// [`AssetPlugin`](crate::assets::plugin::AssetPlugin) fills in `processed`
37/// after a successful [`Asset::upload`](crate::assets::upload::Asset::upload).
38///
39/// Use [`get`](Self::get) to retrieve the processed result (e.g. for
40/// rendering), and [`get_source`](Self::get_source) to access the raw data.
41pub struct Assets<T: AssetSource> {
42    storage: SlotMap<RawAssetHandle, AssetEntry<T>>,
43    handles: HashMap<String, RawAssetHandle>,
44    queue: Vec<RawAssetHandle>,
45    removed: Vec<RawAssetHandle>,
46}
47
48impl<T: AssetSource> Assets<T> {
49    pub fn new() -> Self {
50        Self {
51            storage: SlotMap::with_key(),
52            handles: HashMap::new(),
53            queue: Vec::new(),
54            removed: Vec::new(),
55        }
56    }
57
58    /// Insert `source` under `name`, returning its handle.
59    ///
60    /// If an asset with the same name already exists, its source data is
61    /// replaced **in-place** (the same handle is reused and re-queued for
62    /// re-upload), and any previously processed result is cleared.
63    pub fn insert(&mut self, name: &str, source: T) -> Handle<T> {
64        if let Some(&existing) = self.handles.get(name) {
65            if let Some(entry) = self.storage.get_mut(existing) {
66                entry.source = source;
67                entry.processed = None;
68                if !self.queue.contains(&existing) {
69                    self.queue.push(existing);
70                }
71                tracing::debug!(
72                    "Assets<{}>: replaced source for {:?} ({name}) in-place",
73                    std::any::type_name::<T>(),
74                    existing
75                );
76                return Handle::new(existing);
77            }
78        }
79        let handle = self.storage.insert(AssetEntry { source, processed: None });
80        self.handles.insert(name.to_string(), handle);
81        self.queue.push(handle);
82        Handle::new(handle)
83    }
84
85    /// Look up the processed (uploaded) asset for `handle`.
86    ///
87    /// Returns `None` if the handle is null, stale, or the asset has not
88    /// finished uploading yet.
89    pub fn get(&self, handle: Handle<T>) -> Option<&T::Processed> {
90        let id = handle.id;
91        if warn_if_null::<T>(id, "get", "did you forget to insert the asset and store the returned handle?") {
92            return None;
93        }
94        match self.storage.get(id) {
95            None => {
96                tracing::warn!(
97                    "Assets<{}>: get() called with a stale handle {:?} — \
98                     the asset was likely removed since this handle was obtained",
99                    std::any::type_name::<T>(),
100                    id
101                );
102                None
103            }
104            Some(entry) => {
105                if entry.processed.is_none() {
106                    tracing::debug!(
107                        "Assets<{}>: get() for {:?} returned None — \
108                         the asset may still be pending upload",
109                        std::any::type_name::<T>(),
110                        id
111                    );
112                }
113                entry.processed.as_ref()
114            }
115        }
116    }
117
118    /// Look up the raw source data for `handle`.
119    pub fn get_source(&self, handle: Handle<T>) -> Option<&T> {
120        let id = handle.id;
121        if warn_if_null::<T>(id, "get_source", "did you forget to insert the asset and store the returned handle?") {
122            return None;
123        }
124        let result = self.storage.get(id).map(|e| &e.source);
125        if result.is_none() {
126            tracing::warn!(
127                "Assets<{}>: get_source() called with a stale handle {:?}",
128                std::any::type_name::<T>(),
129                id
130            );
131        }
132        result
133    }
134
135    /// Mutably look up the raw source data for `handle`.
136    pub fn get_source_mut(&mut self, handle: Handle<T>) -> Option<&mut T> {
137        let id = handle.id;
138        if warn_if_null::<T>(id, "get_source_mut", "did you forget to insert the asset and store the returned handle?") {
139            return None;
140        }
141        let result = self.storage.get_mut(id).map(|e| &mut e.source);
142        if result.is_none() {
143            tracing::warn!(
144                "Assets<{}>: get_source_mut() called with a stale handle {:?}",
145                std::any::type_name::<T>(),
146                id
147            );
148        }
149        result
150    }
151
152    /// Returns `true` if `handle` exists and its processed asset is ready.
153    /// Never logs — safe to poll speculatively.
154    pub fn is_ready(&self, handle: Handle<T>) -> bool {
155        self.storage.get(handle.id).is_some_and(|e| e.processed.is_some())
156    }
157
158    /// Returns `true` if `handle` currently refers to a present entry.
159    /// Never logs — safe to poll speculatively.
160    pub fn contains(&self, handle: Handle<T>) -> bool {
161        self.storage.contains_key(handle.id)
162    }
163
164    /// Look up the processed asset by the name it was inserted under.
165    /// `None` if the name is unknown or the asset hasn't uploaded yet —
166    /// not logged, since "still uploading" is a normal transient state.
167    pub fn get_by_name(&self, name: &str) -> Option<&T::Processed> {
168        let handle = self.handles.get(name)?;
169        self.storage.get(*handle)?.processed.as_ref()
170    }
171
172    /// Look up the raw source data by the name it was inserted under.
173    pub fn get_source_by_name(&self, name: &str) -> Option<&T> {
174        let handle = self.handles.get(name)?;
175        Some(&self.storage.get(*handle)?.source)
176    }
177
178    /// Look up a handle by name.
179    pub fn get_handle_by_name(&self, name: &str) -> Option<Handle<T>> {
180        self.handles.get(name).copied().map(Handle::new)
181    }
182
183    /// Replace the source data for `handle`, invalidating the processed
184    /// result and re-queuing for upload. Returns `false` if the handle is
185    /// null or not present.
186    pub fn replace(&mut self, handle: Handle<T>, source: T) -> bool {
187        let id = handle.id;
188        if warn_if_null::<T>(id, "replace", "no-op") {
189            return false;
190        }
191        let Some(entry) = self.storage.get_mut(id) else {
192            tracing::warn!(
193                "Assets<{}>: replace() called with a stale handle {:?} — no-op",
194                std::any::type_name::<T>(),
195                id
196            );
197            return false;
198        };
199        entry.source = source;
200        entry.processed = None;
201        if !self.queue.contains(&id) {
202            self.queue.push(id);
203        }
204        tracing::debug!(
205            "Assets<{}>: replaced source for {:?}{} via handle",
206            std::any::type_name::<T>(),
207            id,
208            self.name_for_handle(id).map(|n| format!(" ({n})")).unwrap_or_default()
209        );
210        true
211    }
212
213    /// Mark a single asset as dirty so the sync system re-uploads it next
214    /// tick, even though its source data has not changed (e.g. a dependency
215    /// was recreated). Does nothing if the handle is null or not present.
216    pub fn mark_dirty(&mut self, handle: Handle<T>) {
217        let id = handle.id;
218        if warn_if_null::<T>(id, "mark_dirty", "no-op") {
219            return;
220        }
221        if self.storage.contains_key(id) && !self.queue.contains(&id) {
222            self.queue.push(id);
223        }
224    }
225
226    /// Remove an asset by handle, returning the source value if it existed.
227    /// The processed asset is also discarded.
228    pub fn remove(&mut self, handle: Handle<T>) -> Option<T> {
229        let id = handle.id;
230        if warn_if_null::<T>(id, "remove", "no-op") {
231            return None;
232        }
233        let entry = self.storage.remove(id)?;
234        self.handles.retain(|_, h| *h != id);
235        self.queue.retain(|h| *h != id);
236        self.removed.push(id);
237        Some(entry.source)
238    }
239
240    /// Remove an asset by name, returning the source value if it existed.
241    pub fn remove_by_name(&mut self, name: &str) -> Option<T> {
242        let id = self.handles.remove(name)?;
243        self.queue.retain(|h| *h != id);
244        self.removed.push(id);
245        Some(self.storage.remove(id)?.source)
246    }
247
248    /// Iterate over all entries that have a processed result ready.
249    pub fn iter(&self) -> impl Iterator<Item = (RawAssetHandle, &T::Processed)> {
250        self.storage
251            .iter()
252            .filter_map(|(h, e)| e.processed.as_ref().map(|p| (h, p)))
253    }
254
255    /// Iterate over all source entries regardless of upload state.
256    pub fn iter_source(&self) -> impl Iterator<Item = (RawAssetHandle, &T)> {
257        self.storage.iter().map(|(h, e)| (h, &e.source))
258    }
259
260    /// Iterate over `(name, handle)` pairs for every named asset.
261    pub fn names(&self) -> impl Iterator<Item = (&str, RawAssetHandle)> {
262        self.handles.iter().map(|(name, &handle)| (name.as_str(), handle))
263    }
264
265    // --- sync-system internals (pub(crate)) ---
266
267    /// Look up the source for `handle` without logging on a miss.
268    ///
269    /// Used by the sync system, which legitimately encounters handles
270    /// removed between being queued dirty and sync running.
271    pub(crate) fn get_source_quiet(&self, handle: RawAssetHandle) -> Option<&T> {
272        self.storage.get(handle).map(|e| &e.source)
273    }
274
275    /// Write the processed result for `handle` back into the entry.
276    pub(crate) fn set_processed(&mut self, handle: RawAssetHandle, processed: T::Processed) {
277        if let Some(entry) = self.storage.get_mut(handle) {
278            entry.processed = Some(processed);
279        }
280    }
281
282    /// Drain and return all handles currently in the dirty queue.
283    pub(crate) fn take_dirty(&mut self) -> Vec<RawAssetHandle> {
284        std::mem::take(&mut self.queue)
285    }
286
287    /// Drain and return all handles removed since the last call.
288    pub(crate) fn take_removed(&mut self) -> Vec<RawAssetHandle> {
289        std::mem::take(&mut self.removed)
290    }
291
292    /// Push `handles` back onto the dirty queue so they are retried next tick.
293    pub(crate) fn requeue(&mut self, handles: Vec<RawAssetHandle>) {
294        self.queue.extend(handles);
295    }
296
297    /// Returns `true` if the dirty queue is empty.
298    pub(crate) fn dirty_is_empty(&self) -> bool {
299        self.queue.is_empty()
300    }
301
302    /// Returns the number of handles currently in the dirty queue.
303    pub(crate) fn dirty_len(&self) -> usize {
304        self.queue.len()
305    }
306
307    /// Reverse lookup: the name `handle` was inserted under, if any.
308    /// O(n) scan — for diagnostics/logging only, not a hot path.
309    pub(crate) fn name_for_handle(&self, handle: RawAssetHandle) -> Option<&str> {
310        self.handles
311            .iter()
312            .find(|(_, h)| **h == handle)
313            .map(|(name, _)| name.as_str())
314    }
315}