tessera_ui/
asset.rs

1//! Asset reading abstractions for generated resource handles.
2//!
3//! ## Usage
4//!
5//! Use generated asset constants and call [`AssetExt::read`] to load raw bytes.
6
7use std::{
8    any::TypeId,
9    collections::HashMap,
10    io,
11    sync::{Arc, OnceLock, RwLock, RwLockWriteGuard},
12};
13
14type AssetCacheKey = (TypeId, u64);
15type AssetCacheMap = HashMap<AssetCacheKey, AssetCacheEntry>;
16
17const CACHE_MAX_ENTRIES: usize = 4096;
18const CACHE_MAX_BYTES: usize = 64 * 1024 * 1024;
19
20static ASSET_BYTES_CACHE: OnceLock<RwLock<AssetLruCache>> = OnceLock::new();
21
22#[derive(Clone)]
23struct AssetCacheEntry {
24    bytes: Arc<[u8]>,
25    len: usize,
26    last_access_tick: u64,
27}
28
29#[derive(Default)]
30struct AssetLruCache {
31    entries: AssetCacheMap,
32    total_bytes: usize,
33    next_tick: u64,
34}
35
36impl AssetLruCache {
37    fn get(&mut self, key: AssetCacheKey) -> Option<Arc<[u8]>> {
38        let tick = self.bump_tick();
39        if let Some(entry) = self.entries.get_mut(&key) {
40            entry.last_access_tick = tick;
41            return Some(entry.bytes.clone());
42        }
43        None
44    }
45
46    fn insert(&mut self, key: AssetCacheKey, bytes: Arc<[u8]>) {
47        let tick = self.bump_tick();
48        let entry_len = bytes.len();
49        let entry = AssetCacheEntry {
50            len: entry_len,
51            bytes,
52            last_access_tick: tick,
53        };
54
55        if let Some(previous) = self.entries.insert(key, entry) {
56            self.total_bytes = self.total_bytes.saturating_sub(previous.len);
57        }
58        self.total_bytes = self.total_bytes.saturating_add(entry_len);
59        self.evict_if_needed();
60    }
61
62    fn bump_tick(&mut self) -> u64 {
63        self.next_tick = self.next_tick.wrapping_add(1);
64        self.next_tick
65    }
66
67    fn evict_if_needed(&mut self) {
68        while (self.total_bytes > CACHE_MAX_BYTES || self.entries.len() > CACHE_MAX_ENTRIES)
69            && self.entries.len() > 1
70        {
71            let Some(victim_key) = self
72                .entries
73                .iter()
74                .min_by_key(|(_, entry)| entry.last_access_tick)
75                .map(|(key, _)| *key)
76            else {
77                break;
78            };
79
80            if let Some(removed) = self.entries.remove(&victim_key) {
81                self.total_bytes = self.total_bytes.saturating_sub(removed.len);
82            }
83        }
84    }
85}
86
87/// Trait implemented by generated asset handle types.
88pub trait AssetExt: Copy {
89    /// Read raw bytes for this asset.
90    fn read(self) -> io::Result<Arc<[u8]>>;
91}
92
93/// Shared helper for generated asset readers that adds an in-memory LRU cache.
94///
95/// Each generated asset type should call this function from `AssetExt::read`,
96/// passing its `index` as `asset_id` and a backend-specific loader closure.
97#[doc(hidden)]
98pub fn read_with_lru_cache<T, F>(asset_id: u64, loader: F) -> io::Result<Arc<[u8]>>
99where
100    T: 'static,
101    F: FnOnce() -> io::Result<Arc<[u8]>>,
102{
103    let cache_key = (TypeId::of::<T>(), asset_id);
104
105    {
106        let mut cache = cache_write();
107        if let Some(bytes) = cache.get(cache_key) {
108            return Ok(bytes);
109        }
110    }
111
112    let loaded = loader()?;
113
114    let mut cache = cache_write();
115    if let Some(bytes) = cache.get(cache_key) {
116        return Ok(bytes);
117    }
118
119    cache.insert(cache_key, loaded.clone());
120    Ok(loaded)
121}
122
123/// Backward-compatible alias kept for previously generated code.
124#[doc(hidden)]
125pub fn read_with_weak_cache<T, F>(asset_id: u64, loader: F) -> io::Result<Arc<[u8]>>
126where
127    T: 'static,
128    F: FnOnce() -> io::Result<Arc<[u8]>>,
129{
130    read_with_lru_cache::<T, F>(asset_id, loader)
131}
132
133fn cache_instance() -> &'static RwLock<AssetLruCache> {
134    ASSET_BYTES_CACHE.get_or_init(|| RwLock::new(AssetLruCache::default()))
135}
136
137fn cache_write() -> RwLockWriteGuard<'static, AssetLruCache> {
138    match cache_instance().write() {
139        Ok(guard) => guard,
140        Err(poisoned) => poisoned.into_inner(),
141    }
142}