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