1use crate::sync::MutexExt;
27use std::{
28 any::{Any, TypeId},
29 collections::BTreeMap,
30 io::{self, Read},
31 path::{Path, PathBuf},
32 sync::{
33 Arc, Mutex, OnceLock, Weak,
34 atomic::{AtomicBool, AtomicUsize, Ordering},
35 },
36};
37
38use handle::Entry;
39
40pub mod environment;
41mod handle;
42mod store;
43#[cfg(test)]
44mod tests;
45
46pub use environment::{FontEnvironment, FontIdentity};
47pub use handle::{AssetState, Handle};
48pub use store::{ResourceLimits, ResourceStore};
49
50const MAX_ENCODED: usize = 64 * 1024 * 1024;
51
52const MAX_STORE_BYTES: usize = 256 * 1024 * 1024;
53
54const MAX_ENTRIES: usize = 4096;
55
56thread_local! {
57 static RESOLVING: std::cell::RefCell<Vec<usize>> = const { std::cell::RefCell::new(Vec::new()) };
58}
59
60#[derive(Clone, Default, Debug)]
64pub struct RequestDiagnostics(Arc<Mutex<RequestFailures>>);
65
66#[derive(Default, Debug)]
67struct RequestFailures {
68 loads: Vec<Arc<crate::Error>>,
69 scene: crate::ErrorReport,
70}
71
72impl RequestDiagnostics {
73 fn locked(&self) -> std::sync::MutexGuard<'_, RequestFailures> {
74 self.0.lock_unpoisoned()
75 }
76
77 pub fn record(&self, error: Arc<crate::Error>) {
79 let mut failures = self.locked();
80 if !failures
81 .loads
82 .iter()
83 .any(|existing| Arc::ptr_eq(existing, &error))
84 {
85 failures.loads.push(error);
86 }
87 }
88
89 pub fn record_scene(&self, error: Arc<crate::Error>) {
91 let mut failures = self.locked();
92 if !failures
93 .scene
94 .errors()
95 .iter()
96 .any(|existing| Arc::ptr_eq(existing, &error))
97 {
98 failures.scene.record_shared(error);
99 }
100 }
101
102 pub fn errors(&self) -> Vec<Arc<crate::Error>> {
104 self.locked().loads.clone()
105 }
106
107 pub fn scene_errors(&self) -> crate::ErrorReport {
109 self.locked().scene.clone()
110 }
111}
112
113struct ResolveGuard;
114
115impl Drop for ResolveGuard {
116 fn drop(&mut self) {
117 RESOLVING.with(|entries| {
118 entries.borrow_mut().pop();
119 });
120 }
121}
122
123pub trait Asset: Sized + Send + Sync + 'static {
125 const KIND: &'static str;
127
128 fn decode(bytes: &[u8], context: &LoadContext) -> crate::Result<Self>;
132
133 fn retained_bytes(&self) -> usize {
136 std::mem::size_of::<Self>()
137 }
138}
139
140#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
143pub enum Source {
144 File(PathBuf),
146 Bytes {
148 key: Arc<str>,
150 data: Arc<[u8]>,
152 },
153}
154
155impl std::fmt::Debug for Source {
156 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::File(path) => formatter.debug_tuple("File").field(path).finish(),
159 Self::Bytes { key, data } => formatter
160 .debug_struct("Bytes")
161 .field("key", key)
162 .field("length", &data.len())
163 .finish(),
164 }
165 }
166}
167
168impl From<PathBuf> for Source {
169 fn from(path: PathBuf) -> Self {
170 Self::File(path)
171 }
172}
173
174impl From<&Path> for Source {
175 fn from(path: &Path) -> Self {
176 Self::File(path.to_path_buf())
177 }
178}
179
180impl From<&str> for Source {
181 fn from(path: &str) -> Self {
182 Self::File(path.into())
183 }
184}
185
186impl From<String> for Source {
187 fn from(path: String) -> Self {
188 Self::File(path.into())
189 }
190}
191
192#[derive(Clone, Debug)]
194pub struct LoadContext {
195 root: Arc<PathBuf>,
196 fonts: Option<crate::asset::environment::FontEnvironment>,
197 namespace: u64,
198}
199
200impl LoadContext {
201 pub fn fonts(&self) -> Option<&crate::asset::environment::FontEnvironment> {
203 self.fonts.as_ref()
204 }
205
206 pub fn root(&self) -> &Path {
208 &self.root
209 }
210
211 pub fn max_encoded_bytes(&self) -> usize {
213 MAX_ENCODED
214 }
215}
216
217type CacheKey = (TypeId, PathBuf, u64, u64, Source);
218
219type Erased = dyn Any + Send + Sync;
220
221struct OwnedEntry {
222 value: Arc<Erased>,
223 initialized: Box<dyn Fn() -> bool + Send + Sync>,
224}
225
226impl OwnedEntry {
227 fn new<AssetValue: Asset>(entry: Arc<Entry<AssetValue>>) -> Arc<Self> {
228 let weak = Arc::downgrade(&entry);
229 Arc::new(Self {
230 value: entry,
231 initialized: Box::new(move || {
232 weak.upgrade()
233 .is_some_and(|entry| entry.result.get().is_some())
234 }),
235 })
236 }
237}
238
239#[derive(Clone)]
243pub struct Assets {
244 context: LoadContext,
245 entries: Arc<Mutex<BTreeMap<CacheKey, Arc<OwnedEntry>>>>,
246 store: ResourceStore,
247 diagnostics: Option<RequestDiagnostics>,
248}
249
250impl std::fmt::Debug for Assets {
251 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 formatter
253 .debug_struct("Assets")
254 .field("root", &self.context.root)
255 .finish_non_exhaustive()
256 }
257}
258
259impl Default for Assets {
260 fn default() -> Self {
261 Self {
262 context: LoadContext {
263 root: Arc::new(PathBuf::new()),
264 fonts: None,
265 namespace: 0,
266 },
267 entries: Arc::new(Mutex::new(BTreeMap::new())),
268 store: ResourceStore::new(),
269 diagnostics: None,
270 }
271 }
272}
273
274impl Assets {
275 pub fn with_diagnostics(mut self, diagnostics: RequestDiagnostics) -> Self {
278 self.diagnostics = Some(diagnostics);
279 self
280 }
281
282 pub fn snapshot(&self) -> Self {
285 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
286 let mut snapshot = self.clone();
287 snapshot.context.namespace = NEXT.fetch_add(1, Ordering::Relaxed);
288 let entries = self
289 .entries
290 .lock_unpoisoned()
291 .iter()
292 .filter(|(_, entry)| (entry.initialized)())
293 .map(|(key, entry)| {
294 let mut key = key.clone();
295 key.3 = snapshot.context.namespace;
296 (key, entry.clone())
297 })
298 .collect();
299 snapshot.entries = Arc::new(Mutex::new(entries));
300 snapshot
301 }
302
303 pub fn preload<A: Asset>(
306 &self,
307 source: impl Into<Source>,
308 value: Arc<A>,
309 ) -> crate::Result<Handle<A>> {
310 let handle = self.load::<A>(source);
311 let inserted = std::cell::Cell::new(false);
312 let result = handle.entry.result.get_or_init(|| {
313 inserted.set(true);
314 let charge = value
315 .retained_bytes()
316 .checked_add(handle.entry.charged.load(Ordering::Acquire));
317 let result = charge
318 .ok_or_else(|| {
319 io::Error::new(
320 io::ErrorKind::OutOfMemory,
321 "Asset payload accounting overflow",
322 )
323 })
324 .and_then(|charge| {
325 let additional = charge - handle.entry.charged.load(Ordering::Acquire);
326 self.store
327 .0
328 .bytes
329 .try_update(Ordering::AcqRel, Ordering::Acquire, |current| {
330 current
331 .checked_add(additional)
332 .filter(|total| *total <= self.store.0.limits.max_bytes)
333 })
334 .map_err(|_| {
335 io::Error::new(
336 io::ErrorKind::OutOfMemory,
337 "Shared asset-cache logical payload exceeds its allowance",
338 )
339 })?;
340 handle.entry.charged.store(charge, Ordering::Release);
341 Ok(value)
342 });
343 result.map_err(|error| {
344 Arc::new(crate::Error::asset(A::KIND, handle.source_name(), error))
345 })
346 });
347 if let Err(error) = result {
348 return Err(crate::Error::asset(
349 A::KIND,
350 handle.source_name(),
351 error.clone(),
352 ));
353 }
354 if !inserted.get() {
355 return Err(io::Error::new(
356 io::ErrorKind::AlreadyExists,
357 "Asset source is already resolved",
358 )
359 .into());
360 }
361 Ok(handle)
362 }
363
364 pub fn with_store(mut self, store: ResourceStore) -> Self {
367 self.store = store;
368 self.entries = Arc::new(Mutex::new(BTreeMap::new()));
369 self
370 }
371
372 pub fn store(&self) -> &ResourceStore {
374 &self.store
375 }
376
377 pub fn remove<A: Asset>(&self, source: impl Into<Source>) -> bool {
381 let mut source = source.into();
382 if let Source::File(path) = &mut source
383 && !path.is_absolute()
384 {
385 *path = self.context.root.join(&*path);
386 }
387 let key = (
388 TypeId::of::<A>(),
389 self.context.root.as_ref().clone(),
390 self.context
391 .fonts
392 .as_ref()
393 .map_or(0, crate::asset::environment::FontEnvironment::snapshot_id),
394 self.context.namespace,
395 source,
396 );
397 let mut entries = self.entries.lock_unpoisoned();
398 let Some(entry) = entries.remove(&key) else {
399 return false;
400 };
401 let mut global = self.store.0.entries.lock_unpoisoned();
402 if global
403 .get(&key)
404 .and_then(Weak::upgrade)
405 .is_some_and(|value| Arc::ptr_eq(&value, &entry.value))
406 {
407 global.remove(&key);
408 }
409 true
410 }
411
412 pub fn with_fonts(
415 mut self,
416 fonts: impl Into<crate::asset::environment::FontEnvironment>,
417 ) -> Self {
418 self.context.fonts = Some(fonts.into());
419 self.entries = Arc::new(Mutex::new(BTreeMap::new()));
420 self
421 }
422
423 pub fn fonts(&self) -> Option<&crate::asset::environment::FontEnvironment> {
425 self.context.fonts()
426 }
427
428 pub fn new(root: impl AsRef<Path>) -> crate::Result<Self> {
430 Ok(Self {
431 context: LoadContext {
432 root: Arc::new(std::path::absolute(root)?),
433 fonts: None,
434 namespace: 0,
435 },
436 entries: Arc::new(Mutex::new(BTreeMap::new())),
437 store: ResourceStore::new(),
438 diagnostics: None,
439 })
440 }
441
442 pub fn load<A: Asset>(&self, source: impl Into<Source>) -> Handle<A> {
446 let mut source = source.into();
447 let failure = if let Source::File(path) = &mut source {
448 if self.context.root.as_os_str().is_empty() && !path.is_absolute() {
449 Some("Relative file assets require an explicit project root")
450 } else if path
451 .components()
452 .any(|component| component == std::path::Component::ParentDir)
453 {
454 Some("Asset paths must not contain parent traversal")
455 } else {
456 if !path.is_absolute() {
457 *path = self.context.root.join(&*path);
458 }
459 None
460 }
461 } else if let Source::Bytes { key, data } = &source {
462 if data.len() > MAX_ENCODED {
463 Some("Asset source exceeds 64 MiB")
464 } else if key.is_empty() || key.len() > 1024 || key.chars().any(char::is_control) {
465 Some("Asset byte keys require 1..=1024 bytes without control characters")
466 } else {
467 None
468 }
469 } else {
470 None
471 };
472 let key = (
473 TypeId::of::<A>(),
474 self.context.root.as_ref().clone(),
475 self.context
476 .fonts
477 .as_ref()
478 .map_or(0, crate::asset::environment::FontEnvironment::snapshot_id),
479 self.context.namespace,
480 source.clone(),
481 );
482 let mut entries = self.entries.lock_unpoisoned();
483 if failure.is_none()
484 && let Some(entry) = entries.get(&key)
485 {
486 return Handle {
487 entry: entry
488 .value
489 .clone()
490 .downcast::<Entry<A>>()
491 .expect("typed resource key"),
492 diagnostics: self.diagnostics.clone(),
493 };
494 }
495 let mut global = self.store.0.entries.lock_unpoisoned();
496 global.retain(|_, entry| entry.strong_count() > 0);
497 let failure = failure.or_else(|| {
498 (entries.len() >= self.store.0.limits.max_entries)
499 .then_some("Resource cache exceeds its entry allowance")
500 });
501 if failure.is_none()
502 && let Some(entry) = global.get(&key).and_then(Weak::upgrade)
503 {
504 let entry = entry.downcast::<Entry<A>>().expect("typed global key");
505 entries.insert(key, OwnedEntry::new(entry.clone()));
506 return Handle {
507 entry,
508 diagnostics: self.diagnostics.clone(),
509 };
510 }
511 let mut failure = failure.or_else(|| {
512 (global.len() >= self.store.0.limits.max_entries)
513 .then_some("Resource cache exceeds its entry allowance")
514 });
515 let mut charged = 0;
516 if failure.is_none()
517 && let Source::Bytes { data, .. } = &source
518 {
519 if self
520 .store
521 .0
522 .bytes
523 .try_update(Ordering::AcqRel, Ordering::Acquire, |current| {
524 current
525 .checked_add(data.len())
526 .filter(|total| *total <= self.store.0.limits.max_bytes)
527 })
528 .is_ok()
529 {
530 charged = data.len();
531 } else {
532 failure = Some("Shared asset-cache logical payload exceeds its allowance");
533 }
534 }
535 if failure.is_some()
536 && let Source::Bytes { data, .. } = &mut source
537 {
538 *data = Arc::from([]);
539 }
540 let entry = Arc::new(Entry {
541 store: self.store.clone(),
542 source,
543 context: self.context.clone(),
544 result: OnceLock::new(),
545 loading: AtomicBool::new(false),
546 charged: AtomicUsize::new(charged),
547 });
548 if let Some(message) = failure {
549 let source_name = match &entry.source {
550 Source::File(path) => path.display().to_string(),
551 Source::Bytes { key, .. } => key.to_string(),
552 };
553 let _result = entry.result.set(Err(Arc::new(crate::Error::asset(
554 A::KIND,
555 source_name,
556 io::Error::new(io::ErrorKind::InvalidInput, message),
557 ))));
558 } else {
559 let erased: Arc<Erased> = entry.clone();
560 global.insert(key.clone(), Arc::downgrade(&erased));
561 entries.insert(key, OwnedEntry::new(entry.clone()));
562 }
563 Handle {
564 entry,
565 diagnostics: self.diagnostics.clone(),
566 }
567 }
568}